diff --git a/README.md b/README.md index fa89cd1..5a23f02 100644 --- a/README.md +++ b/README.md @@ -624,12 +624,27 @@ by default, but you can set any single instance to search on. ### User management -Admin commands to manage users. +Admin commands to manage users and some utilities. + +You can classify users based on MXID to get stats on where users come from. #### Usage -* !users list [pattern] - List users matching wildcard pattern (must be owner) +* !users list [pattern] - List users matching wildcard pattern in this room (must be owner) * !users kick [pattern] - Kick users matching wildcard pattern from room (must be admin in room) +* !users classify add [name] [pattern] - Add a classification pattern (must be owner) +* !users classify list - List classifications +* !users classify del [name] - Delete classification +* !users roomstats - List how many users are in each class in this room +* !users stats - List how many users are in each class globally as seen by bot + +Example: + +* !users classify add matrix.org @*:matrix.org +* !users classify add libera.chat @*:libera.chat +* !users classify add discord @*discordpuppet*:* +* !users stats +* !users roomstats ### RASP (Gliding Weather forecast) diff --git a/modules/users.py b/modules/users.py index 81a101a..379a6de 100644 --- a/modules/users.py +++ b/modules/users.py @@ -2,10 +2,43 @@ from modules.common.module import BotModule import fnmatch class MatrixModule(BotModule): + def __init__(self, name): + super().__init__(name) + self.classes = dict() # classname <-> pattern + async def matrix_message(self, bot, room, event): args = event.body.split() args.pop(0) + if len(args) == 1: + if args[0] == 'stats' or args[0] == 'roomstats': + stats = dict() + for name, pattern in self.classes.items(): + stats[name] = 0 + if args[0] == 'stats': + allusers = self.get_users(bot) + else: + allusers = self.get_users(bot, room.room_id) + total = len(allusers) + matched = 0 + for user in allusers: + for name, pattern in self.classes.items(): + match = fnmatch.fnmatch(user, pattern) + if match: + stats[name] = stats[name] + 1 + matched = matched + 1 + + stats['Matrix'] = total - matched + stats = dict(sorted(stats.items(), key=lambda item: item[1], reverse=True)) + + if args[0] == 'stats': + reply = f'I am seeing total {len(allusers)} users:\n' + else: + reply = f'I am seeing {len(allusers)} users in this room:\n' + for name in stats: + reply = reply + f' - {name}: {stats[name]} ({stats[name] / total * 100}%)\n' + await bot.send_text(room, reply) + return if len(args) == 2: if args[0] == 'list': bot.must_be_owner(event) @@ -25,20 +58,64 @@ class MatrixModule(BotModule): else: await bot.send_text(room, 'No matching users found!') return + if args[0] == 'classify': + if args[1] == 'list': + await bot.send_text(room, f'Classes in use: {self.classes}.') + return + elif len(args) == 4: + if args[0] == 'classify': + if args[1] == 'add': + bot.must_be_owner(event) + name = args[2] + pattern = args[3] + self.classes[name] = pattern + await bot.send_text(room, f'Added class {name} pattern {pattern}.') + bot.save_settings() + return + elif len(args) == 3: + if args[0] == 'classify': + if args[1] == 'del': + bot.must_be_owner(event) + name = args[2] + del self.classes[name] + await bot.send_text(room, f'Deleted class {name}.') + bot.save_settings() + return + await bot.send_text(room, 'Unknown command - please see readme') - def search_users(self, bot, pattern): + def get_users(self, bot, roomid=None): allusers = [] - for croomid in bot.client.rooms: + for croomid in self.bot.client.rooms: + if roomid and (roomid != croomid): + break try: - users = bot.client.rooms[croomid].users + users = self.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 allusers + + def search_users(self, bot, pattern): + allusers = self.get_users(self, bot) return fnmatch.filter(allusers, pattern) def help(self): return 'User management tools' + + def get_settings(self): + data = super().get_settings() + data["classes"] = self.classes + return data + + def set_settings(self, data): + super().set_settings(data) + if data.get("classes"): + self.classes = data["classes"] + + def matrix_start(self, bot): + super().matrix_start(bot) + self.bot = bot \ No newline at end of file