diff --git a/README.md b/README.md index 03cefe7..d23b4f8 100644 --- a/README.md +++ b/README.md @@ -26,3 +26,4 @@ NOTE: [Privileged Intents](https://discordpy.readthedocs.io/en/latest/intents.ht - [x] Editing messages - [x] Replies - [x] Bridging multiple channels/rooms +- [x] Discord emotes bridged as inline images (Works on Element Web, Fluffychat) diff --git a/main.py b/main.py index 8ccba05..e3efb87 100644 --- a/main.py +++ b/main.py @@ -1,10 +1,14 @@ -import discord.ext.commands +import aiofiles +import aiofiles.os +import aiohttp import discord +import discord.ext.commands import json import logging import nio import os import re +import uuid def config_gen(config_file): @@ -14,7 +18,7 @@ def config_gen(config_file): "password": "my-secret-password", "token": "my-secret-token", "discord_prefix": "my-command-prefix", - "bridge": {"channel_id": "room_id", } + "bridge": {"channel_id": "room_id"} } if not os.path.exists(config_file): @@ -35,9 +39,13 @@ message_store, channel_store = {}, {} class MatrixClient(nio.AsyncClient): - async def start(self, discord_client): - self.logger = logging.getLogger("matrix_logger") + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.logger = logging.getLogger("matrix_logger") + self.uploaded_emotes = {} + + async def start(self, discord_client): password = config["password"] timeout = 30000 @@ -70,15 +78,71 @@ class MatrixClient(nio.AsyncClient): await self.close() - async def message_send(self, message, channel_id, + async def process_emotes(self, message, emotes): + formatted_body = message + + async def upload_emote(emote_name, emote_id): + if emote_id in self.uploaded_emotes.keys(): + return self.uploaded_emotes[emote_id] + + emote_url = f"https://cdn.discordapp.com/emojis/{emote_id}" + + emote_file = f"/tmp/{str(uuid.uuid4())}" + + try: + async with aiohttp.ClientSession() as session: + async with session.get(emote_url) as resp: + emote = await resp.read() + content_type = resp.content_type + except Exception as e: + self.logger.warning( + f"Failed to download emote {emote}: {e}" + ) + return + + async with aiofiles.open(emote_file, "wb") as f: + await f.write(emote) + + try: + async with aiofiles.open(emote_file, "rb") as f: + resp, maybe_keys = await self.upload( + f, content_type=content_type + ) + except Exception as e: + self.logger.warning( + f"Failed to upload emote {emote}: {e}" + ) + return + + await aiofiles.os.remove(emote_file) + + self.uploaded_emotes[emote_id] = resp.content_uri + + return resp.content_uri + + for emote in emotes.keys(): + emote_ = await upload_emote(emote, emotes[emote]) + if emote_: + emote = f":{emote}:" + formatted_body = formatted_body.replace( + emote, f"""\"{emote}\"""" + ) + + return formatted_body + + async def message_send(self, message, channel_id, emotes, reply_id=None, edit_id=None): room_id = config["bridge"][str(channel_id)] content = { - "msgtype": "m.text", - "body": message, + "format": "org.matrix.custom.html", + "msgtype": "m.text" } + content["body"], content["formatted_body"] = message, await \ + self.process_emotes(message, emotes) + if reply_id: reply_event = await self.room_get_event( room_id, reply_id @@ -91,16 +155,18 @@ class MatrixClient(nio.AsyncClient): content["format"] = "org.matrix.custom.html" - content["formatted_body"] = f"""
-In reply to -{reply_event.sender}
-{reply_event.body}
{message}""" + content["formatted_body"] = f"""
\ +In reply to\ +{reply_event.sender}\ +
{reply_event.body}
{content["formatted_body"]}""" if edit_id: content["body"] = f" * {message}" content["m.new_content"] = { "body": message, + "formatted_body": content["formatted_body"], + "format": "org.matrix.custom.html", "msgtype": "m.text" } @@ -183,7 +249,8 @@ class DiscordClient(discord.ext.commands.Bot): content = await self.process_message(message) matrix_message = await self.matrix_client.message_send( - content[0], message.channel.id, reply_id=content[1] + content[0], message.channel.id, + reply_id=content[1], emotes=content[2] ) message_store[message.id] = matrix_message @@ -196,7 +263,8 @@ class DiscordClient(discord.ext.commands.Bot): if before.id in message_store: await self.matrix_client.message_send( - content[0], after.channel.id, edit_id=message_store[before.id] + content[0], after.channel.id, + edit_id=message_store[before.id], emotes=content[2] ) async def on_message_delete(self, message): @@ -217,6 +285,12 @@ class DiscordClient(discord.ext.commands.Bot): async def process_message(self, message): content = message.clean_content + regex = r"" + emotes = {} + + for emote in re.findall(regex, content): + emotes[emote[0]] = emote[1] + replied_event = None if message.reference: replied_message = await message.channel.fetch_message( @@ -228,7 +302,7 @@ class DiscordClient(discord.ext.commands.Bot): pass # Replace emote IDs with names - content = re.sub(r"", r"\g<1>", content) + content = re.sub(regex, r":\g<1>:", content) # Append attachments to message for attachment in message.attachments: @@ -236,7 +310,7 @@ class DiscordClient(discord.ext.commands.Bot): content = f"[{message.author.name}] {content}" - return content, replied_event + return content, replied_event, emotes class Callbacks(object): diff --git a/requirements.txt b/requirements.txt index 583c1a8..5bf6424 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -git+git://github.com/rapptz/discord.py@c793737 +discord.py==1.6.0 matrix-nio==0.15.2