Merge pull request #34 from ancho/feature/enable-disable-modules
enable/disable modules
This commit is contained in:
commit
f8825a0582
|
@ -1,5 +1,6 @@
|
|||
# editors
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# ignore Pipfile.lock
|
||||
Pipfile.lock
|
||||
|
|
168
bot.py
168
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)
|
||||
|
@ -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())
|
||||
|
|
151
modules/bot.py
151
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)'
|
||||
|
|
|
@ -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
|
|
@ -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'
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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>)')
|
||||
|
|
|
@ -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':
|
||||
|
|
|
@ -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>)')
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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:')
|
||||
|
|
|
@ -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)))
|
Loading…
Reference in New Issue