Version 1.1: access control, commands can require admin or owner status.

This commit is contained in:
Ville Ranki 2019-12-25 20:49:20 +02:00
parent e1c444024f
commit 4017a53e5b
8 changed files with 66 additions and 26 deletions

View File

@ -42,15 +42,15 @@ Howto:
* Create a calendar in Teamup https://teamup.com/ * Create a calendar in Teamup https://teamup.com/
* Get api key at https://teamup.com/api-keys/request * Get api key at https://teamup.com/api-keys/request
* !teamup apikey [your api key] * !teamup apikey [your api key]
* !teamup add [calendar id] * !teamup add [calendar id]
Commands: 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 - list upcoming events in calendar
* !teamup add [calendar id] - add calendar to 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 * !teamup del [calendar id] - delete calendar from this room (Must be done as room admin)
* !teamup list - list calendars in this room * !teamup list - list calendars in this room
* !teamup poll - poll now for changes * !teamup poll - poll now for changes
@ -82,8 +82,8 @@ Commands:
* !googlecal - Show next 10 events in calendar * !googlecal - Show next 10 events in calendar
* !googlecal today - Show today's events * !googlecal today - Show today's events
* !googlecal add [calendar id] - Add new calendar to room * !googlecal add [calendar id] - Add new calendar to room (Must be done as room admin)
* !googlecal del [calendar id] - Delete calendar from room * !googlecal del [calendar id] - Delete calendar from room (Must be done as room admin)
* !googlecal list - List calendars in this room * !googlecal list - List calendars in this room
### Cron ### Cron
@ -92,9 +92,9 @@ Can schedule things to be done.
Commands: 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 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: Examples:
@ -127,7 +127,8 @@ sudo apt install python3-pip
sudo pip3 install pipenv sudo pip3 install pipenv
pipenv shell pipenv shell
pipenv install --pre 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 ## Running with Docker
@ -139,6 +140,7 @@ MATRIX_USER=@user:matrix.org
MATRIX_ACCESS_TOKEN=MDAxOGxvYlotofcharacters53CgYAYFgo MATRIX_ACCESS_TOKEN=MDAxOGxvYlotofcharacters53CgYAYFgo
MATRIX_SERVER=https://matrix.org MATRIX_SERVER=https://matrix.org
JOIN_ON_INVITE=True JOIN_ON_INVITE=True
BOT_OWNERS=@user1:matrix.org,@user2:matrix.org
``` ```
Note: without quotes! 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. 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 ## Module API
Just write a python file with desired command name and place it in modules. See current modules for Just write a python file with desired command name and place it in modules. See current modules for

44
bot.py
View File

@ -13,15 +13,22 @@ import json
import urllib.parse import urllib.parse
from nio import (AsyncClient, RoomMessageText, RoomMessageUnknown, JoinError, InviteEvent) from nio import (AsyncClient, RoomMessageText, RoomMessageUnknown, JoinError, InviteEvent)
# Couple of custom exceptions
class CommandRequiresAdmin(Exception):
pass
class CommandRequiresOwner(Exception):
pass
class Bot: class Bot:
appid = 'org.vranki.hemppa' appid = 'org.vranki.hemppa'
version = '1.0' version = '1.1'
client = None client = None
join_on_invite = False join_on_invite = False
modules = dict() modules = dict()
pollcount = 0 pollcount = 0
poll_task = None poll_task = None
owners = []
async def send_text(self, room, body): async def send_text(self, room, body):
msg = { msg = {
@ -42,6 +49,27 @@ 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]
# 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): def save_settings(self):
module_settings = dict() module_settings = dict()
for modulename, moduleobject in self.modules.items(): for modulename, moduleobject in self.modules.items():
@ -81,15 +109,14 @@ class Bot:
if "matrix_message" in dir(moduleobject): if "matrix_message" in dir(moduleobject):
try: try:
await moduleobject.matrix_message(bot, room, event) 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: except:
await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
traceback.print_exc(file=sys.stderr) 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): async def invite_cb(self, room, event):
for attempt in range(3): for attempt in range(3):
result = await self.client.join(room.room_id) 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 = AsyncClient(os.environ['MATRIX_SERVER'], os.environ['MATRIX_USER'])
self.client.access_token = os.getenv('MATRIX_ACCESS_TOKEN') self.client.access_token = os.getenv('MATRIX_ACCESS_TOKEN')
self.join_on_invite = os.getenv('JOIN_ON_INVITE') self.join_on_invite = os.getenv('JOIN_ON_INVITE')
self.owners = os.environ['BOT_OWNERS'].split(',')
self.get_modules() self.get_modules()
def stop(self): def stop(self):
@ -188,11 +215,10 @@ class Bot:
if self.client.logged_in: if self.client.logged_in:
self.load_settings(self.get_account_data()) self.load_settings(self.get_account_data())
self.client.add_event_callback(self.message_cb, RoomMessageText) self.client.add_event_callback(self.message_cb, RoomMessageText)
self.client.add_event_callback(self.unknown_cb, RoomMessageUnknown)
if self.join_on_invite: if self.join_on_invite:
print('Note: Bot will join rooms if invited') print('Note: Bot will join rooms if invited')
self.client.add_event_callback(self.invite_cb, (InviteEvent,)) 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) await self.client.sync_forever(timeout=30000)
else: else:
print('Client was not able to log in, check env variables!') print('Client was not able to log in, check env variables!')

View File

@ -12,6 +12,7 @@ services:
- MATRIX_PASSWORD - MATRIX_PASSWORD
- MATRIX_SERVER - MATRIX_SERVER
- JOIN_ON_INVITE - JOIN_ON_INVITE
- BOT_OWNERS
volumes: volumes:
- ${PWD}/credentials.json:/bot/credentials.json - ${PWD}/credentials.json:/bot/credentials.json
- ${PWD}/token.pickle:/bot/token.pickle - ${PWD}/token.pickle:/bot/token.pickle

View File

@ -6,6 +6,8 @@ class MatrixModule:
last_hour = datetime.now().hour last_hour = datetime.now().hour
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
bot.must_be_admin(room, event)
args = shlex.split(event.body) args = shlex.split(event.body)
args.pop(0) args.pop(0)
if len(args) == 3: if len(args) == 3:

View File

@ -1,10 +1,4 @@
class MatrixModule: 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): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
args.pop(0) args.pop(0)

View File

@ -79,6 +79,8 @@ class MatrixModule:
await bot.send_text(room, 'Calendars in this room: ' + str(self.calendar_rooms.get(room.room_id))) await bot.send_text(room, 'Calendars in this room: ' + str(self.calendar_rooms.get(room.room_id)))
elif len(args) == 3: elif len(args) == 3:
if args[1] == 'add': if args[1] == 'add':
bot.must_be_admin(room, event)
calid = args[2] calid = args[2]
print(f'Adding calendar {calid} to room id {room.room_id}') 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') await bot.send_text(room, 'Added new google calendar to this room')
if args[1] == 'del': if args[1] == 'del':
bot.must_be_admin(room, event)
calid = args[2] calid = args[2]
print(f'Removing calendar {calid} from room id {room.room_id}') print(f'Removing calendar {calid} from room id {room.room_id}')

View File

@ -1,6 +1,6 @@
class MatrixModule: class MatrixModule:
async def matrix_message(self, bot, room, event): 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(): for modulename, moduleobject in bot.modules.items():
msg = msg + '!' + modulename msg = msg + '!' + modulename

View File

@ -36,6 +36,8 @@ class MatrixModule:
await self.poll_all_calendars(bot) await self.poll_all_calendars(bot)
elif len(args) == 3: elif len(args) == 3:
if args[1] == 'add': if args[1] == 'add':
bot.must_be_admin(room, event)
calid = args[2] calid = args[2]
print(f'Adding calendar {calid} to room id {room.room_id}') print(f'Adding calendar {calid} to room id {room.room_id}')
@ -54,6 +56,8 @@ class MatrixModule:
self.setup_calendars() self.setup_calendars()
await bot.send_text(room, 'Added new teamup calendar to this room') await bot.send_text(room, 'Added new teamup calendar to this room')
if args[1] == 'del': if args[1] == 'del':
bot.must_be_admin(room, event)
calid = args[2] calid = args[2]
print(f'Removing calendar {calid} from room id {room.room_id}') print(f'Removing calendar {calid} from room id {room.room_id}')
@ -66,6 +70,8 @@ class MatrixModule:
self.setup_calendars() self.setup_calendars()
await bot.send_text(room, 'Removed teamup calendar from this room') await bot.send_text(room, 'Removed teamup calendar from this room')
if args[1] == 'apikey': if args[1] == 'apikey':
bot.must_be_owner(event)
self.api_key = args[2] self.api_key = args[2]
bot.save_settings() bot.save_settings()
self.setup_calendars() self.setup_calendars()