Mastodon: Per-room accounts

This commit is contained in:
Ville Ranki 2020-09-12 16:48:47 +03:00
parent 28db0fedfe
commit e372aadd73
3 changed files with 100 additions and 24 deletions

View File

@ -364,22 +364,33 @@ sends different messages and has not yet been reverse engineered.
### Mastodon ### Mastodon
Send toots to Mastodon. You can login to Mastodon with the bot and toot with it. Login is Send toots to Mastodon. You can login to Mastodon with the bot and toot with it.
personal - it's mapped to your Matrix ID. You can limit usage to bot owners only or make
it public. 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: Commands:
* !md status - print status of mastodon module * !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 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 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 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 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 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 login https://my.instance/ me@email.invalid r00tm3
* !md roomlogin #myroom:matrix.org https://my.instance/ me@email.invalid r00tm3
## Bot setup ## Bot setup

13
bot.py
View File

@ -19,7 +19,7 @@ import datetime
from importlib import reload from importlib import reload
import requests 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 # Couple of custom exceptions
@ -37,7 +37,7 @@ class Bot:
def __init__(self): def __init__(self):
self.appid = 'org.vranki.hemppa' self.appid = 'org.vranki.hemppa'
self.version = '1.4' self.version = '1.5'
self.client = None self.client = None
self.join_on_invite = False self.join_on_invite = False
self.modules = dict() self.modules = dict()
@ -144,6 +144,12 @@ class Bot:
def get_room_by_id(self, room_id): def get_room_by_id(self, room_id):
return self.client.rooms[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 # Throws exception if event sender is not a room admin
def must_be_admin(self, room, event): def must_be_admin(self, room, event):
if not self.is_admin(room, event): if not self.is_admin(room, event):
@ -197,7 +203,8 @@ class Bot:
async def message_cb(self, room, event): async def message_cb(self, room, event):
# Ignore if asked to ignore # Ignore if asked to ignore
if self.should_ignore_event(event): if self.should_ignore_event(event):
print('Ignoring this!') if self.debug:
print('Ignoring event!')
return return
body = event.body body = event.body

View File

@ -6,6 +6,7 @@ from modules.common.module import BotModule
class MatrixModule(BotModule): class MatrixModule(BotModule):
apps = dict() # instance url <-> [app_id, app_secret] apps = dict() # instance url <-> [app_id, app_secret]
logins = dict() # mxid <-> [username, accesstoken, instanceurl] logins = dict() # mxid <-> [username, accesstoken, instanceurl]
roomlogins = dict() # roomid <-> [username, accesstoken, instanceurl]
public = False public = False
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
@ -16,16 +17,22 @@ class MatrixModule(BotModule):
if not self.public: if not self.public:
bot.must_be_owner(event) bot.must_be_owner(event)
toot_body = " ".join(args[1:]) 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] username = self.logins[event.sender][0]
accesstoken = self.logins[event.sender][1] accesstoken = self.logins[event.sender][1]
instanceurl = self.logins[event.sender][2] instanceurl = self.logins[event.sender][2]
if accesstoken:
toottodon = Mastodon( toottodon = Mastodon(
access_token = accesstoken, access_token = accesstoken,
api_base_url = instanceurl api_base_url = instanceurl
) )
tootdict = toottodon.toot(toot_body) tootdict = toottodon.toot(toot_body)
print(tootdict)
await bot.send_text(room, tootdict['url']) await bot.send_text(room, tootdict['url'])
else: else:
await bot.send_text(room, f'{event.sender} has not logged in yet with the bot. Please do so.') 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] instanceurl = args[1]
username = args[2] username = args[2]
password = args[3] password = args[3]
if not instanceurl in self.apps.keys(): await self.register_app_if_necessary(bot, room, instanceurl)
app = Mastodon.create_app(f'Hemppa The Bot - {bot.client.user}', api_base_url = instanceurl) await self.login_to_account(bot, room, mxid, None, instanceurl, username, password)
self.apps[instanceurl] = [app[0], app[1]] return
bot.save_settings() if len(args) == 5:
await bot.send_text(room, f'Registered Mastodon app on {instanceurl}') if args[0] == "roomlogin":
if not self.public:
mastodon = Mastodon(client_id = self.apps[instanceurl][0], client_secret = self.apps[instanceurl][1], api_base_url = instanceurl) bot.must_be_owner(event)
access_token = mastodon.log_in(username, password) roomalias = args[1]
self.logins[mxid] = [username, access_token, instanceurl] instanceurl = args[2]
bot.save_settings() username = args[3]
await bot.send_text(room, f'Logged into {instanceurl} as {username}') 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 return
if len(args) == 1: if len(args) == 1:
if args[0] == "status": 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 args[0] == "logout":
if event.sender in self.logins.keys(): if event.sender in self.logins.keys():
# TODO: Is there a way to invalidate the access token with API? # TODO: Is there a way to invalidate the access token with API?
del self.logins[event.sender] del self.logins[event.sender]
bot.save_settings() bot.save_settings()
await bot.send_text(room, f'{event.sender} login data removed from the bot.') 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": if args[0] == "setpublic":
bot.must_be_owner(event) bot.must_be_owner(event)
self.public = True self.public = True
@ -71,10 +106,31 @@ class MatrixModule(BotModule):
bot.save_settings() bot.save_settings()
await bot.send_text(room, f'Mastodon usage is now restricted to bot owners') 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): def get_settings(self):
data = super().get_settings() data = super().get_settings()
data['apps'] = self.apps data['apps'] = self.apps
data['logins'] = self.logins data['logins'] = self.logins
data['roomlogins'] = self.roomlogins
data['public'] = self.public data['public'] = self.public
return data return data
@ -84,6 +140,8 @@ class MatrixModule(BotModule):
self.apps = data["apps"] self.apps = data["apps"]
if data.get("logins"): if data.get("logins"):
self.logins = data["logins"] self.logins = data["logins"]
if data.get("roomlogins"):
self.roomlogins = data["roomlogins"]
if data.get("public"): if data.get("public"):
self.public = data["public"] self.public = data["public"]