From e372aadd739b37b5c6f487fdb33edd929ebf02cc Mon Sep 17 00:00:00 2001 From: Ville Ranki Date: Sat, 12 Sep 2020 16:48:47 +0300 Subject: [PATCH] Mastodon: Per-room accounts --- README.md | 23 ++++++++++---- bot.py | 13 ++++++-- modules/md.py | 88 ++++++++++++++++++++++++++++++++++++++++++--------- 3 files changed, 100 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index e1afbf2..039b871 100644 --- a/README.md +++ b/README.md @@ -364,22 +364,33 @@ sends different messages and has not yet been reverse engineered. ### Mastodon -Send toots to Mastodon. You can login to Mastodon with the bot and toot with it. Login is -personal - it's mapped to your Matrix ID. You can limit usage to bot owners only or make -it public. +Send toots to Mastodon. You can login to Mastodon with the bot and toot with it. + +Normal login is personal - it's mapped to your Matrix ID. + +Room login overrides personal login and allows room admins to toot to a specified account. + +You can limit usage to bot owners only or make it public. + +Note: You can subscribe to Mastodon users and hashtags with stock RSS feed integration. +Mastodon generates the feeds automatically! Commands: * !md status - print status of mastodon module -* !md login [instanceurl] [e-mail] [password] - log in your Matrix user to mastodon instance (do in private chat!) -* !md toot [toot] - send a toot message +* !md login [instanceurl] [e-mail] [password] - log in your Matrix user to Mastodon instance (do in private chat!) +* !md toot [toot] - send a toot message. If said in a room with per-room login, user must be a admin in the room. * !md logout - log out the user from the bot +* !md roomlogin [room] [instanceurl] [e-mail] [password] - same as login but assigns a per-room mastodon account (do in private chat!) +* !md roomlogout - Say in a room that has per-room login. This will remove the login. Must be room admin. * !md setpublic - ANY user can use login command to login and use the Mastodon module (must be done as bot owner) * !md setprivate - Only bot owners can use Mastodon module (default) (must be done as bot owner) +* !md clear - Clear all logins from the bot -Example of login command: +Example commands: * !md login https://my.instance/ me@email.invalid r00tm3 +* !md roomlogin #myroom:matrix.org https://my.instance/ me@email.invalid r00tm3 ## Bot setup diff --git a/bot.py b/bot.py index aca8b1f..088d8ec 100755 --- a/bot.py +++ b/bot.py @@ -19,7 +19,7 @@ import datetime from importlib import reload import requests -from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError +from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LoginError, RoomMemberEvent, RoomVisibility, RoomPreset, RoomCreateError, RoomResolveAliasResponse # Couple of custom exceptions @@ -37,7 +37,7 @@ class Bot: def __init__(self): self.appid = 'org.vranki.hemppa' - self.version = '1.4' + self.version = '1.5' self.client = None self.join_on_invite = False self.modules = dict() @@ -144,6 +144,12 @@ class Bot: def get_room_by_id(self, room_id): return self.client.rooms[room_id] + async def get_room_by_alias(self, alias): + rar = await self.client.room_resolve_alias(alias) + if type(rar) is RoomResolveAliasResponse: + return rar.room_id + return None + # Throws exception if event sender is not a room admin def must_be_admin(self, room, event): if not self.is_admin(room, event): @@ -197,7 +203,8 @@ class Bot: async def message_cb(self, room, event): # Ignore if asked to ignore if self.should_ignore_event(event): - print('Ignoring this!') + if self.debug: + print('Ignoring event!') return body = event.body diff --git a/modules/md.py b/modules/md.py index 3b78fbc..23c6c70 100644 --- a/modules/md.py +++ b/modules/md.py @@ -6,6 +6,7 @@ from modules.common.module import BotModule class MatrixModule(BotModule): apps = dict() # instance url <-> [app_id, app_secret] logins = dict() # mxid <-> [username, accesstoken, instanceurl] + roomlogins = dict() # roomid <-> [username, accesstoken, instanceurl] public = False async def matrix_message(self, bot, room, event): @@ -16,16 +17,22 @@ class MatrixModule(BotModule): if not self.public: bot.must_be_owner(event) toot_body = " ".join(args[1:]) - if event.sender in self.logins.keys(): + accesstoken = None + if room.room_id in self.roomlogins.keys(): + bot.must_be_admin(room, event) + username = self.roomlogins[room.room_id][0] + accesstoken = self.roomlogins[room.room_id][1] + instanceurl = self.roomlogins[room.room_id][2] + elif event.sender in self.logins.keys(): username = self.logins[event.sender][0] accesstoken = self.logins[event.sender][1] instanceurl = self.logins[event.sender][2] + if accesstoken: toottodon = Mastodon( access_token = accesstoken, api_base_url = instanceurl ) tootdict = toottodon.toot(toot_body) - print(tootdict) await bot.send_text(room, tootdict['url']) else: await bot.send_text(room, f'{event.sender} has not logged in yet with the bot. Please do so.') @@ -39,27 +46,55 @@ class MatrixModule(BotModule): instanceurl = args[1] username = args[2] password = args[3] - if not instanceurl in self.apps.keys(): - app = Mastodon.create_app(f'Hemppa The Bot - {bot.client.user}', api_base_url = instanceurl) - self.apps[instanceurl] = [app[0], app[1]] - bot.save_settings() - await bot.send_text(room, f'Registered Mastodon app on {instanceurl}') - - mastodon = Mastodon(client_id = self.apps[instanceurl][0], client_secret = self.apps[instanceurl][1], api_base_url = instanceurl) - access_token = mastodon.log_in(username, password) - self.logins[mxid] = [username, access_token, instanceurl] - bot.save_settings() - await bot.send_text(room, f'Logged into {instanceurl} as {username}') + await self.register_app_if_necessary(bot, room, instanceurl) + await self.login_to_account(bot, room, mxid, None, instanceurl, username, password) + return + if len(args) == 5: + if args[0] == "roomlogin": + if not self.public: + bot.must_be_owner(event) + roomalias = args[1] + instanceurl = args[2] + username = args[3] + password = args[4] + roomid = await bot.get_room_by_alias(roomalias) + if roomid: + await self.register_app_if_necessary(bot, room, instanceurl) + await self.login_to_account(bot, room, None, roomid, instanceurl, username, password) + else: + await bot.send_text(room, f'Unknown room alias {roomalias} - invite bot to the room first.') return if len(args) == 1: if args[0] == "status": - await bot.send_text(room, f'{len(self.logins)} users logged in, app registered on {len(self.apps)} instances, public use enabled: {self.public}') + out = f'App registered on {len(self.apps)} instances, public use enabled: {self.public}\n' + out = out + f'{len(self.logins)} users logged in:\n' + for login in self.logins.keys(): + out = out + f' - {login} as {self.logins[login][0]} on {self.logins[login][2]}\n' + out = out + f'{len(self.roomlogins)} per-room logins:\n' + for roomlogin in self.roomlogins: + out = out + f' - {roomlogin} as {self.roomlogins[roomlogin][0]} on {self.roomlogins[roomlogin][2]}\n' + + await bot.send_text(room, out) if args[0] == "logout": if event.sender in self.logins.keys(): # TODO: Is there a way to invalidate the access token with API? del self.logins[event.sender] bot.save_settings() await bot.send_text(room, f'{event.sender} login data removed from the bot.') + if args[0] == "roomlogout": + bot.must_be_admin(room, event) + if room.room_id in self.roomlogins.keys(): + del self.roomlogins[room.room_id] + bot.save_settings() + await bot.send_text(room, f'Login data for this room removed from the bot.') + else: + await bot.send_text(room, f'No login found for room id {room.room_id}.') + if args[0] == "clear": + bot.must_be_owner(event) + self.logins = dict() + self.roomlogins = dict() + bot.save_settings() + await bot.send_text(room, f'All Mastodon logins cleared') if args[0] == "setpublic": bot.must_be_owner(event) self.public = True @@ -70,11 +105,32 @@ class MatrixModule(BotModule): self.public = False bot.save_settings() await bot.send_text(room, f'Mastodon usage is now restricted to bot owners') - + + async def register_app_if_necessary(self, bot, room, instanceurl): + if not instanceurl in self.apps.keys(): + app = Mastodon.create_app(f'Hemppa The Bot - {bot.client.user}', api_base_url = instanceurl) + self.apps[instanceurl] = [app[0], app[1]] + bot.save_settings() + await bot.send_text(room, f'Registered Mastodon app on {instanceurl}') + + async def login_to_account(self, bot, room, mxid, roomid, instanceurl, username, password): + mastodon = Mastodon(client_id = self.apps[instanceurl][0], client_secret = self.apps[instanceurl][1], api_base_url = instanceurl) + access_token = mastodon.log_in(username, password) + print('login_To_account', mxid, roomid) + if mxid: + self.logins[mxid] = [username, access_token, instanceurl] + await bot.send_text(room, f'Logged Matrix user {mxid} into {instanceurl} as {username}') + elif roomid: + self.roomlogins[roomid] = [username, access_token, instanceurl] + await bot.send_text(room, f'Set room {roomid} Mastodon user to {username} on {instanceurl}') + + bot.save_settings() + def get_settings(self): data = super().get_settings() data['apps'] = self.apps data['logins'] = self.logins + data['roomlogins'] = self.roomlogins data['public'] = self.public return data @@ -84,6 +140,8 @@ class MatrixModule(BotModule): self.apps = data["apps"] if data.get("logins"): self.logins = data["logins"] + if data.get("roomlogins"): + self.roomlogins = data["roomlogins"] if data.get("public"): self.public = data["public"]