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:
commit
575ebee081
78
bridge.py
78
bridge.py
|
@ -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
305
irc.py
|
@ -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}"
|
||||
|
|
206
shoutbox.py
206
shoutbox.py
|
@ -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']
|
||||
|
|
18
users.py
18
users.py
|
@ -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_"),
|
||||
]
|
||||
|
|
Loading…
Reference in New Issue