diff --git a/README.md b/README.md index 7981dfe..e249636 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Bot management commands. * !bot stats - show statistics on matrix users seen by bot The following must be done as the bot owner: + * !bot enable [module] - enable module * !bot disable [module] - disable module * !bot quit - quit the bot process @@ -53,6 +54,7 @@ The following must be done as the bot owner: The uri cache prevents the bot from uploading a blob from a url repeatedly * !bot leave - ask bot to leave this room * !bot modules - list all modules including enabled status +* !bot rooms - list rooms the bot is on ### Help @@ -183,11 +185,15 @@ Example: This module is for interacting with the room that the commands are being executed on. -* !room servers: Lists the servers in the room -* !room joined: Responds with the joined members count -* !room banned: Lists the banned users and their provided reason -* !room kicked: Lists the kicked users and their provided reason -* !room state [event type] [optional state key]: Gets a state event with given event type and optional state key +* !room servers Lists the servers in the room +* !room joined Responds with the joined members count +* !room banned Lists the banned users and their provided reason +* !room kicked Lists the kicked users and their provided reason +* !room state [event type] [optional state key] Gets a state event with given event type and optional state key +* !room tombstone [target] Creates a tombstone event pointing to target room. Target room can be alias (starting with #) or id (starting with !). + +Note on tombstone: If using alias, bot must be present in target room. This is the preferred way. If using id, make sure it's correct, as it's not validated! +Tombstoning requires power level for room upgrade. Make sure bot has it in the room. ### Welcome to Room @@ -602,6 +608,15 @@ by default, but you can set any single instance to search on. * !pt setinstance [url] - Set instance url, must end with / (example: https://peertube.cpy.re/) * !pt showinstance - Print which instance is used +### User management + +Admin commands to manage users. + +#### Usage + +* !users list [pattern] - List users matching wildcard pattern +* !users kick [pattern] - Kick users matching wildcard pattern from room + ## Bot setup * Create a Matrix user @@ -668,9 +683,9 @@ docker-compose up ## Env variables -`MATRIX_USER` is the full MXID (not just username) of the Matrix user. `MATRIX_ACCESS_TOKEN` -and `MATRIX_SERVER` should be self-explanatory. Set `JOIN_ON_INVITE` (default true) to false -if you don't want the bot automatically joining rooms. +`MATRIX_USER` is the full MXID (not just username) of the Matrix user. `MATRIX_ACCESS_TOKEN` +and `MATRIX_SERVER` should be url to the user's server (non-delegated server). Set `JOIN_ON_INVITE` (default true) +to false if you don't want the bot automatically joining rooms. You can get access token by logging in with Element Android and looking from Settings / Help & About. diff --git a/bot.py b/bot.py index 1f0a0e7..2817b64 100755 --- a/bot.py +++ b/bot.py @@ -424,8 +424,8 @@ class Bot: async def memberevent_cb(self, room, event): # Automatically leaves rooms where bot is alone. - if room.member_count == 1 and event.membership=='leave': - self.logger.info(f"membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' - leaving room as i don't want to be left alone!") + if room.member_count == 1 and event.membership=='leave' and event.sender != self.matrix_user: + self.logger.info(f"Membership event in {room.display_name} ({room.room_id}) with {room.member_count} members by '{event.sender}' (I am {self.matrix_user})- leaving room as i don't want to be left alone!") await self.client.room_leave(room.room_id) def load_module(self, modulename): diff --git a/modules/bot.py b/modules/bot.py index 87deab7..f13fd62 100644 --- a/modules/bot.py +++ b/modules/bot.py @@ -57,6 +57,8 @@ class MatrixModule(BotModule): await self.export_settings(bot, event) elif args[1] == 'ping': await self.get_ping(bot, room, event) + elif args[1] == 'rooms': + await self.rooms(bot, room, event) elif len(args) == 3: if args[1] == 'enable': @@ -297,6 +299,14 @@ class MatrixModule(BotModule): bot.uri_cache = dict() bot.save_settings() + async def rooms(self, bot, room, event): + bot.must_be_owner(event) + rooms = [] + for croomid in bot.client.rooms: + roomobj = bot.get_room_by_id(croomid) + rooms.append(roomobj.machine_name) + await bot.send_text(room, f'I\'m in following {len(rooms)} rooms: {rooms}') + def disable(self): raise ModuleCannotBeDisabled diff --git a/modules/loc.py b/modules/loc.py index a1a7729..04a8f4d 100644 --- a/modules/loc.py +++ b/modules/loc.py @@ -39,7 +39,7 @@ class MatrixModule(BotModule): latlon[0] + "&mlon=" + latlon[1] plain = sender + ' 🚩 ' + osm_link - html = f'{sender} 🚩 {location_text}' + html = f'{sender} 🚩 {location_text}' await self.bot.send_html(room, html, plain) diff --git a/modules/room.py b/modules/room.py index 075dd19..ccfc2b9 100644 --- a/modules/room.py +++ b/modules/room.py @@ -43,6 +43,8 @@ class MatrixModule(BotModule): await MatrixModule.kicked_members(bot, room) elif command == 'state': await MatrixModule.get_state_event(bot, room, args) + elif command == 'tombstone': + await MatrixModule.tombstone(bot, room, args, event) @staticmethod async def servers_in_room(bot, room: nio.MatrixRoom): @@ -210,3 +212,35 @@ class MatrixModule(BotModule): else: # Raise the error and the bot module will handle the rest raise res + + @staticmethod + async def tombstone(bot, room, args, event): + bot.must_be_admin(room, event) + + body = "This room has been replaced" # Todo: make settable + + if len(args) == 2: + target = args[1] + if "#" in target: + targetid = await bot.get_room_by_alias(target) + if not targetid: + await bot.send_text(room, f"Bot is not on room {targetid}?") + return + elif "!" in target: + targetid = target + else: + await bot.send_text(room, f"Give a room alias (starts with #) or room id (starts with !)") + return + + tombstone_event = { + "body": body, + "replacement_room": targetid + } + response = await bot.client.room_put_state(room.room_id, 'm.room.tombstone', tombstone_event) + if type(response) == nio.RoomPutStateResponse: + await bot.send_text(room, f"See you in the new room!") + await bot.client.room_leave(room.room_id) + else: + await bot.send_text(room, f"Error creating tombstone event: {response}") + return + await bot.send_text(room, f"Usage: !room tombstone #room:server.org") diff --git a/modules/url.py b/modules/url.py index 0f1c901..954890b 100644 --- a/modules/url.py +++ b/modules/url.py @@ -128,7 +128,7 @@ class MatrixModule(BotModule): # failed fetching, give up continue - msg = None + msg = "" if status == "TITLE" and title is not None: msg = f"Title: {title}" @@ -143,7 +143,7 @@ class MatrixModule(BotModule): elif status == "BOTH" and description is not None: msg = f"Description: {description}" - if msg is not None: + if msg.strip(): # Evaluates to true on non-empty strings await self.bot.send_text(room, msg, msgtype=self.type, bot_ignore=True) except Exception as e: self.logger.warning(f"Unexpected error in url module text_cb: {e}") diff --git a/modules/users.py b/modules/users.py new file mode 100644 index 0000000..0648ffb --- /dev/null +++ b/modules/users.py @@ -0,0 +1,42 @@ +from modules.common.module import BotModule +import fnmatch + +class MatrixModule(BotModule): + async def matrix_message(self, bot, room, event): + args = event.body.split() + args.pop(0) + + if len(args) == 2: + if args[0] == 'list': + users = self.search_users(bot, args[1]) + if len(users): + await bot.send_text(room, ' '.join(users)) + else: + await bot.send_text(room, 'No matching users found!') + return + if args[0] == 'kick': + users = self.search_users(bot, args[1]) + if len(users): + for user in users: + self.logger.debug(f"Kicking {user} from {room.room_id} as requested by {event.sender}") + await bot.client.room_kick(room.room_id, user) + else: + await bot.send_text(room, 'No matching users found!') + return + await bot.send_text(room, 'Unknown command - please see readme') + + def search_users(self, bot, pattern): + allusers = [] + for croomid in bot.client.rooms: + try: + users = bot.client.rooms[croomid].users + except (KeyError, ValueError) as e: + self.logger.warning(f"Couldn't get user list in room with id {croomid}, skipping: {repr(e)}") + continue + for user in users: + allusers.append(user) + allusers = list(dict.fromkeys(allusers)) # Deduplicate + return fnmatch.filter(allusers, pattern) + + def help(self): + return 'User management tools'