From 4017a53e5b01061680b91a1c5b5727f11228d94c Mon Sep 17 00:00:00 2001 From: Ville Ranki Date: Wed, 25 Dec 2019 20:49:20 +0200 Subject: [PATCH] Version 1.1: access control, commands can require admin or owner status. --- README.md | 27 +++++++++++++++++---------- bot.py | 44 +++++++++++++++++++++++++++++++++++--------- docker-compose.yml | 1 + modules/cron.py | 2 ++ modules/echo.py | 6 ------ modules/googlecal.py | 4 ++++ modules/help.py | 2 +- modules/teamup.py | 6 ++++++ 8 files changed, 66 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 56a951d..f009923 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,15 @@ Howto: * Create a calendar in Teamup https://teamup.com/ * Get api key at https://teamup.com/api-keys/request -* !teamup apikey [your api key] -* !teamup add [calendar id] +* !teamup apikey [your api key] +* !teamup add [calendar id] Commands: -* !teamup apikey [apikey] - set api key +* !teamup apikey [apikey] - set api key (Must be done as bot owner) * !teamup - list upcoming events in calendar -* !teamup add [calendar id] - add calendar to this room -* !teamup del [calendar id] - delete calendar from this room +* !teamup add [calendar id] - add calendar to this room (Must be done as room admin) +* !teamup del [calendar id] - delete calendar from this room (Must be done as room admin) * !teamup list - list calendars in this room * !teamup poll - poll now for changes @@ -82,8 +82,8 @@ Commands: * !googlecal - Show next 10 events in calendar * !googlecal today - Show today's events -* !googlecal add [calendar id] - Add new calendar to room -* !googlecal del [calendar id] - Delete calendar from room +* !googlecal add [calendar id] - Add new calendar to room (Must be done as room admin) +* !googlecal del [calendar id] - Delete calendar from room (Must be done as room admin) * !googlecal list - List calendars in this room ### Cron @@ -92,9 +92,9 @@ Can schedule things to be done. Commands: -* !cron daily [hour] [command] - Run command on start of hour +* !cron daily [hour] [command] - Run command on start of hour (Must be done as room admin) * !cron list - List commands in this room -* !cron clear - Clear command s in this room +* !cron clear - Clear command s in this room (Must be done as room admin) Examples: @@ -127,7 +127,8 @@ sudo apt install python3-pip sudo pip3 install pipenv pipenv shell pipenv install --pre -MATRIX_USER="@user:matrix.org" MATRIX_ACCESS_TOKEN="MDAxOGxvYlotofcharacters53CgYAYFgo" MATRIX_SERVER="https://matrix.org" JOIN_ON_INVITE=True python3 bot.py +MATRIX_USER="@user:matrix.org" MATRIX_ACCESS_TOKEN="MDAxOGxvYlotofcharacters53CgYAYFgo" MATRIX_SERVER="https://matrix.org" JOIN_ON_INVITE=True BOT_OWNERS=@botowner:matrix.org + python3 bot.py ``` ## Running with Docker @@ -139,6 +140,7 @@ MATRIX_USER=@user:matrix.org MATRIX_ACCESS_TOKEN=MDAxOGxvYlotofcharacters53CgYAYFgo MATRIX_SERVER=https://matrix.org JOIN_ON_INVITE=True +BOT_OWNERS=@user1:matrix.org,@user2:matrix.org ``` Note: without quotes! @@ -156,6 +158,11 @@ join invites automatically. You can set MATRIX_PASSWORD if you want to get access token. Normally you can use Riot to get it. +BOT_OWNERS is a comma-separated list of matrix id's for the owners of the bot. Some commands require +sender to be bot owner. Typically set your own id into it. Don't include bot itself in BOT_OWNERS if cron +or any other module that can cause bot to send custom commands is used as it could potentially be used to run +owner commands as the bot itself. + ## Module API Just write a python file with desired command name and place it in modules. See current modules for diff --git a/bot.py b/bot.py index 0c8e136..952ab28 100755 --- a/bot.py +++ b/bot.py @@ -13,15 +13,22 @@ import json import urllib.parse from nio import (AsyncClient, RoomMessageText, RoomMessageUnknown, JoinError, InviteEvent) +# Couple of custom exceptions +class CommandRequiresAdmin(Exception): + pass + +class CommandRequiresOwner(Exception): + pass class Bot: appid = 'org.vranki.hemppa' - version = '1.0' + version = '1.1' client = None join_on_invite = False modules = dict() pollcount = 0 poll_task = None + owners = [] async def send_text(self, room, body): msg = { @@ -42,6 +49,27 @@ class Bot: def get_room_by_id(self, room_id): return self.client.rooms[room_id] + # Throws exception if event sender is not a room admin + def must_be_admin(self, room, event): + if not self.is_admin(room, event): + raise CommandRequiresAdmin + + # Throws exception if event sender is not a bot owner + def must_be_owner(self, event): + if not self.is_owner(event): + raise CommandRequiresOwner + + # Returns true if event's sender is admin in the room event was sent in + def is_admin(self, room, event): + print(room.power_levels) + if not event.sender in room.power_levels.users: + return False + return room.power_levels.users[event.sender] >= 50 + + # Returns true if event's sender is owner of the bot + def is_owner(self, event): + return event.sender in self.owners + def save_settings(self): module_settings = dict() for modulename, moduleobject in self.modules.items(): @@ -81,15 +109,14 @@ class Bot: if "matrix_message" in dir(moduleobject): try: await moduleobject.matrix_message(bot, room, event) + except CommandRequiresAdmin: + await self.send_text(room, f'Sorry, you need admin power level in this room to run that command.') + except CommandRequiresOwner: + await self.send_text(room, f'Sorry, only bot owner can run that command.') except: await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') traceback.print_exc(file=sys.stderr) - async def unknown_cb(self, room, event): - if event.msgtype != 'm.location': - return - pass - async def invite_cb(self, room, event): for attempt in range(3): result = await self.client.join(room.room_id) @@ -154,7 +181,7 @@ class Bot: self.client = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER']) self.client.access_token = os.getenv('MATRIX_ACCESS_TOKEN') self.join_on_invite = os.getenv('JOIN_ON_INVITE') - + self.owners = os.environ['BOT_OWNERS'].split(',') self.get_modules() def stop(self): @@ -188,11 +215,10 @@ class Bot: if self.client.logged_in: self.load_settings(self.get_account_data()) self.client.add_event_callback(self.message_cb, RoomMessageText) - self.client.add_event_callback(self.unknown_cb, RoomMessageUnknown) if self.join_on_invite: print('Note: Bot will join rooms if invited') self.client.add_event_callback(self.invite_cb, (InviteEvent,)) - print('Bot running') + print('Bot running as', self.client.user, ', owners', self.owners) await self.client.sync_forever(timeout=30000) else: print('Client was not able to log in, check env variables!') diff --git a/docker-compose.yml b/docker-compose.yml index e476f7c..f459a6a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: - MATRIX_PASSWORD - MATRIX_SERVER - JOIN_ON_INVITE + - BOT_OWNERS volumes: - ${PWD}/credentials.json:/bot/credentials.json - ${PWD}/token.pickle:/bot/token.pickle diff --git a/modules/cron.py b/modules/cron.py index f6917ff..8f0566f 100644 --- a/modules/cron.py +++ b/modules/cron.py @@ -6,6 +6,8 @@ class MatrixModule: last_hour = datetime.now().hour async def matrix_message(self, bot, room, event): + bot.must_be_admin(room, event) + args = shlex.split(event.body) args.pop(0) if len(args) == 3: diff --git a/modules/echo.py b/modules/echo.py index d57c3c0..6143341 100644 --- a/modules/echo.py +++ b/modules/echo.py @@ -1,10 +1,4 @@ class MatrixModule: - def matrix_start(self, bot): - print("Echo started.") - - def matrix_stop(self, bot): - print("Echo stopped") - async def matrix_message(self, bot, room, event): args = event.body.split() args.pop(0) diff --git a/modules/googlecal.py b/modules/googlecal.py index 475f304..a8f72c8 100644 --- a/modules/googlecal.py +++ b/modules/googlecal.py @@ -79,6 +79,8 @@ class MatrixModule: await bot.send_text(room, 'Calendars in this room: ' + str(self.calendar_rooms.get(room.room_id))) elif len(args) == 3: if args[1] == 'add': + bot.must_be_admin(room, event) + calid = args[2] print(f'Adding calendar {calid} to room id {room.room_id}') @@ -97,6 +99,8 @@ class MatrixModule: await bot.send_text(room, 'Added new google calendar to this room') if args[1] == 'del': + bot.must_be_admin(room, event) + calid = args[2] print(f'Removing calendar {calid} from room id {room.room_id}') diff --git a/modules/help.py b/modules/help.py index 4449a95..b55bd86 100644 --- a/modules/help.py +++ b/modules/help.py @@ -1,6 +1,6 @@ class MatrixModule: async def matrix_message(self, bot, room, event): - msg = 'This is Hemppa, a generic Matrix bot. Known commands:\n\n' + msg = f'This is Hemppa {bot.version}, a generic Matrix bot. Known commands:\n\n' for modulename, moduleobject in bot.modules.items(): msg = msg + '!' + modulename diff --git a/modules/teamup.py b/modules/teamup.py index db905fa..50e2fa6 100644 --- a/modules/teamup.py +++ b/modules/teamup.py @@ -36,6 +36,8 @@ class MatrixModule: await self.poll_all_calendars(bot) elif len(args) == 3: if args[1] == 'add': + bot.must_be_admin(room, event) + calid = args[2] print(f'Adding calendar {calid} to room id {room.room_id}') @@ -54,6 +56,8 @@ class MatrixModule: self.setup_calendars() await bot.send_text(room, 'Added new teamup calendar to this room') if args[1] == 'del': + bot.must_be_admin(room, event) + calid = args[2] print(f'Removing calendar {calid} from room id {room.room_id}') @@ -66,6 +70,8 @@ class MatrixModule: self.setup_calendars() await bot.send_text(room, 'Removed teamup calendar from this room') if args[1] == 'apikey': + bot.must_be_owner(event) + self.api_key = args[2] bot.save_settings() self.setup_calendars()