refactor (#5)
* separate dir * lint * config * log unhandled exceptions * refactor * refactor webhook check * cogs cannnot be loaded from another dir * fix * fix * use logger.exception * Update README.md * Update README.md * fix * Update README.md * Create README.md * rm basedir * cogs * Update README.md * bold
This commit is contained in:
parent
96ef420f28
commit
8062ba36c6
6 changed files with 173 additions and 181 deletions
55
README.md
55
README.md
|
@ -1,51 +1,22 @@
|
||||||
# matrix-discord-bridge
|
# matrix-discord-bridge
|
||||||
|
|
||||||
A simple non-puppeting bridge between Matrix and Discord written in Python.
|
A simple bridge between Matrix and Discord written in Python.
|
||||||
|
|
||||||
## Installation
|
This repository contains two bridges:
|
||||||
|
* A [puppeting appservice](appservice): The puppeting bridge written with minimal dependencies. Running this requires a self-hosted homeserver.
|
||||||
|
|
||||||
`pip install -r requirements.txt`
|
* A [non-puppeting bridge](bridge): The non-puppeting bridge written with `matrix-nio` and `discord.py`, most people would want to use this one.
|
||||||
|
|
||||||
## Usage
|
Check their READMEs for specific information.
|
||||||
|
|
||||||
* Run `main.py` to generate `config.json`
|
|
||||||
|
|
||||||
* Edit `config.json`
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"homeserver": "https://matrix.org",
|
|
||||||
"username": "@name:matrix.org",
|
|
||||||
"password": "my-secret-password",
|
|
||||||
"token": "my-secret-token",
|
|
||||||
"discord_prefix": "my-command-prefix", # Prefix for Discord commands
|
|
||||||
"bridge": {
|
|
||||||
"channel_id": "room_id", # Bridge multiple channels and rooms
|
|
||||||
"channel_id2": "room_id2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
* Logs are saved to the `bot.log` file in `$PWD`.
|
|
||||||
|
|
||||||
* Normal Discord bot functionality like commands can be added to the bot via [cogs](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html), example [here](https://gist.github.com/EvieePy/d78c061a4798ae81be9825468fe146be).
|
|
||||||
|
|
||||||
* Replace `guild.emojis` with `self.discord_client.emojis` (`Callbacks()`, `process_message()`) to make the Discord bot use emojis from ALL it's guilds.
|
|
||||||
|
|
||||||
NOTE: [Privileged Intents](https://discordpy.readthedocs.io/en/latest/intents.html#privileged-intents) must be enabled for your Discord bot.
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
TODO
|
|
||||||
|
|
||||||
## What Works
|
## What Works
|
||||||
|
|
||||||
- [x] Sending messages
|
- [x] Puppeting (Appservice only, regular bridge only uses webhooks on Discord.)
|
||||||
- [x] Discord webhooks (with avatars)
|
- [x] Attachments (Converted to URLs.)
|
||||||
- [x] Attachments (Converted to URLs)
|
- [x] Typing Indicators (Per-user indicators on Appservice, otherwise sent as bot user.)
|
||||||
- [x] Typing status (Not very accurate)
|
- [x] Message redaction
|
||||||
- [x] Redacting messages
|
|
||||||
- [x] Editing messages
|
|
||||||
- [x] Replies
|
- [x] Replies
|
||||||
- [x] Bridging multiple channels/rooms
|
- [x] Bridging multiple channels
|
||||||
- [x] `:emote:` in Matrix message converted to Discord emotes
|
- [x] Discord emojis displayed as inline images
|
||||||
- [x] Discord emotes bridged as inline images (Works on Element Web, Fluffychat)
|
- [x] Sending Discord emotes from Matrix (`:emote_name:`)
|
||||||
|
- [x] Mentioning Discord users via partial username (`@partialname`)
|
||||||
|
|
31
bridge/README.md
Normal file
31
bridge/README.md
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
`pip install -r requirements.txt`
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
* Run `main.py` to generate `config.json`
|
||||||
|
|
||||||
|
* Edit `config.json`:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"homeserver": "https://matrix.org",
|
||||||
|
"username": "@name:matrix.org",
|
||||||
|
"password": "my-secret-password", # Matrix password.
|
||||||
|
"token": "my-secret-token", # Discord bot token.
|
||||||
|
"discord_cmd_prefix": "my-command-prefix",
|
||||||
|
"bridge": {
|
||||||
|
"channel_id": "room_id",
|
||||||
|
"channel_id2": "room_id2", # Bridge multiple rooms.
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This bridge does not use databases for keeping track of bridged rooms to avoid a dependency on persistent storage. This makes it easy to host on something like Heroku with the free tier.
|
||||||
|
|
||||||
|
* Logs are saved to the `bridge.log` file in `$PWD`.
|
||||||
|
|
||||||
|
* Normal Discord bot functionality like commands can be added to the bot via [cogs](https://discordpy.readthedocs.io/en/latest/ext/commands/cogs.html), example [here](https://gist.github.com/EvieePy/d78c061a4798ae81be9825468fe146be).
|
||||||
|
|
||||||
|
NOTE: [Privileged Intents](https://discordpy.readthedocs.io/en/latest/intents.html#privileged-intents) must be enabled for your Discord bot.
|
|
@ -3,9 +3,9 @@ import json
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import traceback
|
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import aiofiles.os
|
import aiofiles.os
|
||||||
import aiohttp
|
import aiohttp
|
||||||
|
@ -20,8 +20,8 @@ def config_gen(config_file):
|
||||||
"username": "@name:matrix.org",
|
"username": "@name:matrix.org",
|
||||||
"password": "my-secret-password",
|
"password": "my-secret-password",
|
||||||
"token": "my-secret-token",
|
"token": "my-secret-token",
|
||||||
"discord_prefix": "my-command-prefix",
|
"discord_cmd_prefix": "my-command-prefix",
|
||||||
"bridge": {"channel_id": "room_id"}
|
"bridge": {"channel_id": "room_id"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
|
@ -37,7 +37,6 @@ def config_gen(config_file):
|
||||||
|
|
||||||
|
|
||||||
config = config_gen("config.json")
|
config = config_gen("config.json")
|
||||||
|
|
||||||
message_store = {}
|
message_store = {}
|
||||||
|
|
||||||
|
|
||||||
|
@ -56,17 +55,17 @@ class MatrixClient(nio.AsyncClient):
|
||||||
self.add_callbacks()
|
self.add_callbacks()
|
||||||
|
|
||||||
def start_discord(self):
|
def start_discord(self):
|
||||||
# Disable everyone and role mentions.
|
|
||||||
allowed_mentions = discord.AllowedMentions(everyone=False, roles=False)
|
|
||||||
# Set command prefix for Discord bot.
|
|
||||||
command_prefix = config["discord_prefix"]
|
|
||||||
# Intents to fetch members from guild.
|
# Intents to fetch members from guild.
|
||||||
intents = discord.Intents.default()
|
intents = discord.Intents.default()
|
||||||
intents.members = True
|
intents.members = True
|
||||||
|
|
||||||
self.discord_client = DiscordClient(
|
self.discord_client = DiscordClient(
|
||||||
self, allowed_mentions=allowed_mentions,
|
self,
|
||||||
command_prefix=command_prefix, intents=intents
|
allowed_mentions=discord.AllowedMentions(
|
||||||
|
everyone=False, roles=False
|
||||||
|
),
|
||||||
|
command_prefix=config["discord_cmd_prefix"],
|
||||||
|
intents=intents,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.bg_task = self.loop.create_task(
|
self.bg_task = self.loop.create_task(
|
||||||
|
@ -78,8 +77,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
self.add_event_callback(
|
self.add_event_callback(
|
||||||
callbacks.message_callback,
|
callbacks.message_callback,
|
||||||
(nio.RoomMessageText, nio.RoomMessageMedia,
|
(nio.RoomMessageText, nio.RoomMessageMedia, nio.RoomMessageEmote),
|
||||||
nio.RoomMessageEmote)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.add_event_callback(
|
self.add_event_callback(
|
||||||
|
@ -107,16 +105,12 @@ class MatrixClient(nio.AsyncClient):
|
||||||
await f.write(emote)
|
await f.write(emote)
|
||||||
|
|
||||||
async with aiofiles.open(emote_file, "rb") as f:
|
async with aiofiles.open(emote_file, "rb") as f:
|
||||||
resp, maybe_keys = await self.upload(
|
resp, maybe_keys = await self.upload(f, content_type=content_type)
|
||||||
f, content_type=content_type
|
|
||||||
)
|
|
||||||
|
|
||||||
await aiofiles.os.remove(emote_file)
|
await aiofiles.os.remove(emote_file)
|
||||||
|
|
||||||
if type(resp) != nio.UploadResponse:
|
if type(resp) != nio.UploadResponse:
|
||||||
self.logger.warning(
|
self.logger.warning(f"Failed to upload emote {emote_id}")
|
||||||
f"Failed to upload emote {emote_id}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.uploaded_emotes[emote_id] = resp.content_uri
|
self.uploaded_emotes[emote_id] = resp.content_uri
|
||||||
|
@ -125,18 +119,18 @@ class MatrixClient(nio.AsyncClient):
|
||||||
|
|
||||||
async def get_fmt_body(self, body, emotes):
|
async def get_fmt_body(self, body, emotes):
|
||||||
replace_ = [
|
replace_ = [
|
||||||
# Code blocks
|
# Bold.
|
||||||
|
("**", "<strong>", "</strong>"),
|
||||||
|
# Code blocks.
|
||||||
("```", "<pre><code>", "</code></pre>"),
|
("```", "<pre><code>", "</code></pre>"),
|
||||||
# Spoilers
|
# Spoilers.
|
||||||
("||", "<span data-mx-spoiler>", "</span>"),
|
("||", "<span data-mx-spoiler>", "</span>"),
|
||||||
# Strikethrough
|
# Strikethrough.
|
||||||
("~~", "<del>", "</del>")
|
("~~", "<del>", "</del>"),
|
||||||
]
|
]
|
||||||
|
|
||||||
for replace in replace_:
|
for replace in replace_:
|
||||||
for i in range(body.count(replace[0])):
|
for i in range(1, body.count(replace[0]) + 1):
|
||||||
i += 1
|
|
||||||
|
|
||||||
if i % 2:
|
if i % 2:
|
||||||
body = body.replace(replace[0], replace[1], 1)
|
body = body.replace(replace[0], replace[1], 1)
|
||||||
else:
|
else:
|
||||||
|
@ -147,77 +141,85 @@ class MatrixClient(nio.AsyncClient):
|
||||||
if emote_:
|
if emote_:
|
||||||
emote = f":{emote}:"
|
emote = f":{emote}:"
|
||||||
body = body.replace(
|
body = body.replace(
|
||||||
emote, f"""<img alt=\"{emote}\" title=\"{emote}\" \
|
emote,
|
||||||
height=\"32\" src=\"{emote_}\" data-mx-emoticon />"""
|
f"""<img alt=\"{emote}\" title=\"{emote}\" \
|
||||||
|
height=\"32\" src=\"{emote_}\" data-mx-emoticon />""",
|
||||||
)
|
)
|
||||||
|
|
||||||
return body
|
return body
|
||||||
|
|
||||||
async def message_send(self, message, channel_id, emotes,
|
async def message_send(
|
||||||
reply_id=None, edit_id=None):
|
self, message, channel_id, emotes, reply_id=None, edit_id=None
|
||||||
|
):
|
||||||
room_id = config["bridge"][str(channel_id)]
|
room_id = config["bridge"][str(channel_id)]
|
||||||
|
|
||||||
content = {
|
content = {
|
||||||
"body": message,
|
"body": message,
|
||||||
"format": "org.matrix.custom.html",
|
"format": "org.matrix.custom.html",
|
||||||
"formatted_body": await self.get_fmt_body(message, emotes),
|
"formatted_body": await self.get_fmt_body(message, emotes),
|
||||||
"msgtype": "m.text"
|
"msgtype": "m.text",
|
||||||
}
|
}
|
||||||
|
|
||||||
if reply_id:
|
if reply_id:
|
||||||
reply_event = await self.room_get_event(
|
reply_event = await self.room_get_event(room_id, reply_id)
|
||||||
room_id, reply_id
|
|
||||||
)
|
|
||||||
reply_event = reply_event.event
|
reply_event = reply_event.event
|
||||||
|
|
||||||
content["m.relates_to"] = {"m.in_reply_to": {"event_id": reply_id}}
|
content = {
|
||||||
|
**content,
|
||||||
content["formatted_body"] = f"""<mx-reply><blockquote>\
|
"m.relates_to": {"m.in_reply_to": {"event_id": reply_id}},
|
||||||
|
"formatted_body": f"""<mx-reply><blockquote>\
|
||||||
<a href="https://matrix.to/#/{room_id}/{reply_id}">In reply to</a>\
|
<a href="https://matrix.to/#/{room_id}/{reply_id}">In reply to</a>\
|
||||||
<a href="https://matrix.to/#/{reply_event.sender}">{reply_event.sender}</a>\
|
<a href="https://matrix.to/#/{reply_event.sender}">{reply_event.sender}</a>\
|
||||||
<br>{reply_event.body}</blockquote></mx-reply>{content["formatted_body"]}"""
|
<br>{reply_event.body}</blockquote></mx-reply>{content["formatted_body"]}""",
|
||||||
|
|
||||||
if edit_id:
|
|
||||||
content["body"] = f" * {content['body']}"
|
|
||||||
|
|
||||||
content["m.relates_to"] = {
|
|
||||||
"event_id": edit_id, "rel_type": "m.replace"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
content["m.new_content"] = {
|
if edit_id:
|
||||||
"body": content["body"],
|
content = {
|
||||||
"formatted_body": content["formatted_body"],
|
**content,
|
||||||
"format": content["format"],
|
"body": f" * {content['body']}",
|
||||||
"msgtype": content["msgtype"]
|
"formatted_body": f" * {content['formatted_body']}",
|
||||||
|
"m.relates_to": {"event_id": edit_id, "rel_type": "m.replace"},
|
||||||
|
"m.new_content": {**content},
|
||||||
}
|
}
|
||||||
|
|
||||||
message = await self.room_send(
|
message = await self.room_send(
|
||||||
room_id=room_id,
|
room_id=room_id, message_type="m.room.message", content=content
|
||||||
message_type="m.room.message",
|
|
||||||
content=content
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return message.event_id
|
return message.event_id
|
||||||
|
|
||||||
async def message_redact(self, message, channel_id):
|
async def message_redact(self, message, channel_id):
|
||||||
await self.room_redact(
|
await self.room_redact(
|
||||||
room_id=config["bridge"][str(channel_id)],
|
room_id=config["bridge"][str(channel_id)], event_id=message
|
||||||
event_id=message
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def webhook_send(self, author, avatar, message,
|
async def webhook_send(
|
||||||
event_id, channel_id, embed=None):
|
self, author, avatar, message, event_id, channel_id, embed=None
|
||||||
|
):
|
||||||
channel = self.discord_client.channel_store[channel_id]
|
channel = self.discord_client.channel_store[channel_id]
|
||||||
|
|
||||||
# Recreate hook if it was deleted.
|
hook_name = "matrix_bridge"
|
||||||
hook = await self.discord_client.hook_create(channel)
|
|
||||||
|
hook = self.discord_client.webhook_cache.get(str(channel.id))
|
||||||
|
|
||||||
|
if not hook:
|
||||||
|
hooks = await channel.webhooks()
|
||||||
|
hook = discord.utils.get(hooks, name=hook_name)
|
||||||
|
|
||||||
|
if not hook:
|
||||||
|
hook = await channel.create_webhook(name=hook_name)
|
||||||
|
|
||||||
|
self.discord_client.webhook_cache[str(channel.id)] = hook
|
||||||
|
|
||||||
# Username must be between 1 and 80 characters in length,
|
# Username must be between 1 and 80 characters in length,
|
||||||
# 'wait=True' allows us to store the sent message.
|
# 'wait=True' allows us to store the sent message.
|
||||||
try:
|
try:
|
||||||
hook = await hook.send(
|
hook = await hook.send(
|
||||||
username=author[:80], avatar_url=avatar,
|
username=author[:80],
|
||||||
content=message, embed=embed, wait=True
|
avatar_url=avatar,
|
||||||
|
content=message,
|
||||||
|
embed=embed,
|
||||||
|
wait=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
message_store[event_id] = hook
|
message_store[event_id] = hook
|
||||||
|
@ -232,7 +234,7 @@ class DiscordClient(discord.ext.commands.Bot):
|
||||||
|
|
||||||
self.channel_store = {}
|
self.channel_store = {}
|
||||||
|
|
||||||
self.webhook_ids = set()
|
self.webhook_cache = {}
|
||||||
|
|
||||||
self.ready = asyncio.Event()
|
self.ready = asyncio.Event()
|
||||||
|
|
||||||
|
@ -251,37 +253,20 @@ class DiscordClient(discord.ext.commands.Bot):
|
||||||
cog = f"cogs.{cog[:-3]}"
|
cog = f"cogs.{cog[:-3]}"
|
||||||
self.load_extension(cog)
|
self.load_extension(cog)
|
||||||
|
|
||||||
async def hook_create(self, channel):
|
|
||||||
hook_name = "matrix_bridge"
|
|
||||||
|
|
||||||
hooks = await channel.webhooks()
|
|
||||||
|
|
||||||
# Check if webhook exists.
|
|
||||||
hook = discord.utils.get(hooks, name=hook_name)
|
|
||||||
if not hook:
|
|
||||||
hook = await channel.create_webhook(name=hook_name)
|
|
||||||
|
|
||||||
self.webhook_ids.add(hook.id)
|
|
||||||
|
|
||||||
return hook
|
|
||||||
|
|
||||||
async def to_return(self, channel_id, message=None):
|
async def to_return(self, channel_id, message=None):
|
||||||
await self.matrix_client.ready.wait()
|
await self.matrix_client.ready.wait()
|
||||||
|
|
||||||
if str(channel_id) not in config["bridge"].keys():
|
return str(channel_id) not in config["bridge"].keys() or (
|
||||||
return True
|
message
|
||||||
|
and message.webhook_id
|
||||||
if message:
|
in [hook.id for hook in self.webhook_cache.values()]
|
||||||
if message.webhook_id in self.webhook_ids:
|
)
|
||||||
return True
|
|
||||||
|
|
||||||
async def on_ready(self):
|
async def on_ready(self):
|
||||||
for channel in config["bridge"].keys():
|
for channel in config["bridge"].keys():
|
||||||
channel_ = self.get_channel(int(channel))
|
channel_ = self.get_channel(int(channel))
|
||||||
self.channel_store[channel] = channel_
|
self.channel_store[channel] = channel_
|
||||||
|
|
||||||
await self.hook_create(channel_)
|
|
||||||
|
|
||||||
self.ready.set()
|
self.ready.set()
|
||||||
|
|
||||||
async def on_message(self, message):
|
async def on_message(self, message):
|
||||||
|
@ -294,8 +279,10 @@ class DiscordClient(discord.ext.commands.Bot):
|
||||||
content = await self.process_message(message)
|
content = await self.process_message(message)
|
||||||
|
|
||||||
matrix_message = await self.matrix_client.message_send(
|
matrix_message = await self.matrix_client.message_send(
|
||||||
content[0], message.channel.id,
|
content[0],
|
||||||
reply_id=content[1], emotes=content[2]
|
message.channel.id,
|
||||||
|
reply_id=content[1],
|
||||||
|
emotes=content[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
message_store[message.id] = matrix_message
|
message_store[message.id] = matrix_message
|
||||||
|
@ -309,8 +296,10 @@ class DiscordClient(discord.ext.commands.Bot):
|
||||||
# Edit message only if it can be looked up in the cache.
|
# Edit message only if it can be looked up in the cache.
|
||||||
if before.id in message_store:
|
if before.id in message_store:
|
||||||
await self.matrix_client.message_send(
|
await self.matrix_client.message_send(
|
||||||
content[0], after.channel.id,
|
content[0],
|
||||||
edit_id=message_store[before.id], emotes=content[2]
|
after.channel.id,
|
||||||
|
edit_id=message_store[before.id],
|
||||||
|
emotes=content[2],
|
||||||
)
|
)
|
||||||
|
|
||||||
async def on_message_delete(self, message):
|
async def on_message_delete(self, message):
|
||||||
|
@ -371,8 +360,12 @@ class Callbacks(object):
|
||||||
|
|
||||||
def get_channel(self, room):
|
def get_channel(self, room):
|
||||||
channel_id = next(
|
channel_id = next(
|
||||||
(channel_id for channel_id, room_id in config["bridge"].items()
|
(
|
||||||
if room_id == room.room_id), None
|
channel_id
|
||||||
|
for channel_id, room_id in config["bridge"].items()
|
||||||
|
if room_id == room.room_id
|
||||||
|
),
|
||||||
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return channel_id
|
return channel_id
|
||||||
|
@ -380,10 +373,11 @@ class Callbacks(object):
|
||||||
async def to_return(self, room, event):
|
async def to_return(self, room, event):
|
||||||
await self.matrix_client.discord_client.ready.wait()
|
await self.matrix_client.discord_client.ready.wait()
|
||||||
|
|
||||||
if room.room_id not in config["bridge"].values() or \
|
return (
|
||||||
event.sender == self.matrix_client.user or \
|
room.room_id not in config["bridge"].values()
|
||||||
not self.matrix_client.listen:
|
or event.sender == self.matrix_client.user
|
||||||
return True
|
or not self.matrix_client.listen
|
||||||
|
)
|
||||||
|
|
||||||
async def message_callback(self, room, event):
|
async def message_callback(self, room, event):
|
||||||
message = event.body
|
message = event.body
|
||||||
|
@ -418,7 +412,8 @@ class Callbacks(object):
|
||||||
await webhook_message.edit(content=edited_content)
|
await webhook_message.edit(content=edited_content)
|
||||||
# Handle exception if edited message was deleted on Discord.
|
# Handle exception if edited message was deleted on Discord.
|
||||||
except (
|
except (
|
||||||
discord.errors.NotFound, discord.errors.HTTPException
|
discord.errors.NotFound,
|
||||||
|
discord.errors.HTTPException,
|
||||||
) as e:
|
) as e:
|
||||||
self.matrix_client.logger.warning(
|
self.matrix_client.logger.warning(
|
||||||
f"Failed to edit message {edited_event}: {e}"
|
f"Failed to edit message {edited_event}: {e}"
|
||||||
|
@ -429,9 +424,11 @@ class Callbacks(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if content_dict["m.relates_to"]["m.in_reply_to"]["event_id"] in \
|
if (
|
||||||
message_store.values():
|
content_dict["m.relates_to"]["m.in_reply_to"]["event_id"]
|
||||||
# Remove the first occurance of our bot's username if replying.
|
in message_store.values()
|
||||||
|
):
|
||||||
|
# Remove the first occurence of our bot's username if replying.
|
||||||
# > <@discordbridge:something.org> [discord user]
|
# > <@discordbridge:something.org> [discord user]
|
||||||
message = message.replace(f"<{config['username']}>", "", 1)
|
message = message.replace(f"<{config['username']}>", "", 1)
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -489,13 +486,14 @@ class Callbacks(object):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def typing_callback(self, room, event):
|
async def typing_callback(self, room, event):
|
||||||
if not room.typing_users \
|
if (
|
||||||
or room.room_id not in config["bridge"].values():
|
not room.typing_users
|
||||||
return
|
or (
|
||||||
|
len(room.typing_users) == 1
|
||||||
# Return if the event is sent by our bot.
|
and self.matrix_client.user in room.typing_users
|
||||||
if len(room.typing_users) == 1 and \
|
)
|
||||||
self.matrix_client.user in room.typing_users:
|
or room.room_id not in config["bridge"].values()
|
||||||
|
):
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get the corresponding Discord channel.
|
# Get the corresponding Discord channel.
|
||||||
|
@ -506,8 +504,8 @@ class Callbacks(object):
|
||||||
return
|
return
|
||||||
|
|
||||||
async def process_message(self, message, channel_id):
|
async def process_message(self, message, channel_id):
|
||||||
mentions = re.findall(r"(^|\s)(@(\w*))", message)
|
|
||||||
emotes = re.findall(r":(\w*):", message)
|
emotes = re.findall(r":(\w*):", message)
|
||||||
|
mentions = re.findall(r"(@(\w*))", message)
|
||||||
|
|
||||||
# Get the guild from channel ID.
|
# Get the guild from channel ID.
|
||||||
guild = self.discord_client.channel_store[channel_id].guild
|
guild = self.discord_client.channel_store[channel_id].guild
|
||||||
|
@ -522,15 +520,15 @@ class Callbacks(object):
|
||||||
if emote_:
|
if emote_:
|
||||||
message = message.replace(f":{emote}:", str(emote_))
|
message = message.replace(f":{emote}:", str(emote_))
|
||||||
|
|
||||||
# mentions = [('', '@name', 'name'), (' ', '@', '')]
|
# mentions = [('@name', 'name'), ('@', '')]
|
||||||
for mention in mentions:
|
for mention in mentions:
|
||||||
# Don't fetch member if mention is empty.
|
# Don't fetch member if mention is empty.
|
||||||
# Single "@" without any name.
|
# Single "@" without any name.
|
||||||
if mention[2]:
|
if mention[1]:
|
||||||
member = await guild.query_members(query=mention[2])
|
member = await guild.query_members(query=mention[1])
|
||||||
if member:
|
if member:
|
||||||
# Get first result.
|
# Get first result.
|
||||||
message = message.replace(mention[1], member[0].mention)
|
message = message.replace(mention[0], member[0].mention)
|
||||||
|
|
||||||
return message
|
return message
|
||||||
|
|
||||||
|
@ -538,17 +536,17 @@ class Callbacks(object):
|
||||||
async def main():
|
async def main():
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(name)s:%(levelname)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
handlers=[
|
handlers=[
|
||||||
logging.FileHandler("bot.log"),
|
logging.FileHandler("bridge.log"),
|
||||||
logging.StreamHandler()
|
logging.StreamHandler(),
|
||||||
]
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
retry = 2
|
retry = 2
|
||||||
|
|
||||||
matrix_client = MatrixClient(
|
matrix_client = MatrixClient(config["homeserver"], config["username"])
|
||||||
config["homeserver"], config["username"]
|
|
||||||
)
|
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
resp = await matrix_client.login(config["password"])
|
resp = await matrix_client.login(config["password"])
|
||||||
|
@ -563,9 +561,7 @@ async def main():
|
||||||
try:
|
try:
|
||||||
await matrix_client.sync(full_state=True)
|
await matrix_client.sync(full_state=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
matrix_client.logger.error(
|
matrix_client.logger.exception("Initial sync failed!")
|
||||||
f"Initial sync failed!\n{traceback.format_exc()}"
|
|
||||||
)
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -576,9 +572,8 @@ async def main():
|
||||||
|
|
||||||
await matrix_client.sync_forever(timeout=30000, full_state=True)
|
await matrix_client.sync_forever(timeout=30000, full_state=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
matrix_client.logger.error(
|
matrix_client.logger.exception(
|
||||||
f"Unknown exception occured\n{traceback.format_exc()}\n"
|
f"Unknown exception occured, retrying in {retry} seconds..."
|
||||||
f"Retrying in {retry} seconds..."
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Clear "ready" status.
|
# Clear "ready" status.
|
||||||
|
@ -593,5 +588,6 @@ async def main():
|
||||||
await matrix_client.close()
|
await matrix_client.close()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
|
@ -1,3 +1,3 @@
|
||||||
aiofiles==0.6.0
|
aiofiles==0.6.0
|
||||||
discord.py==1.6.0
|
discord.py==1.6.0
|
||||||
matrix-nio==0.16.0
|
matrix-nio==0.17.0
|
|
@ -17,7 +17,7 @@ def config_gen(config_file):
|
||||||
"username": "@name:matrix.org",
|
"username": "@name:matrix.org",
|
||||||
"password": "my-secret-password",
|
"password": "my-secret-password",
|
||||||
"token": "my-secret-token",
|
"token": "my-secret-token",
|
||||||
"migrate": {"guild_id": "room_id"}
|
"migrate": {"guild_id": "room_id"},
|
||||||
}
|
}
|
||||||
|
|
||||||
if not os.path.exists(config_file):
|
if not os.path.exists(config_file):
|
||||||
|
@ -66,16 +66,12 @@ class MatrixClient(nio.AsyncClient):
|
||||||
await f.write(emote_)
|
await f.write(emote_)
|
||||||
|
|
||||||
async with aiofiles.open(emote_file, "rb") as f:
|
async with aiofiles.open(emote_file, "rb") as f:
|
||||||
resp, maybe_keys = await self.upload(
|
resp, maybe_keys = await self.upload(f, content_type=content_type)
|
||||||
f, content_type=content_type
|
|
||||||
)
|
|
||||||
|
|
||||||
await aiofiles.os.remove(emote_file)
|
await aiofiles.os.remove(emote_file)
|
||||||
|
|
||||||
if type(resp) != nio.UploadResponse:
|
if type(resp) != nio.UploadResponse:
|
||||||
self.logger.warning(
|
self.logger.warning(f"Failed to upload {emote_name}")
|
||||||
f"Failed to upload {emote_name}"
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.info(f"Uploaded {emote_name}")
|
self.logger.info(f"Uploaded {emote_name}")
|
||||||
|
@ -101,9 +97,7 @@ class MatrixClient(nio.AsyncClient):
|
||||||
resp = await self.room_put_state(room_id, event_type, content)
|
resp = await self.room_put_state(room_id, event_type, content)
|
||||||
|
|
||||||
if type(resp) == nio.RoomPutStateError:
|
if type(resp) == nio.RoomPutStateError:
|
||||||
self.logger.warning(
|
self.logger.warning(f"Failed to send emote state: {resp}")
|
||||||
f"Failed to send emote state: {resp}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class DiscordClient(discord.Client):
|
class DiscordClient(discord.Client):
|
||||||
|
@ -138,9 +132,9 @@ class DiscordClient(discord.Client):
|
||||||
f"Guild: {emote_guild.name} Room: {emote_room}"
|
f"Guild: {emote_guild.name} Room: {emote_room}"
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.gather(*map(
|
await asyncio.gather(
|
||||||
self.matrix_client.upload_emote, emote_guild.emojis
|
*map(self.matrix_client.upload_emote, emote_guild.emojis)
|
||||||
))
|
)
|
||||||
|
|
||||||
self.logger.info("Sending state event to room...")
|
self.logger.info("Sending state event to room...")
|
||||||
|
|
Loading…
Reference in a new issue