diff --git a/.gitignore b/.gitignore
index ef64859..554baea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
# editors
.vscode
+.idea
# ignore Pipfile.lock
Pipfile.lock
diff --git a/bot.py b/bot.py
index fe5204f..c396bf6 100755
--- a/bot.py
+++ b/bot.py
@@ -9,10 +9,12 @@ import re
import sys
import traceback
import urllib.parse
+from importlib import reload
import requests
-from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText
-from importlib import reload
+from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText, MatrixRoom, LogoutResponse, LogoutError, \
+ LoginError
+
# Couple of custom exceptions
@@ -51,6 +53,12 @@ class Bot:
}
await self.client.room_send(room.room_id, 'm.room.message', msg)
+ def remove_callback(self, callback):
+ for cb_object in bot.client.event_callbacks:
+ if cb_object.func == callback:
+ print("remove callback")
+ bot.client.event_callbacks.remove(cb_object)
+
def get_room_by_id(self, room_id):
return self.client.rooms[room_id]
@@ -80,11 +88,10 @@ class Bot:
def save_settings(self):
module_settings = dict()
for modulename, moduleobject in self.modules.items():
- if "get_settings" in dir(moduleobject):
- try:
- module_settings[modulename] = moduleobject.get_settings()
- except Exception:
- traceback.print_exc(file=sys.stderr)
+ try:
+ module_settings[modulename] = moduleobject.get_settings()
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
data = {self.appid: self.version, 'module_settings': module_settings}
self.set_account_data(data)
@@ -95,12 +102,11 @@ class Bot:
return
for modulename, moduleobject in self.modules.items():
if data['module_settings'].get(modulename):
- if "set_settings" in dir(moduleobject):
- try:
- moduleobject.set_settings(
- data['module_settings'][modulename])
- except Exception:
- traceback.print_exc(file=sys.stderr)
+ try:
+ moduleobject.set_settings(
+ data['module_settings'][modulename])
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
async def message_cb(self, room, event):
# Figure out the command
@@ -117,18 +123,27 @@ class Bot:
moduleobject = self.modules.get(command)
- 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 Exception:
- await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
- traceback.print_exc(file=sys.stderr)
+ if moduleobject is not None:
+ if moduleobject.enabled:
+ 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 Exception:
+ await self.send_text(room,
+ f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
+ traceback.print_exc(file=sys.stderr)
+ else:
+ print(f"Unknown command: {command}")
+ await self.send_text(room, f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")
+
async def invite_cb(self, room, event):
+ room: MatrixRoom
+ event: InviteEvent
+
if self.join_on_invite or self.is_owner(event):
for attempt in range(3):
result = await self.client.join(room.room_id)
@@ -137,17 +152,18 @@ class Bot:
attempt, result.message,
)
else:
+ print(f"joining room '{room.display_name}'({room.room_id}) invited by '{event.sender}'")
break
else:
- print(
- f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}')
+ print(f'Received invite event, but not joining as sender is not owner or bot not configured to join on invite. {event}')
def load_module(self, modulename):
try:
+ print("load module: " + modulename)
module = importlib.import_module('modules.' + modulename)
module = reload(module)
cls = getattr(module, 'MatrixModule')
- return cls()
+ return cls(modulename)
except ModuleNotFoundError:
print('Module ', modulename, ' failed to load!')
traceback.print_exc(file=sys.stderr)
@@ -168,7 +184,7 @@ class Bot:
moduleobject = self.load_module(modulename)
if moduleobject:
self.modules[modulename] = moduleobject
-
+
def clear_modules(self):
self.modules = dict()
@@ -176,7 +192,7 @@ class Bot:
while True:
self.pollcount = self.pollcount + 1
for modulename, moduleobject in self.modules.items():
- if "matrix_poll" in dir(moduleobject):
+ if moduleobject.enabled:
try:
await moduleobject.matrix_poll(bot, self.pollcount)
except Exception:
@@ -184,39 +200,69 @@ class Bot:
await asyncio.sleep(10)
def set_account_data(self, data):
- userid = urllib.parse.quote(os.environ['MATRIX_USER'])
+ userid = urllib.parse.quote(self.matrix_user)
ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"
response = requests.put(ad_url, json.dumps(data))
+ self.__handle_error_response(response)
+
if response.status_code != 200:
print('Setting account data failed:', response, response.json())
def get_account_data(self):
- userid = urllib.parse.quote(os.environ['MATRIX_USER'])
+ userid = urllib.parse.quote(self.matrix_user)
ad_url = f"{self.client.homeserver}/_matrix/client/r0/user/{userid}/account_data/{self.appid}?access_token={self.client.access_token}"
response = requests.get(ad_url)
+ self.__handle_error_response(response)
+
if response.status_code == 200:
return response.json()
print(
f'Getting account data failed: {response} {response.json()} - this is normal if you have not saved any settings yet.')
return None
+ def __handle_error_response(self, response):
+ if response.status_code == 401:
+ print("ERROR: access token is invalid or missing")
+ print("NOTE: check MATRIX_ACCESS_TOKEN or set MATRIX_PASSWORD")
+ sys.exit(2)
+
def init(self):
- 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") is not None
- self.owners = os.environ['BOT_OWNERS'].split(',')
- self.get_modules()
+
+ self.matrix_user = os.getenv('MATRIX_USER')
+ self.matrix_pass = os.getenv('MATRIX_PASSWORD')
+ matrix_server = os.getenv('MATRIX_SERVER')
+ bot_owners = os.getenv('BOT_OWNERS')
+ access_token = os.getenv('MATRIX_ACCESS_TOKEN')
+ join_on_invite = os.getenv('JOIN_ON_INVITE')
+
+ if matrix_server and self.matrix_user and bot_owners:
+ self.client = AsyncClient(matrix_server, self.matrix_user)
+ self.client.access_token = access_token
+
+ if self.client.access_token is None:
+ if self.matrix_pass is None:
+ print("Either MATRIX_ACCESS_TOKEN or MATRIX_PASSWORD need to be set")
+ sys.exit(1)
+
+ self.join_on_invite = join_on_invite is not None
+ self.owners = bot_owners.split(',')
+ self.get_modules()
+
+ else:
+ print("The environment variables MATRIX_SERVER, MATRIX_USER and BOT_OWNERS are mandatory")
+ sys.exit(1)
+
def start(self):
- print(f'Starting {len(self.modules)} modules..')
+ self.load_settings(self.get_account_data())
+ enabled_modules = [module for module_name, module in self.modules.items() if module.enabled]
+ print(f'Starting {len(enabled_modules)} modules..')
for modulename, moduleobject in self.modules.items():
- print('Starting', modulename, '..')
- if "matrix_start" in dir(moduleobject):
+ if moduleobject.enabled:
try:
moduleobject.matrix_start(bot)
except Exception:
@@ -225,23 +271,26 @@ class Bot:
def stop(self):
print(f'Stopping {len(self.modules)} modules..')
for modulename, moduleobject in self.modules.items():
- print('Stopping', modulename, '..')
- if "matrix_stop" in dir(moduleobject):
- try:
- moduleobject.matrix_stop(bot)
- except Exception:
- traceback.print_exc(file=sys.stderr)
+ try:
+ moduleobject.matrix_stop(bot)
+ except Exception:
+ traceback.print_exc(file=sys.stderr)
async def run(self):
if not self.client.access_token:
- await self.client.login(os.environ['MATRIX_PASSWORD'])
- print("Logged in with password, access token:",
- self.client.access_token)
+ login_response = await self.client.login(self.matrix_pass)
+
+ if isinstance(login_response, LoginError):
+ print(f"Failed to login: {login_response.message}")
+ return
+
+ last_16 = self.client.access_token[-16:]
+ print(f"Logged in with password, access token: ...{last_16}")
await self.client.sync()
- for roomid in self.client.rooms:
- print(f'Bot is on {roomid} with {len(self.client.rooms[roomid].users)} users')
- if len(self.client.rooms[roomid].users) == 1:
+ for roomid, room in self.client.rooms.items():
+ print(f"Bot is on '{room.display_name}'({roomid}) with {len(room.users)} users")
+ if len(room.users) == 1:
print(f'Room {roomid} has no other users - leaving it.')
print(await self.client.room_leave(roomid))
@@ -262,6 +311,24 @@ class Bot:
else:
print('Client was not able to log in, check env variables!')
+ async def shutdown(self):
+
+ if self.client.logged_in:
+ logout = await self.client.logout()
+
+ if isinstance(logout, LogoutResponse):
+ print("Logout successful")
+ try:
+ await self.client.close()
+ print("Connection closed")
+ except Exception as e:
+ print("error while closing client", e)
+
+ else:
+ logout: LogoutError
+ print(f"Logout unsuccessful. msg: {logout.message}")
+ else:
+ await self.client.client_session.close()
bot = Bot()
bot.init()
@@ -273,3 +340,4 @@ except KeyboardInterrupt:
bot.bot_task.cancel()
bot.stop()
+asyncio.get_event_loop().run_until_complete(bot.shutdown())
diff --git a/modules/bot.py b/modules/bot.py
index f18855e..eaaf5bf 100644
--- a/modules/bot.py
+++ b/modules/bot.py
@@ -1,58 +1,119 @@
-import urllib.request
-from datetime import datetime, timedelta
+from datetime import datetime
+from modules.common.module import BotModule
+
+
+class MatrixModule(BotModule):
+
+ def __init__(self, name):
+ super().__init__(name)
+ self.enable()
-class MatrixModule:
def matrix_start(self, bot):
+ super().matrix_start(bot)
self.starttime = datetime.now()
-
+
async def matrix_message(self, bot, room, event):
args = event.body.split()
if len(args) == 2:
- if args[1]=='quit':
- bot.must_be_admin(room, event)
- await bot.send_text(room, f'Quitting, as requested')
- print(f'{event.sender} commanded bot to quit, so quitting..')
- bot.bot_task.cancel()
- elif args[1]=='version':
- await bot.send_text(room, f'Hemppa version {bot.version} - https://github.com/vranki/hemppa')
- elif args[1]=='reload':
- bot.must_be_admin(room, event)
- await bot.send_text(room, f'Reloading modules..')
- bot.stop()
- bot.reload_modules()
- bot.start()
- elif args[1]=='status':
- uptime = datetime.now() - self.starttime
- await bot.send_text(room, f'Uptime {uptime} - system time is {datetime.now()} - loaded {len(bot.modules)} modules.')
- elif args[1]=='stats':
- roomcount = len(bot.client.rooms)
- usercount = 0
- homeservers = dict()
+ if args[1] == 'quit':
+ await self.quit(bot, room, event)
+ elif args[1] == 'version':
+ await self.version(bot, room)
+ elif args[1] == 'reload':
+ await self.reload(bot, room, event)
+ elif args[1] == 'status':
+ await self.status(bot, room)
+ elif args[1] == 'stats':
+ await self.stats(bot, room)
+ elif args[1] == 'leave':
+ await self.leave(bot, room, event)
+ elif args[1] == 'modules':
+ await self.show_modules(bot, room)
- for croomid in bot.client.rooms:
- roomobj = bot.client.rooms[croomid]
- usercount = usercount + len(roomobj.users)
- for user in roomobj.users:
- hs = user.split(':')[1]
- if homeservers.get(hs):
- homeservers[hs] = homeservers[hs] + 1
- else:
- homeservers[hs] = 1
-
- homeservers = sorted(homeservers.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
-
- if len(homeservers) > 10:
- homeservers = homeservers[0:10]
-
- await bot.send_text(room, f'I\'m seeing {usercount} users in {roomcount} rooms. Top ten homeservers: {homeservers}')
- elif args[1]=='leave':
- bot.must_be_admin(room, event)
- print(f'{event.sender} asked bot to leave room {room.room_id}')
- await bot.send_text(room, f'By your command.')
- await bot.client.room_leave(room.room_id)
+ elif len(args) == 3:
+ if args[1] == 'enable':
+ await self.enable_module(bot, room, event, args[2])
+ elif args[1] == 'disable':
+ await self.disable_module(bot, room, event, args[2])
else:
await bot.send_text(room, 'Unknown command, sorry.')
+ async def leave(self, bot, room, event):
+ bot.must_be_admin(room, event)
+ print(f'{event.sender} asked bot to leave room {room.room_id}')
+ await bot.send_text(room, f'By your command.')
+ await bot.client.room_leave(room.room_id)
+
+ async def stats(self, bot, room):
+ roomcount = len(bot.client.rooms)
+ usercount = 0
+ homeservers = dict()
+ for croomid in bot.client.rooms:
+ roomobj = bot.client.rooms[croomid]
+ usercount = usercount + len(roomobj.users)
+ for user in roomobj.users:
+ hs = user.split(':')[1]
+ if homeservers.get(hs):
+ homeservers[hs] = homeservers[hs] + 1
+ else:
+ homeservers[hs] = 1
+ homeservers = sorted(homeservers.items(), key=lambda kv: (kv[1], kv[0]), reverse=True)
+ if len(homeservers) > 10:
+ homeservers = homeservers[0:10]
+ await bot.send_text(room,
+ f'I\'m seeing {usercount} users in {roomcount} rooms. Top ten homeservers: {homeservers}')
+
+ async def status(self, bot, room):
+ uptime = datetime.now() - self.starttime
+ await bot.send_text(room,
+ f'Uptime {uptime} - system time is {datetime.now()} - loaded {len(bot.modules)} modules.')
+
+ async def reload(self, bot, room, event):
+ bot.must_be_admin(room, event)
+ await bot.send_text(room, f'Reloading modules..')
+ bot.stop()
+ bot.reload_modules()
+ bot.start()
+
+ async def version(self, bot, room):
+ await bot.send_text(room, f'Hemppa version {bot.version} - https://github.com/vranki/hemppa')
+
+ async def quit(self, bot, room, event):
+ bot.must_be_admin(room, event)
+ await bot.send_text(room, f'Quitting, as requested')
+ print(f'{event.sender} commanded bot to quit, so quitting..')
+ bot.bot_task.cancel()
+
+ async def enable_module(self, bot, room, event, module_name):
+ bot.must_be_admin(room, event)
+ print(f"asked to enable {module_name}")
+ if bot.modules.get(module_name):
+ module = bot.modules.get(module_name)
+ module.enable()
+ module.matrix_start(bot)
+ bot.save_settings()
+ await bot.send_text(room, f"module {module_name} enabled")
+ else:
+ await bot.send_text(room, f"module with name {module_name} not found. execute !bot modules for a list of available modules")
+
+ async def disable_module(self, bot, room, event, module_name):
+ bot.must_be_admin(room, event)
+ print(f"asked to disable {module_name}")
+ if bot.modules.get(module_name):
+ module = bot.modules.get(module_name)
+ module.disable()
+ module.matrix_stop(bot)
+ bot.save_settings()
+ await bot.send_text(room, f"module {module_name} disabled")
+ else:
+ await bot.send_text(room, f"module with name {module_name} not found. execute !bot modules for a list of available modules")
+
+
+ async def show_modules(self, bot, room):
+ await bot.send_text(room, "Modules:\n")
+ for modulename, module in bot.modules.items():
+ await bot.send_text(room, f"Name: {modulename:20s} Enabled: {module.enabled}")
+
def help(self):
- return('Bot management commands')
+ return 'Bot management commands. (quit, version, reload, status, stats, leave, modules, enable, disable)'
diff --git a/modules/common/module.py b/modules/common/module.py
new file mode 100644
index 0000000..db39f2e
--- /dev/null
+++ b/modules/common/module.py
@@ -0,0 +1,97 @@
+from abc import ABC, abstractmethod
+from nio import RoomMessageText, MatrixRoom
+
+
+class BotModule(ABC):
+ """Abtract bot module
+
+ A module derives from this class to process and interact on room messages. The subcluss must be named `MatrixModule`.
+ Just write a python file with desired command name and place it in modules. See current modules for examples.
+
+ No need to register it anywhere else.
+
+ Example:
+
+ class MatrixModule(BotModule):
+ async def matrix_message(self, bot, room, event):
+ args = event.body.split()
+ args.pop(0)
+
+ # Echo what they said back
+ await bot.send_text(room, ' '.join(args))
+
+ def help(self):
+ return 'Echoes back what user has said'
+
+ """
+
+ def __init__(self, name):
+ self.enabled = False
+ self.name = name
+
+ def matrix_start(self, bot):
+ """Called once on startup
+
+ :param bot: a reference to the bot
+ :type bot: Bot
+ """
+ print('Starting', self.name, '..')
+
+ @abstractmethod
+ async def matrix_message(self, bot, room, event):
+ """Called when a message is sent to room starting with !module_name
+
+ :param bot: a reference to the bot
+ :type bot: Bot
+ :param room: a matrix room message
+ :type room: MatrixRoom
+ :param event: a handle to the event that triggered the callback
+ :type event: RoomMessageText
+ """
+ pass
+
+ def matrix_stop(self, bot):
+ """Called once before exit
+
+ :param bot: a reference to the bot
+ :type bot: Bot
+ """
+ print('Stopping', self.name, '..')
+
+ async def matrix_poll(self, bot, pollcount):
+ """Called every 10 seconds
+
+ :param bot: a reference to the bot
+ :type bot: Bot
+ :param pollcount: the actual poll count
+ :type pollcount: int
+ """
+ pass
+
+ @abstractmethod
+ def help(self):
+ """Return one-liner help text"""
+ pass
+
+ def get_settings(self):
+ """Must return a dict object that can be converted to JSON and sent to server
+
+ :return: a dict object that can be converted to JSON
+ :rtype: dict
+ """
+ return {'enabled': self.enabled}
+
+ def set_settings(self, data):
+ """Load these settings. It should be the same JSON you returned in previous get_settings
+
+ :param data: a dict object containing the settings read from the account
+ :type data: dict
+ """
+ if data.get('enabled'):
+ self.enabled = data['enabled']
+
+ def enable(self):
+ self.enabled = True
+
+ def disable(self):
+ self.enabled = False
\ No newline at end of file
diff --git a/modules/common/pollingservice.py b/modules/common/pollingservice.py
index a1ff14f..bedac51 100644
--- a/modules/common/pollingservice.py
+++ b/modules/common/pollingservice.py
@@ -1,15 +1,17 @@
-import traceback
-import sys
from datetime import datetime, timedelta
from random import randrange
-class PollingService:
- def __init__(self):
+from modules.common.module import BotModule
+
+
+class PollingService(BotModule):
+ def __init__(self, name):
+ super().__init__(name)
self.known_ids = set()
self.account_rooms = dict() # Roomid -> [account, account..]
self.next_poll_time = dict() # Roomid -> datetime, None = not polled yet
self.service_name = "Service"
- self.poll_interval_min = 30 # TODO: Configurable
+ self.poll_interval_min = 30 # TODO: Configurable
self.poll_interval_random = 30
async def matrix_poll(self, bot, pollcount):
@@ -47,16 +49,17 @@ class PollingService:
await self.poll_implementation(bot, account, roomid, send_messages)
-
async def matrix_message(self, bot, room, event):
args = event.body.split()
if len(args) == 2:
if args[1] == 'list':
- await bot.send_text(room, f'{self.service_name} accounts in this room: {self.account_rooms.get(room.room_id) or []}')
+ await bot.send_text(room,
+ f'{self.service_name} accounts in this room: {self.account_rooms.get(room.room_id) or []}')
elif args[1] == 'debug':
- await bot.send_text(room, f"{self.service_name} accounts: {self.account_rooms.get(room.room_id) or []} - known ids: {self.known_ids}\n" \
- f"Next poll in this room at {self.next_poll_time.get(room.room_id)} - in {self.next_poll_time.get(room.room_id) - datetime.now()}")
+ await bot.send_text(room,
+ f"{self.service_name} accounts: {self.account_rooms.get(room.room_id) or []} - known ids: {self.known_ids}\n" \
+ f"Next poll in this room at {self.next_poll_time.get(room.room_id)} - in {self.next_poll_time.get(room.room_id) - datetime.now()}")
elif args[1] == 'poll':
bot.must_be_owner(event)
print(f'{self.service_name} force polling requested by {event.sender}')
@@ -111,4 +114,4 @@ class PollingService:
self.account_rooms = data['account_rooms']
def help(self):
- return(f'{self.service_name} polling')
+ return f'{self.service_name} polling'
diff --git a/modules/cron.py b/modules/cron.py
index a823096..24e8c16 100644
--- a/modules/cron.py
+++ b/modules/cron.py
@@ -1,8 +1,9 @@
import shlex
from datetime import datetime
+from .common.module import BotModule
-class MatrixModule:
+class MatrixModule(BotModule):
daily_commands = dict() # room_id -> command json
last_hour = datetime.now().hour
@@ -30,12 +31,15 @@ class MatrixModule:
await bot.send_text(room, 'Cleared commands on this room.')
def help(self):
- return('Runs scheduled commands')
+ return ('Runs scheduled commands')
def get_settings(self):
- return {'daily_commands': self.daily_commands}
+ data = super().get_settings()
+ data['daily_commands'] = self.daily_commands
+ return data
def set_settings(self, data):
+ super().set_settings(data)
if data.get('daily_commands'):
self.daily_commands = data['daily_commands']
@@ -54,4 +58,4 @@ class MatrixModule:
delete_rooms.append(room_id)
for roomid in delete_rooms:
- self.daily_commands.pop(roomid, None)
\ No newline at end of file
+ self.daily_commands.pop(roomid, None)
diff --git a/modules/echo.py b/modules/echo.py
index 6143341..0d73581 100644
--- a/modules/echo.py
+++ b/modules/echo.py
@@ -1,4 +1,7 @@
-class MatrixModule:
+from modules.common.module import BotModule
+
+
+class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event):
args = event.body.split()
args.pop(0)
@@ -7,4 +10,4 @@ class MatrixModule:
await bot.send_text(room, ' '.join(args))
def help(self):
- return('Echoes back what user has said')
+ return ('Echoes back what user has said')
diff --git a/modules/googlecal.py b/modules/googlecal.py
index a089fdc..ccfb6c1 100644
--- a/modules/googlecal.py
+++ b/modules/googlecal.py
@@ -9,6 +9,7 @@ from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
+
#
# Google calendar notifications
#
@@ -16,16 +17,21 @@ from googleapiclient.discovery import build
# It's created on first run (run from console!) and
# can be copied to another computer.
#
+from modules.common.module import BotModule
-class MatrixModule:
- def matrix_start(self, bot):
- self.bot = bot
- self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
+class MatrixModule(BotModule):
+ def __init__(self, name):
+ super().__init__(name)
self.credentials_file = "credentials.json"
+ self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
+ self.bot = None
self.service = None
self.calendar_rooms = dict() # Contains room_id -> [calid, calid] ..
+ def matrix_start(self, bot):
+ super().matrix_start(bot)
+ self.bot = bot
creds = None
if not os.path.exists(self.credentials_file) or os.path.getsize(self.credentials_file) == 0:
@@ -41,8 +47,7 @@ class MatrixModule:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
- flow = InstalledAppFlow.from_client_secrets_file(
- self.credentials_file, self.SCOPES)
+ flow = InstalledAppFlow.from_client_secrets_file(self.credentials_file, self.SCOPES)
# urn:ietf:wg:oauth:2.0:oob
creds = flow.run_local_server(port=0)
# Save the credentials for the next run
@@ -135,7 +140,8 @@ class MatrixModule:
async def send_events(self, bot, events, room):
for event in events:
start = event['start'].get('dateTime', event['start'].get('date'))
- await bot.send_html(room, f'{self.parse_date(start)} {event["summary"]}', f'{self.parse_date(start)} {event["summary"]}')
+ await bot.send_html(room, f'{self.parse_date(start)} {event["summary"]}',
+ f'{self.parse_date(start)} {event["summary"]}')
def list_upcoming(self, calid):
startTime = datetime.utcnow()
@@ -160,12 +166,15 @@ class MatrixModule:
return events_result.get('items', [])
def help(self):
- return('Google calendar. Lists 10 next events by default. today = list today\'s events.')
+ return ('Google calendar. Lists 10 next events by default. today = list today\'s events.')
def get_settings(self):
- return {'calendar_rooms': self.calendar_rooms}
+ data = super().get_settings()
+ data['calendar_rooms'] = self.calendar_rooms
+ return data
def set_settings(self, data):
+ super().set_settings(data)
if data.get('calendar_rooms'):
self.calendar_rooms = data['calendar_rooms']
diff --git a/modules/help.py b/modules/help.py
index b55bd86..2914165 100644
--- a/modules/help.py
+++ b/modules/help.py
@@ -1,16 +1,25 @@
-class MatrixModule:
+from modules.common.module import BotModule
+
+
+class MatrixModule(BotModule):
+
+ def __init__(self, name):
+ super().__init__(name)
+ self.enable()
+
async def matrix_message(self, bot, room, event):
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
- try:
- msg = msg + ' - ' + moduleobject.help() + '\n'
- except AttributeError:
- pass
- msg + msg + '\n'
+ if moduleobject.enabled:
+ msg = msg + '!' + modulename
+ try:
+ msg = msg + ' - ' + moduleobject.help() + '\n'
+ except AttributeError:
+ pass
+ msg = msg + '\n'
msg = msg + "\nAdd your own commands at https://github.com/vranki/hemppa"
await bot.send_text(room, msg)
def help(self):
- return('Prints help on commands')
+ return 'Prints help on commands'
diff --git a/modules/ig.py b/modules/ig.py
index 4609161..7ec3e46 100644
--- a/modules/ig.py
+++ b/modules/ig.py
@@ -1,16 +1,18 @@
-import traceback
import sys
+import traceback
from datetime import datetime, timedelta
from random import randrange
-from modules.common.pollingservice import PollingService
from igramscraper.exception.instagram_not_found_exception import \
InstagramNotFoundException
from igramscraper.instagram import Instagram
+from modules.common.pollingservice import PollingService
+
+
class MatrixModule(PollingService):
- def __init__(self):
- super().__init__()
+ def __init__(self, name):
+ super().__init__(name)
self.instagram = Instagram()
self.service_name = 'Instagram'
@@ -21,12 +23,14 @@ class MatrixModule(PollingService):
for media in medias:
if send_messages:
if media.identifier not in self.known_ids:
- await bot.send_html(bot.get_room_by_id(roomid), f'Instagram {account}: {media.caption}', f'{account}: {media.caption} {media.link}')
+ await bot.send_html(bot.get_room_by_id(roomid),
+ f'Instagram {account}: {media.caption}',
+ f'{account}: {media.caption} {media.link}')
self.known_ids.add(media.identifier)
except InstagramNotFoundException:
print('ig error: there is ', account,
- ' account that does not exist - deleting from room')
+ ' account that does not exist - deleting from room')
self.account_rooms[roomid].remove(account)
bot.save_settings()
except Exception:
diff --git a/modules/loc.py b/modules/loc.py
index fdecfb6..8f0c475 100644
--- a/modules/loc.py
+++ b/modules/loc.py
@@ -1,14 +1,20 @@
from geopy.geocoders import Nominatim
-from nio import RoomMessageUnknown
+from nio import RoomMessageUnknown, AsyncClient
+from modules.common.module import BotModule
-class MatrixModule:
+class MatrixModule(BotModule):
bot = None
def matrix_start(self, bot):
+ super().matrix_start(bot)
self.bot = bot
bot.client.add_event_callback(self.unknown_cb, RoomMessageUnknown)
+ def matrix_stop(self, bot):
+ super().matrix_stop(bot)
+ bot.remove_callback(self.unknown_cb)
+
async def unknown_cb(self, room, event):
if event.msgtype != 'm.location':
return
@@ -29,7 +35,7 @@ class MatrixModule:
float(latlon[1])
osm_link = 'https://www.openstreetmap.org/?mlat=' + \
- latlon[0] + "&mlon=" + latlon[1]
+ latlon[0] + "&mlon=" + latlon[1]
plain = sender + ' 🚩 ' + osm_link
html = f'{sender} 🚩 {location_text}'
@@ -58,4 +64,4 @@ class MatrixModule:
await bot.send_text(room, "Can't find " + query + " on map!")
def help(self):
- return('Search for locations and display Matrix location events as OSM links')
+ return 'Search for locations and display Matrix location events as OSM links'
diff --git a/modules/metar.py b/modules/metar.py
index ad94d15..44ffb18 100644
--- a/modules/metar.py
+++ b/modules/metar.py
@@ -1,13 +1,15 @@
import urllib.request
+from modules.common.module import BotModule
-class MatrixModule:
+
+class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event):
args = event.body.split()
if len(args) == 2:
icao = args[1]
metar_url = "https://tgftp.nws.noaa.gov/data/observations/metar/stations/" + \
- icao.upper() + ".TXT"
+ icao.upper() + ".TXT"
response = urllib.request.urlopen(metar_url)
lines = response.readlines()
await bot.send_text(room, lines[1].decode("utf-8").strip())
@@ -15,4 +17,4 @@ class MatrixModule:
await bot.send_text(room, 'Usage: !metar ')
def help(self):
- return('Metar data access (usage: !metar )')
+ return ('Metar data access (usage: !metar )')
diff --git a/modules/notam.py b/modules/notam.py
index 6e005d4..1c5012c 100644
--- a/modules/notam.py
+++ b/modules/notam.py
@@ -1,8 +1,10 @@
-import urllib.request
import re
+import urllib.request
+
+from modules.common.module import BotModule
-class MatrixModule:
+class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event):
args = event.body.split()
if len(args) == 2 and len(args[1]) == 4:
@@ -13,12 +15,12 @@ class MatrixModule:
await bot.send_text(room, 'Usage: !notam ')
def help(self):
- return('NOTAM data access (usage: !notam ) - Currently Finnish airports only')
+ return ('NOTAM data access (usage: !notam ) - Currently Finnish airports only')
-# TODO: This handles only finnish airports. Implement support for other countries.
+ # TODO: This handles only finnish airports. Implement support for other countries.
def get_notam(self, icao):
if not icao.startswith('EF'):
- return('Only Finnish airports supported currently, sorry.')
+ return ('Only Finnish airports supported currently, sorry.')
icao_first_letter = icao[2]
if icao_first_letter < 'M':
diff --git a/modules/taf.py b/modules/taf.py
index 5704f77..dfe018a 100644
--- a/modules/taf.py
+++ b/modules/taf.py
@@ -1,7 +1,9 @@
import urllib.request
+from modules.common.module import BotModule
-class MatrixModule:
+
+class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event):
args = event.body.split()
if len(args) == 2:
@@ -18,4 +20,4 @@ class MatrixModule:
await bot.send_text(room, 'Usage: !taf ')
def help(self):
- return('Taf data access (usage: !taf )')
+ return ('Taf data access (usage: !taf )')
diff --git a/modules/teamup.py b/modules/teamup.py
index 5ae4de1..c2bc421 100644
--- a/modules/teamup.py
+++ b/modules/teamup.py
@@ -3,12 +3,14 @@ from datetime import datetime
from pyteamup import Calendar
+
#
# TeamUp calendar notifications
#
+from modules.common.module import BotModule
-class MatrixModule:
+class MatrixModule(BotModule):
api_key = None
calendar_rooms = dict() # Roomid -> [calid, calid..]
calendars = dict() # calid -> Calendar
@@ -87,7 +89,7 @@ class MatrixModule:
await bot.send_text(room, 'Api key set')
def help(self):
- return('Polls teamup calendar.')
+ return ('Polls teamup calendar.')
async def poll_all_calendars(self, bot):
delete_rooms = []
@@ -102,7 +104,7 @@ class MatrixModule:
await bot.send_text(bot.get_room_by_id(roomid), 'Calendar: ' + self.eventToString(event))
else:
delete_rooms.append(roomid)
-
+
for roomid in delete_rooms:
self.calendar_rooms.pop(roomid, None)
@@ -115,7 +117,7 @@ class MatrixModule:
return datetime.strptime(dts, '%Y-%m-%dT%H:%M:%S')
except ValueError:
pos = len(dts) - 3
- dts = dts[:pos] + dts[pos+1:]
+ dts = dts[:pos] + dts[pos + 1:]
return datetime.strptime(dts, '%Y-%m-%dT%H:%M:%S%z')
def eventToString(self, event):
@@ -123,7 +125,7 @@ class MatrixModule:
if len(event['title']) == 0:
event['title'] = '(empty name)'
- if(event['delete_dt']):
+ if (event['delete_dt']):
s = event['title'] + ' deleted.'
else:
s = event['title'] + " " + (event['notes'] or '') + \
@@ -144,9 +146,13 @@ class MatrixModule:
self.calendars[calid].timestamp = int(time.time())
def get_settings(self):
- return {'apikey': self.api_key or '', 'calendar_rooms': self.calendar_rooms}
+ data = super().get_settings()
+ data['apikey'] = self.api_key
+ data['calendar_rooms'] = self.calendar_rooms
+ return data
def set_settings(self, data):
+ super().set_settings(data)
if data.get('calendar_rooms'):
self.calendar_rooms = data['calendar_rooms']
if data.get('apikey'):
diff --git a/modules/twitter.py b/modules/twitter.py
index d302d6f..0582ea7 100644
--- a/modules/twitter.py
+++ b/modules/twitter.py
@@ -1,11 +1,16 @@
+import sys
+import traceback
+
from twitterscraper import query_tweets_from_user
+
from modules.common.pollingservice import PollingService
+
# https://github.com/taspinar/twitterscraper/tree/master/twitterscraper
-class MatrixModule(PollingService):
- def __init__(self):
- super().__init__()
+class MatrixModule(PollingService):
+ def __init__(self, name):
+ super().__init__(name)
self.service_name = 'Twitter'
async def poll_implementation(self, bot, account, roomid, send_messages):
@@ -15,7 +20,9 @@ class MatrixModule(PollingService):
for tweet in tweets:
if tweet.tweet_id not in self.known_ids:
if send_messages:
- await bot.send_html(bot.get_room_by_id(roomid), f'Twitter {account}: {tweet.text}', f'Twitter {account}: {tweet.text} - https://twitter.com{tweet.tweet_url}')
+ await bot.send_html(bot.get_room_by_id(roomid),
+ f'Twitter {account}: {tweet.text}',
+ f'Twitter {account}: {tweet.text} - https://twitter.com{tweet.tweet_url}')
self.known_ids.add(tweet.tweet_id)
except Exception:
print('Polling twitter account failed:')
diff --git a/modules/url.py b/modules/url.py
index 9f37144..3536d99 100644
--- a/modules/url.py
+++ b/modules/url.py
@@ -4,33 +4,43 @@ from functools import lru_cache
import httpx
from bs4 import BeautifulSoup
-from nio import RoomMessageText
+from nio import RoomMessageText, AsyncClient
+
+from modules.common.module import BotModule
-class MatrixModule:
+class MatrixModule(BotModule):
"""
Simple url fetch and spit out title module.
Everytime a url is seen in a message we do http request to it and try to get a title tag contents to spit out to the room.
"""
- bot = None
- status = dict() # room_id -> what to do with urls
+ def __init__(self, name):
+ super().__init__(name)
- STATUSES = {
- "OFF": "Not spamming this channel",
- "TITLE": "Spamming this channel with titles",
- "DESCRIPTION": "Spamming this channel with descriptions",
- "BOTH": "Spamming this channel with both title and description",
- }
+ self.bot = None
+ self.status = dict() # room_id -> what to do with urls
+
+ self.STATUSES = {
+ "OFF": "Not spamming this channel",
+ "TITLE": "Spamming this channel with titles",
+ "DESCRIPTION": "Spamming this channel with descriptions",
+ "BOTH": "Spamming this channel with both title and description",
+ }
def matrix_start(self, bot):
"""
Register callback for all RoomMessageText events on startup
"""
+ super().matrix_start(bot)
self.bot = bot
bot.client.add_event_callback(self.text_cb, RoomMessageText)
+ def matrix_stop(self, bot):
+ super().matrix_stop(bot)
+ bot.remove_callback(self.text_cb)
+
async def text_cb(self, room, event):
"""
Handle client callbacks for all room text events
@@ -144,11 +154,18 @@ class MatrixModule:
return
def get_settings(self):
- return {"status": self.status}
+ data = super().get_settings()
+ data['status'] = self.status
+ return data
def set_settings(self, data):
+ super().set_settings(data)
if data.get("status"):
self.status = data["status"]
def help(self):
return "If I see a url in a message I will try to get the title from the page and spit it out"
+
+ def dump(self, obj):
+ for attr in dir(obj):
+ print("obj.%s = %r" % (attr, getattr(obj, attr)))
\ No newline at end of file