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'