Merge pull request 'better-bridge' (#1) from better-bridge into main

Reviewed-on: https://gitea.planet-casio.com/devs/shoutbridge/pulls/1
This commit is contained in:
Darks 2022-12-16 11:58:50 +01:00
commit 575ebee081
4 changed files with 366 additions and 241 deletions

View File

@ -1,35 +1,43 @@
import logging
from irc import IRC
from shoutbox import Shoutbox
from cookies import cookies
from sasl import nick, password
from users import users
LOG_FORMAT = "%(asctime)s [%(levelname)s] <%(filename)s> %(funcName)s: %(message)s"
logging.basicConfig(format=LOG_FORMAT, level=logging.INFO)
channels = ["hs", "projets", "annonces"]
irc = IRC('irc.planet-casio.com', 6697)
shoutbox = Shoutbox(cookies)
@irc.on(lambda m: m.to[1:] in channels)
def handle_irc(m):
shoutbox.post(m.author, m.text, m.to[1:], users)
@shoutbox.on(lambda m: m.channel in channels and m.author != "IRC" and not m.text.endswith("[IRC]"))
def handle_shoutbox(m):
irc.send(f"#{m.channel}", f"{m.author}: {m.text}")
irc.start("Shoutbox", password, nick)
for c in channels:
irc.join(f"#{c}")
shoutbox.run()
irc.run()
import logging
import sys
from irc import IRC
from shoutbox import Shoutbox
from cookies import cookies
from sasl import nick, password
from users import USERS
LOG_FORMAT = "%(asctime)s [%(levelname)s] %(threadName)s <%(filename)s> %(funcName)s: %(message)s"
logging.basicConfig(format=LOG_FORMAT, level=logging.DEBUG)
channels = ["hs", "projets", "annonces"]
irc = IRC('irc.planet-casio.com', 6697)
shoutbox = Shoutbox(cookies)
@irc.on(lambda m: m.to[1:] in channels and not m.author.endswith("[s]"))
def handle_irc(m):
shoutbox.post(m.author, m.text, m.to[1:], USERS)
@shoutbox.on(lambda m: m.channel in channels)
def handle_shoutbox(m):
author = Shoutbox.normalize(m.author)
shoutbox.irc_clients[author][0].send(f"#{m.channel}", f"{m.text}")
irc.start(f"Shoutbox[{nick}]", password, nick)
for c in channels:
irc.join(f"#{c}")
try:
shoutbox.run()
irc.run()
except KeyboardInterrupt:
irc.stop()
shoutbox.stop()
sys.exit(0)

305
irc.py
View File

@ -1,133 +1,172 @@
# Manage the IRC layer of GLaDOS
import logging, re, socket, ssl
from functools import wraps
from queue import Queue
from threading import Thread
class IRC(object):
def __init__(self, host, port):
""" Initialize an IRC wrapper """
# Public attributes
self.connected = False # Simple lock
# Private attributes
self._socket = ssl.create_default_context().wrap_socket(
socket.create_connection((host, port)),
server_hostname=host)
self._inbox = Queue()
self._handler = Thread(target=self._handle)
self._callbacks = []
# Public methods
def start(self, nick, password, sasl_nick=None):
""" Start the IRC layer. Manage authentication as well """
sasl_nick = sasl_nick or nick
self._handler.start()
self._send(f"USER {nick} * * :{nick}")
self._send(f"NICK {nick}")
self._waitfor(lambda m: "NOTICE" in m and "/AUTH" in m)
self._send(f"AUTH {sasl_nick}:{password}")
self._waitfor(lambda m: "You are now logged in" in m)
self.connected = True
def run(self):
""" Handle new messages """
while True:
message = self.receive()
logging.info(f"received {message}")
if message is not None:
for event, callback in self._callbacks:
if event(message):
logging.info(f"matched {event.__name__}")
callback(message)
def send(self, target, message):
""" Send a message to the specified target (channel or user) """
self._send(f"PRIVMSG {target} :{message}")
def receive(self):
""" Receive a private message """
while True:
message = self._recv()
if " PRIVMSG " in message:
msg = IRCMessage(message)
if msg:
return msg
def join(self, channel):
""" Join a channel """
self._send(f"JOIN {channel}")
logging.info(f"joined {channel}")
def on(self, event):
""" Adds a callback to the IRC handler
Event is a function taking in parameter a IRCMessage and returning
True if the callback should be executed on the message """
def callback(func):
@wraps(func)
def wrapper(message):
func(message)
self._callbacks.append((event, wrapper))
logging.info(f"added callback {func.__name__}")
return wrapper
return callback
# Private methods
def _handle(self):
""" Handle raw messages from irc and manage ping """
while True:
# Get incoming messages
data = self._socket.recv(4096).decode()
# Split multiple lines
for m in data.split('\r\n'):
# Manage ping
if m.startswith("PING"):
self._send(m.replace("PING", "PONG"))
# Or add a new message to inbox
elif len(m):
self._inbox.put(m)
logging.debug(f"received {m}")
def _send(self, raw):
""" Wrap and encode raw message to send """
self._socket.send(f"{raw}\r\n".encode())
def _recv(self):
m = self._inbox.get()
return m
def _waitfor(self, condition):
""" Wait for a raw message that matches the condition """
msg = self._recv()
while not condition(msg):
msg = self._recv()
return msg
class IRCMessage(object):
r = re.compile("^:(?P<author>[\w.~|]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)")
def __init__(self, raw):
match = re.search(IRCMessage.r, raw)
if match:
self.author = match.group("author")
self.to = match.group("to")
self.text = match.group("text")
logging.debug(f"sucessfully parsed {raw} into {self}")
else:
self.author = ""
self.to = ""
self.text = ""
logging.warning(f"failed to parse {raw} into valid message")
def __str__(self):
return f"{self.author} to {self.to}: {self.text}"
# Manage the IRC layer of GLaDOS
import logging
import re
import socket
import ssl
from functools import wraps
from queue import Queue
from threading import Thread
class IRC(object):
def __init__(self, host, port):
""" Initialize an IRC wrapper """
# Public attributes
self.connected = False # Simple lock
self.running = False
# Private attributes
self._socket = ssl.create_default_context().wrap_socket(
socket.create_connection((host, port)),
server_hostname=host)
self._inbox = Queue()
self._handler = Thread(target=self._handle)
self._callbacks = []
# Public methods
def start(self, nick, password, sasl_nick=None):
""" Start the IRC layer. Manage authentication as well
Return True if authentication succeed, False if failed"""
sasl_nick = sasl_nick or nick
self.nick = nick
self.running = True
logging.debug("Thread start")
self._handler.start()
self._send(f"USER {nick} * * :{nick}")
self._send(f"NICK {nick}")
self._waitfor(lambda m: "NOTICE" in m and "/AUTH" in m)
for i in range(3):
self._send(f"AUTH {sasl_nick}:{password}")
msg = self._waitfor(lambda m: "You are now logged in" in m or "Authentication failed" in m)
if "You are now logged in" in msg:
self.connected = True
return True
logging.info(f"Authentication for {nick} ({sasl_nick}) failed")
self.connected = False
self._handler.stop()
return False
def stop(self):
""" Stop the IRC layer """
self._send("QUIT :Bye bye")
logging.debug("STOP: sent QUIT message")
self.running = False
self._handler.join()
logging.debug("STOP: thread has terminated")
self._socket.close()
logging.debug("STOP: socket close")
def run(self):
""" Handle new messages """
while True:
message = self.receive()
logging.info(f"received {message}")
if message is not None:
for event, callback in self._callbacks:
if event(message):
logging.info(f"matched {event.__name__}")
callback(message)
def send(self, target, message):
""" Send a message to the specified target (channel or user) """
self._send(f"PRIVMSG {target} :{message}")
def receive(self):
""" Receive a private message """
while True:
message = self._recv()
if " PRIVMSG " in message:
msg = IRCMessage(message)
if msg:
return msg
logging.debug(f"skipped message {msg}")
def join(self, channel):
""" Join a channel """
self._send(f"JOIN {channel}")
logging.info(f"joined {channel}")
def on(self, event):
""" Adds a callback to the IRC handler
Event is a function taking in parameter a IRCMessage and returning
True if the callback should be executed on the message """
def callback(func):
@wraps(func)
def wrapper(message):
func(message)
self._callbacks.append((event, wrapper))
logging.info(f"added callback {func.__name__}")
return wrapper
return callback
# Private methods
def _handle(self, store=True):
""" Handle raw messages from irc and manage ping """
while self.running:
# Get incoming messages
data = self._socket.recv(4096).decode()
# Split multiple lines
for m in data.split('\r\n'):
# Manage ping
if m.startswith("PING"):
self._send(m.replace("PING", "PONG"))
# Or add a new message to inbox
elif len(m):
if store:
self._inbox.put(m)
logging.debug(f"_handle has quit: running={self.running}")
def _send(self, raw):
""" Wrap and encode raw message to send """
try:
self._socket.send(f"{raw}\r\n".encode())
except OSError as e:
logging.warning(e)
# Do not display password in logs
if raw.startswith("AUTH"):
raw = raw.split(":")[0] + ":*REDACTED*"
logging.debug(raw)
def _recv(self):
try:
m = self._inbox.get()
except OSError as e:
logging.warning(e)
logging.debug(m)
return m
def _waitfor(self, condition):
""" Wait for a raw message that matches the condition """
msg = self._recv()
while not condition(msg):
msg = self._recv()
return msg
class IRCMessage(object):
r = re.compile("^:(?P<author>[\w.~|\[\]]+)(?:!(?P<host>\S+))? PRIVMSG (?P<to>\S+) :(?P<text>.+)")
def __init__(self, raw):
match = re.search(IRCMessage.r, raw)
if match:
self.author = match.group("author")
self.to = match.group("to")
self.text = match.group("text")
logging.debug(f"sucessfully parsed {raw} into {self}")
else:
self.author = ""
self.to = ""
self.text = ""
logging.warning(f"failed to parse {raw} into valid message")
def __str__(self):
return f"{self.author} to {self.to}: {self.text}"

View File

@ -1,70 +1,136 @@
import json
import requests as r
import logging
import time
from functools import wraps
from threading import Thread
class Shoutbox(object):
def __init__(self, cookies):
self.channels = {'annonces': 0, 'projets': 0, 'hs': 0}
self.cookies = cookies
self._callbacks = []
for channel, last_id in self.channels.items():
messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages']
for m in messages:
self.channels[channel] = m['id']
def run(self):
def handler():
while True:
for channel, last_id in self.channels.items():
messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages']
for m in messages:
logging.debug(m)
self.channels[channel] = m['id']
message = SBMessage(m, channel)
for event, callback in self._callbacks:
if event(message):
logging.info(f"matched {event.__name__}")
callback(message)
time.sleep(1)
Thread(target=handler).start()
def on(self, event):
""" Adds a callback to the IRC handler
Event is a function taking in parameter a SBMessage and returning
True if the callback should be executed on the message """
def callback(func):
@wraps(func)
def wrapper(message):
func(message)
self._callbacks.append((event, wrapper))
logging.info(f"added callback {func.__name__}")
return wrapper
return callback
def post(self, user, msg, channel, users):
if msg.startswith("ACTION"):
msg = msg.replace("ACTION", "/me")
if any(user in t for t in users):
for i in users:
if i[1] == user:
r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as",
data={"user": i[0], "message": msg, "channel": channel},
cookies=self.cookies)
else:
r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as",
data={"user": "IRC", "message": f"{user} : {msg}", "channel": channel},
cookies=self.cookies)
class SBMessage(object):
def __init__(self, raw, channel):
self.author = raw['author']
self.channel = channel
self.text = raw['content']
import json
import requests as r
import logging
import time
import datetime
import re
from functools import wraps
from threading import Thread
from irc import IRC
from sasl import nick, password
from users import USERS
class Shoutbox(object):
def __init__(self, cookies):
self.channels = {'annonces': 0, 'projets': 0, 'hs': 0}
self.cookies = cookies
self._callbacks = []
self.irc_clients = {} # pseudo: [IRC(), date]
self.running = False
self._handler = Thread(target=self._handle)
for channel, last_id in self.channels.items():
messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages']
for m in messages:
self.channels[channel] = m['id']
def run(self):
self.running = True
logging.debug("Thread start")
self._handler.start()
def stop(self):
logging.debug("STOP: Stop requests to planet-casio.com")
self.running = False
logging.debug("STOP: Halt all irc user threads")
for client in self.irc_clients:
self.irc_clients[client][0].stop()
self.irc_clients.pop(client)
self._handler.join()
logging.debug("STOP: Shoutbox thread closed")
def on(self, event):
""" Adds a callback to the IRC handler
Event is a function taking in parameter a SBMessage and returning
True if the callback should be executed on the message """
def callback(func):
@wraps(func)
def wrapper(message):
func(message)
self._callbacks.append((event, wrapper))
logging.info(f"added callback {func.__name__}")
return wrapper
return callback
def post(self, user, msg, channel, users):
if msg.startswith("ACTION"):
msg = msg.replace("ACTION", "/me")
# Look for pseudo v43-v5 translation
for v43_name, v5_name in users:
if v5_name.lower() == user.lower():
r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as",
data={"user": v43_name, "message": msg, "channel": channel},
cookies=self.cookies)
return
# No translation found
r.post("https://www.planet-casio.com/Fr/shoutbox/api/post-as",
data={"user": "IRC", "message": f"{user} : {msg}", "channel": channel},
cookies=self.cookies)
def normalize(pseudo):
if pseudo.lower() in [u[0].lower() for u in USERS]:
return [u[1] for u in USERS if u[0].lower() == pseudo.lower()][0]
return re.sub(r'[^.A-Za-z0-9_]', '_', pseudo)
def _handle(self):
while self.running:
try:
for channel, last_id in self.channels.items():
# Do not spam with logs
logging.getLogger().setLevel(logging.INFO)
messages = json.loads(r.get(f"https://www.planet-casio.com/Fr/shoutbox/api/read?since={last_id}&channel={channel}&format=text").text)['messages']
logging.getLogger().setLevel(logging.DEBUG)
for m in messages:
logging.debug(m)
# If message comes from IRC, drop it (no loops allowed)
if m['source'] == "IRC":
continue
# Get channel id, parse SBMessage
self.channels[channel] = m['id']
message = SBMessage(m, channel)
# If handler needs to be killed
if not self.running:
logging.debug("going to stop")
break
# For each callback defined with @decorator
for event, callback in self._callbacks:
author = Shoutbox.normalize(message.author)
# client is not known or is disconnected
if author not in self.irc_clients.keys() \
or self.irc_clients[author][0].running == False:
self.irc_clients[author] = [
IRC('irc.planet-casio.com', 6697),
datetime.datetime.now()
]
# Start a thread for new client
if self.irc_clients[author][0].start(f"{author}[s]", password, nick):
logging.debug(f"{author} has joined IRC")
# client is known but AFK
else:
self.irc_clients[author][1] = datetime.datetime.now()
logging.debug(f"{author} has updated IRC")
if event(message):
logging.info(f"matched {event.__name__}")
callback(message)
# kill afk clients
for k, c in self.irc_clients.items():
if datetime.datetime.now() - c[1] > datetime.timedelta(hours=1):
logging.info(f"killing {c[0].nick}")
c[0].stop()
self.irc_clients.pop(k)
except Exception as e:
logging.error(f"Faillure in Shoutbox thread {e}")
finally:
time.sleep(3)
class SBMessage(object):
def __init__(self, raw, channel):
self.author = raw['author']
self.channel = channel
self.text = raw['content']

View File

@ -1,6 +1,7 @@
users = [
USERS = [
# The gods
("Lephenixnoir", "Lephenixnoir")
("Lephenixnoir", "Lephenixnoir"),
("Lephenixnoir", "Lephe"),
("Shadow15510", "Shadow"),
("Critor", "Critor"),
# The old gods
@ -8,10 +9,21 @@ users = [
("Dark Storm", "Eldeberen"),
# The priests
("Breizh_craft", "Breizh"),
# Senators
("Massena", "massena"),
("Potter360", "potter360"),
("Tituya", "Tituya"),
# The masters of the puzzles
("Eragon", "eragon"),
("Hackcell", "Alice"),
# Judges
("Kikoodx", "KikooDX"),
# Old ones, they don't work anymore
# the - char is not allowed on IRC
("-florian66-", "florian66"),
# This ends the list of sacred users
# This is the start of the plebs
("Acrocasio", "Acrocasio")
("Acrocasio", "Acrocasio"),
("FlamingKite", "FKite"),
("FlamingKite", "FKite_"),
]