2021-02-25 17:48:06 +00:00
|
|
|
import eventbus
|
|
|
|
import asyncio
|
|
|
|
import irc.client_aio
|
|
|
|
import random
|
|
|
|
import util
|
|
|
|
import logging
|
|
|
|
import hashlib
|
2021-03-25 17:56:29 +00:00
|
|
|
import discord.ext.commands as commands
|
2021-04-18 11:39:49 +00:00
|
|
|
from jaraco.stream import buffer
|
2021-02-25 17:48:06 +00:00
|
|
|
|
|
|
|
def scramble(text):
|
|
|
|
n = list(text)
|
|
|
|
random.shuffle(n)
|
|
|
|
return "".join(n)
|
|
|
|
|
|
|
|
def color_code(x):
|
|
|
|
return f"\x03{x}"
|
|
|
|
def random_color(id): return color_code(hashlib.blake2b(str(id).encode("utf-8")).digest()[0] % 13 + 2)
|
|
|
|
|
2021-03-25 17:56:29 +00:00
|
|
|
def render_formatting(message):
|
|
|
|
out = ""
|
|
|
|
for seg in message:
|
|
|
|
if isinstance(seg, str):
|
|
|
|
out += seg.replace("\n", " ")
|
|
|
|
else:
|
|
|
|
kind = seg["type"]
|
|
|
|
# TODO: check if user exists on both ends, and possibly drop if so
|
|
|
|
if kind == "user_mention":
|
|
|
|
out += f"@{random_color(seg['id'])}{seg['name']}{color_code('')}"
|
|
|
|
elif kind == "channel_mention": # these appear to be clickable across servers/guilds
|
|
|
|
out += f"#{seg['name']}"
|
|
|
|
else: logging.warn("Unrecognized message seg %s", kind)
|
|
|
|
return out.strip()
|
|
|
|
|
2021-03-08 10:25:11 +00:00
|
|
|
global_conn = None
|
2021-03-25 17:56:29 +00:00
|
|
|
unlisten = None
|
2021-03-08 10:25:11 +00:00
|
|
|
|
2021-02-25 17:48:06 +00:00
|
|
|
async def initialize():
|
2021-03-08 10:25:11 +00:00
|
|
|
logging.info("Initializing IRC link")
|
|
|
|
|
2021-02-25 17:48:06 +00:00
|
|
|
joined = set()
|
|
|
|
|
|
|
|
loop = asyncio.get_event_loop()
|
2021-04-18 11:39:49 +00:00
|
|
|
irc.client.ServerConnection.buffer_class = buffer.LenientDecodingLineBuffer # should not crash in the face of invalid UTF-8
|
2021-02-25 17:48:06 +00:00
|
|
|
reactor = irc.client_aio.AioReactor(loop=loop)
|
|
|
|
conn = await reactor.server().connect(util.config["irc"]["server"], util.config["irc"]["port"], util.config["irc"]["nick"])
|
2021-03-08 10:25:11 +00:00
|
|
|
global global_conn
|
|
|
|
global_conn = conn
|
2021-02-25 17:48:06 +00:00
|
|
|
|
|
|
|
def inuse(conn, event):
|
|
|
|
conn.nick(scramble(conn.get_nickname()))
|
|
|
|
|
|
|
|
def pubmsg(conn, event):
|
2021-07-28 19:30:37 +00:00
|
|
|
msg = eventbus.Message(eventbus.AuthorInfo(event.source.nick, str(event.source), None), [" ".join(event.arguments)], (util.config["irc"]["name"], event.target), util.random_id(), [])
|
2021-02-25 17:48:06 +00:00
|
|
|
asyncio.create_task(eventbus.push(msg))
|
|
|
|
|
2021-10-28 11:55:40 +00:00
|
|
|
def bytewise_truncate(x, max):
|
|
|
|
x = x[:max]
|
|
|
|
while True:
|
|
|
|
try:
|
|
|
|
return x, x.decode("utf-8")
|
|
|
|
except UnicodeDecodeError:
|
|
|
|
x = x[:-1]
|
|
|
|
|
|
|
|
def render_line(author, content):
|
|
|
|
# colorize for aesthetics
|
|
|
|
# add ZWS to prevent pinging
|
|
|
|
return f"<{random_color(author.id)}{author.name[0]}\u200B{author.name[1:]}{color_code('')}> {content}"
|
|
|
|
|
2021-02-25 17:48:06 +00:00
|
|
|
async def on_bridge_message(channel_name, msg):
|
|
|
|
if channel_name in util.config["irc"]["channels"]:
|
|
|
|
if channel_name not in joined: conn.join(channel_name)
|
2021-10-28 11:55:40 +00:00
|
|
|
if msg.reply:
|
2021-11-09 08:11:15 +00:00
|
|
|
if msg.reply[0] and msg.reply[1]:
|
|
|
|
reply_line = render_line(msg.reply[0], render_formatting(msg.reply[1])).encode("utf-8")
|
|
|
|
reply_line_new, reply_line_u = bytewise_truncate(reply_line, 300)
|
|
|
|
if reply_line_new != reply_line:
|
|
|
|
reply_line_u += " ..."
|
|
|
|
conn.privmsg(channel_name, f"[Replying to {reply_line_u}]")
|
|
|
|
else:
|
|
|
|
conn.privmsg(channel_name, "[Replying to an unknown message]")
|
2021-10-28 11:55:40 +00:00
|
|
|
lines = []
|
|
|
|
content = render_formatting(msg.message).encode("utf-8")
|
|
|
|
# somewhat accursedly break string into valid UTF-8 substrings with <=400 bytes
|
|
|
|
while content:
|
|
|
|
next_line, next_line_u = bytewise_truncate(content, 400)
|
|
|
|
lines.append(next_line_u)
|
|
|
|
content = content[len(next_line):]
|
|
|
|
for line in lines:
|
|
|
|
conn.privmsg(channel_name, render_line(msg.author, line))
|
2021-07-28 19:30:37 +00:00
|
|
|
for at in msg.attachments:
|
2021-10-28 11:55:40 +00:00
|
|
|
conn.privmsg(channel_name, render_line(msg.author, f"-> {at.filename}: {at.proxy_url}"))
|
2021-02-25 17:48:06 +00:00
|
|
|
else:
|
|
|
|
logging.warning("IRC channel %s not allowed", channel_name)
|
|
|
|
|
|
|
|
def connect(conn, event):
|
|
|
|
for channel in util.config["irc"]["channels"]:
|
2021-05-12 16:32:52 +00:00
|
|
|
conn.join(channel, key=util.config["irc"]["channel_keys"].get(channel, ""))
|
2021-03-25 17:56:29 +00:00
|
|
|
logging.info("Connected to %s on IRC", channel)
|
2021-02-25 17:48:06 +00:00
|
|
|
joined.add(channel)
|
|
|
|
|
2021-10-16 22:15:11 +00:00
|
|
|
def disconnect(conn, event):
|
|
|
|
logging.warn("Disconnected from IRC, reinitializing")
|
|
|
|
teardown()
|
2022-11-28 16:02:40 +00:00
|
|
|
if not conn.planned_disconnection: asyncio.create_task(initialize())
|
2021-10-16 22:15:11 +00:00
|
|
|
|
2021-02-25 17:48:06 +00:00
|
|
|
# TODO: do better thing
|
|
|
|
conn.add_global_handler("welcome", connect)
|
2021-10-16 22:15:11 +00:00
|
|
|
conn.add_global_handler("disconnect", disconnect)
|
2021-02-25 17:48:06 +00:00
|
|
|
conn.add_global_handler("nicknameinuse", inuse)
|
|
|
|
conn.add_global_handler("pubmsg", pubmsg)
|
|
|
|
|
2021-03-25 17:56:29 +00:00
|
|
|
global unlisten
|
|
|
|
unlisten = eventbus.add_listener(util.config["irc"]["name"], on_bridge_message)
|
2021-03-08 10:25:11 +00:00
|
|
|
|
|
|
|
def setup(bot):
|
|
|
|
asyncio.create_task(initialize())
|
|
|
|
|
2022-06-16 17:56:26 +00:00
|
|
|
def teardown(bot=None):
|
2022-11-28 16:02:40 +00:00
|
|
|
if global_conn:
|
|
|
|
global_conn.planned_disconnection = True
|
|
|
|
global_conn.disconnect()
|
2021-05-12 16:32:52 +00:00
|
|
|
if unlisten: unlisten()
|