diff --git a/README.md b/README.md index c9b1ed4..020e68d 100644 --- a/README.md +++ b/README.md @@ -411,6 +411,26 @@ Example commands: * !md login https://my.instance/ me@email.invalid r00tm3 * !md roomlogin #myroom:matrix.org https://my.instance/ me@email.invalid r00tm3 +### Relay bridge + +Bridges two or more Matrix rooms together via relaybot. + +Note: Room ID is not same as room alias! Rooms can exist without aliases so ID's are more flexible. Room id is usually in format !123LotOfRandomChars:server.org + +To get room ID, it's in Element Web room settings | Advanced | Internal room ID + +Before bridging the bot must be present on both rooms. + +Commands: + +* !relay bridge [roomid] - Bridge room with given ID to this room (must be done as bot owner) +* !relay list - List bridged rooms (and their index numbers) (must be done as bot owner) +* !relay unbridge [number] - Remove the given bridge number (must be done as bot owner) + +File uploads, joins, leaves or other special events are not (yet) handled. Contributions welcome. + +Relaybots are stupid. Please prefer real Matrix bridges to this. Sometimes there's no alternative. + ## Bot setup * Create a Matrix user diff --git a/bot.py b/bot.py index 1b043fe..4947054 100755 --- a/bot.py +++ b/bot.py @@ -142,7 +142,10 @@ class Bot: self.client.event_callbacks.remove(cb_object) def get_room_by_id(self, room_id): - return self.client.rooms[room_id] + try: + return self.client.rooms[room_id] + except KeyError: + return None async def get_room_by_alias(self, alias): rar = await self.client.room_resolve_alias(alias) @@ -205,7 +208,7 @@ class Bot: # Ignore if asked to ignore if self.should_ignore_event(event): if self.debug: - print('Ignoring event!') + self.logger.debug('Ignoring event!') return body = event.body @@ -450,4 +453,4 @@ async def main(): try: asyncio.run(main()) except Exception as e: - print(e) + traceback.print_exc(file=sys.stderr) diff --git a/modules/relay.py b/modules/relay.py new file mode 100644 index 0000000..adf5d9a --- /dev/null +++ b/modules/relay.py @@ -0,0 +1,108 @@ +from modules.common.module import BotModule +from nio import RoomMessageText + +class MatrixModule(BotModule): + def __init__(self, name): + super().__init__(name) + self.bridges = dict() + self.bot = None + + async def message_cb(self, room, event): + if self.bot.should_ignore_event(event): + return + + if event.body.startswith('!'): + return + + source_id = None + target_id = None + + for src_id, tgt_id in self.bridges.items(): + if room.room_id == src_id: + source_id = src_id + target_id = tgt_id + elif room.room_id == tgt_id: + source_id = tgt_id + target_id = src_id + + if not source_id or not target_id: + return + + target_room = self.bot.get_room_by_id(target_id) + if(target_room): + sendernick = target_room.user_name(event.sender) + if not sendernick: + sendernick = event.sender + await self.bot.send_text(target_room, f'<{sendernick}> {event.body}', msgtype="m.text", bot_ignore=True) + else: + self.logger.warning(f"Bot doesn't seem to be in bridged room {target_id}") + + def matrix_start(self, bot): + super().matrix_start(bot) + bot.client.add_event_callback(self.message_cb, RoomMessageText) + self.bot = bot + + def matrix_stop(self, bot): + super().matrix_stop(bot) + bot.remove_callback(self.message_cb) + self.bot = None + + async def matrix_message(self, bot, room, event): + bot.must_be_admin(room, event) + args = event.body.split() + args.pop(0) + if len(args) == 1: + if args[0] == 'list': + i = 1 + msg = f"Active relay bridges ({len(self.bridges)}):\n" + for src_id, tgt_id in self.bridges.items(): + srcroom = self.bot.get_room_by_id(src_id) + tgtroom = self.bot.get_room_by_id(tgt_id) + + if srcroom: + srcroom = srcroom.display_name + else: + srcroom = f'??? {src_id}' + + if tgtroom: + tgtroom = tgtroom.display_name + else: + tgtroom = f'??? {tgt_id}' + + msg += f'{i}: {srcroom} <-> {tgtroom}' + i = i + 1 + await bot.send_text(room, msg) + + if len(args) == 2: + if args[0] == 'bridge': + roomid = args[1] + room_to_bridge = bot.get_room_by_id(roomid) + if room_to_bridge: + await bot.send_text(room, f'Bridging {room_to_bridge.display_name} here.') + self.bridges[room.room_id] = roomid + bot.save_settings() + else: + await bot.send_text(room, f'I am not on room with id {roomid} (note: use id, not alias)!') + elif args[0] == 'unbridge': + idx = int(args[1]) - 1 + i = 0 + for src_id, tgt_id in self.bridges.items(): + if i == idx: + del self.bridges[src_id] + await bot.send_text(room, f'Unbridged {src_id} and {tgt_id}.') + bot.save_settings() + return + i = i + 1 + + def help(self): + return 'Simple relaybot between two Matrix rooms' + + def get_settings(self): + data = super().get_settings() + data["bridges"] = self.bridges + return data + + def set_settings(self, data): + super().set_settings(data) + if data.get("bridges"): + self.bridges = data["bridges"] diff --git a/modules/url.py b/modules/url.py index aea04b5..f2c398b 100644 --- a/modules/url.py +++ b/modules/url.py @@ -3,6 +3,8 @@ import shlex from functools import lru_cache import httpx +import sys +import traceback from bs4 import BeautifulSoup from nio import RoomMessageText @@ -90,6 +92,7 @@ class MatrixModule(BotModule): title, description = self.get_content_from_url(url) except Exception as e: self.logger.warning(f"could not fetch url: {e}") + traceback.print_exc(file=sys.stderr) # failed fetching, give up continue