Merge pull request #34 from ancho/feature/enable-disable-modules

enable/disable modules
This commit is contained in:
Ville Ranki 2020-02-07 22:09:40 +02:00 committed by GitHub
commit f8825a0582
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 472 additions and 171 deletions

1
.gitignore vendored
View File

@ -1,5 +1,6 @@
# editors
.vscode
.idea
# ignore Pipfile.lock
Pipfile.lock

168
bot.py
View File

@ -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)
@ -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())

View File

@ -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)'

97
modules/common/module.py Normal file
View File

@ -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

View File

@ -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'

View File

@ -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']

View File

@ -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')

View File

@ -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)} <a href="{event["htmlLink"]}">{event["summary"]}</a>', f'{self.parse_date(start)} {event["summary"]}')
await bot.send_html(room, f'{self.parse_date(start)} <a href="{event["htmlLink"]}">{event["summary"]}</a>',
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']

View File

@ -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'

View File

@ -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'<a href="{media.link}">Instagram {account}:</a> {media.caption}', f'{account}: {media.caption} {media.link}')
await bot.send_html(bot.get_room_by_id(roomid),
f'<a href="{media.link}">Instagram {account}:</a> {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:

View File

@ -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} 🚩 <a href={osm_link}>{location_text}</a>'
@ -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'

View File

@ -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 <icao code>')
def help(self):
return('Metar data access (usage: !metar <icao code>)')
return ('Metar data access (usage: !metar <icao code>)')

View File

@ -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 <icao code>')
def help(self):
return('NOTAM data access (usage: !notam <icao code>) - Currently Finnish airports only')
return ('NOTAM data access (usage: !notam <icao code>) - 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':

View File

@ -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 <icao code>')
def help(self):
return('Taf data access (usage: !taf <icao code>)')
return ('Taf data access (usage: !taf <icao code>)')

View File

@ -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 = []
@ -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'):

View File

@ -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__()
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'<a href="https://twitter.com{tweet.tweet_url}">Twitter {account}</a>: {tweet.text}', f'Twitter {account}: {tweet.text} - https://twitter.com{tweet.tweet_url}')
await bot.send_html(bot.get_room_by_id(roomid),
f'<a href="https://twitter.com{tweet.tweet_url}">Twitter {account}</a>: {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:')

View File

@ -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)))