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""""""
+ )
+
+ 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