Merge branch 'master' into 'master'

extract BotModule class

See merge request cfdisk/hemppa!1
This commit is contained in:
plocki 2020-02-03 19:17:07 +00:00
commit f42e759b4a
20 changed files with 226 additions and 99 deletions

1
.gitignore vendored
View File

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

7
bot.py
View File

@ -9,10 +9,11 @@ import re
import sys import sys
import traceback import traceback
import urllib.parse import urllib.parse
from importlib import reload
import requests import requests
from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText from nio import AsyncClient, InviteEvent, JoinError, RoomMessageText
from importlib import reload
# Couple of custom exceptions # Couple of custom exceptions
@ -125,7 +126,8 @@ class Bot:
except CommandRequiresOwner: except CommandRequiresOwner:
await self.send_text(room, f'Sorry, only bot owner can run that command.') await self.send_text(room, f'Sorry, only bot owner can run that command.')
except Exception: except Exception:
await self.send_text(room, f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details') await self.send_text(room,
f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
traceback.print_exc(file=sys.stderr) traceback.print_exc(file=sys.stderr)
async def invite_cb(self, room, event): async def invite_cb(self, room, event):
@ -144,6 +146,7 @@ class Bot:
def load_module(self, modulename): def load_module(self, modulename):
try: try:
print("load module: " + modulename)
module = importlib.import_module('modules.' + modulename) module = importlib.import_module('modules.' + modulename)
module = reload(module) module = reload(module)
cls = getattr(module, 'MatrixModule') cls = getattr(module, 'MatrixModule')

View File

@ -1,7 +1,9 @@
import urllib.request from datetime import datetime
from datetime import datetime, timedelta from modules.common.module import BotModule
class MatrixModule(BotModule):
class MatrixModule:
def matrix_start(self, bot): def matrix_start(self, bot):
self.starttime = datetime.now() self.starttime = datetime.now()
@ -23,7 +25,8 @@ class MatrixModule:
bot.start() bot.start()
elif args[1] == 'status': elif args[1] == 'status':
uptime = datetime.now() - self.starttime uptime = datetime.now() - self.starttime
await bot.send_text(room, f'Uptime {uptime} - system time is {datetime.now()} - loaded {len(bot.modules)} modules.') await bot.send_text(room,
f'Uptime {uptime} - system time is {datetime.now()} - loaded {len(bot.modules)} modules.')
elif args[1] == 'stats': elif args[1] == 'stats':
roomcount = len(bot.client.rooms) roomcount = len(bot.client.rooms)
usercount = 0 usercount = 0
@ -44,7 +47,8 @@ class MatrixModule:
if len(homeservers) > 10: if len(homeservers) > 10:
homeservers = homeservers[0:10] homeservers = homeservers[0:10]
await bot.send_text(room, f'I\'m seeing {usercount} users in {roomcount} rooms. Top ten homeservers: {homeservers}') await bot.send_text(room,
f'I\'m seeing {usercount} users in {roomcount} rooms. Top ten homeservers: {homeservers}')
elif args[1] == 'leave': elif args[1] == 'leave':
bot.must_be_admin(room, event) bot.must_be_admin(room, event)
print(f'{event.sender} asked bot to leave room {room.room_id}') print(f'{event.sender} asked bot to leave room {room.room_id}')
@ -55,4 +59,4 @@ class MatrixModule:
await bot.send_text(room, 'Unknown command, sorry.') await bot.send_text(room, 'Unknown command, sorry.')
def help(self): def help(self):
return('Bot management commands') return 'Bot management commands'

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

@ -0,0 +1,86 @@
from abc import ABC, abstractmethod
from nio import RoomMessageText, Event
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 matrix_start(self, bot):
"""Called once on startup
:param bot: a reference to the bot
:type bot: Bot
"""
pass
@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: RoomMessageText
:param event: a handle to the event that triggered the callback
:type event: Event
"""
pass
def matrix_stop(self, bot):
"""Called once before exit
:param bot: a reference to the bot
:type bot: Bot
"""
pass
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
"""
pass
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
"""
pass

View File

@ -1,8 +1,7 @@
import traceback
import sys
from datetime import datetime, timedelta from datetime import datetime, timedelta
from random import randrange from random import randrange
class PollingService: class PollingService:
def __init__(self): def __init__(self):
self.known_ids = set() self.known_ids = set()
@ -47,15 +46,16 @@ class PollingService:
await self.poll_implementation(bot, account, roomid, send_messages) await self.poll_implementation(bot, account, roomid, send_messages)
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
if len(args) == 2: if len(args) == 2:
if args[1] == 'list': 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': 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" \ 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()}") 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': elif args[1] == 'poll':
bot.must_be_owner(event) bot.must_be_owner(event)
@ -111,4 +111,4 @@ class PollingService:
self.account_rooms = data['account_rooms'] self.account_rooms = data['account_rooms']
def help(self): def help(self):
return(f'{self.service_name} polling') return f'{self.service_name} polling'

View File

@ -1,8 +1,9 @@
import shlex import shlex
from datetime import datetime from datetime import datetime
from .common.module import BotModule
class MatrixModule: class MatrixModule(BotModule):
daily_commands = dict() # room_id -> command json daily_commands = dict() # room_id -> command json
last_hour = datetime.now().hour last_hour = datetime.now().hour

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): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
args.pop(0) args.pop(0)

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): async def matrix_message(self, bot, room, event):
msg = f'This is Hemppa {bot.version}, a generic Matrix bot. Known commands:\n\n' msg = f'This is Hemppa {bot.version}, a generic Matrix bot. Known commands:\n\n'
@ -13,4 +16,4 @@ class MatrixModule:
await bot.send_text(room, msg) await bot.send_text(room, msg)
def help(self): def help(self):
return('Prints help on commands') return 'Prints help on commands'

View File

@ -1,8 +1,9 @@
from geopy.geocoders import Nominatim from geopy.geocoders import Nominatim
from nio import RoomMessageUnknown from nio import RoomMessageUnknown
from modules.common.module import BotModule
class MatrixModule: class MatrixModule(BotModule):
bot = None bot = None
def matrix_start(self, bot): def matrix_start(self, bot):
@ -58,4 +59,4 @@ class MatrixModule:
await bot.send_text(room, "Can't find " + query + " on map!") await bot.send_text(room, "Can't find " + query + " on map!")
def help(self): 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

@ -9,6 +9,7 @@ from google.auth.transport.requests import Request
from google_auth_oauthlib.flow import InstalledAppFlow from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build from googleapiclient.discovery import build
# #
# Google calendar notifications # Google calendar notifications
# #
@ -16,9 +17,10 @@ from googleapiclient.discovery import build
# It's created on first run (run from console!) and # It's created on first run (run from console!) and
# can be copied to another computer. # can be copied to another computer.
# #
from modules.common.module import BotModule
class MatrixModule: class MatrixModule(BotModule):
def matrix_start(self, bot): def matrix_start(self, bot):
self.bot = bot self.bot = bot
self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly'] self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
@ -135,7 +137,8 @@ class MatrixModule:
async def send_events(self, bot, events, room): async def send_events(self, bot, events, room):
for event in events: for event in events:
start = event['start'].get('dateTime', event['start'].get('date')) 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): def list_upcoming(self, calid):
startTime = datetime.utcnow() startTime = datetime.utcnow()

View File

@ -1,13 +1,15 @@
import traceback
import sys import sys
import traceback
from datetime import datetime, timedelta from datetime import datetime, timedelta
from random import randrange from random import randrange
from modules.common.pollingservice import PollingService
from igramscraper.exception.instagram_not_found_exception import \ from igramscraper.exception.instagram_not_found_exception import \
InstagramNotFoundException InstagramNotFoundException
from igramscraper.instagram import Instagram from igramscraper.instagram import Instagram
from modules.common.pollingservice import PollingService
class MatrixModule(PollingService): class MatrixModule(PollingService):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -21,7 +23,9 @@ class MatrixModule(PollingService):
for media in medias: for media in medias:
if send_messages: if send_messages:
if media.identifier not in self.known_ids: 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) self.known_ids.add(media.identifier)
except InstagramNotFoundException: except InstagramNotFoundException:

View File

@ -1,7 +1,9 @@
import urllib.request import urllib.request
from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
if len(args) == 2: if len(args) == 2:

View File

@ -1,8 +1,10 @@
import urllib.request
import re import re
import urllib.request
from modules.common.module import BotModule
class MatrixModule: class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
if len(args) == 2 and len(args[1]) == 4: if len(args) == 2 and len(args[1]) == 4:

View File

@ -1,7 +1,9 @@
import urllib.request import urllib.request
from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
if len(args) == 2: if len(args) == 2:

View File

@ -3,12 +3,14 @@ from datetime import datetime
from pyteamup import Calendar from pyteamup import Calendar
# #
# TeamUp calendar notifications # TeamUp calendar notifications
# #
from modules.common.module import BotModule
class MatrixModule: class MatrixModule(BotModule):
api_key = None api_key = None
calendar_rooms = dict() # Roomid -> [calid, calid..] calendar_rooms = dict() # Roomid -> [calid, calid..]
calendars = dict() # calid -> Calendar calendars = dict() # calid -> Calendar

View File

@ -1,6 +1,8 @@
from twitterscraper import query_tweets_from_user from twitterscraper import query_tweets_from_user
from modules.common.pollingservice import PollingService from modules.common.pollingservice import PollingService
# https://github.com/taspinar/twitterscraper/tree/master/twitterscraper # https://github.com/taspinar/twitterscraper/tree/master/twitterscraper
class MatrixModule(PollingService): class MatrixModule(PollingService):
@ -15,7 +17,9 @@ class MatrixModule(PollingService):
for tweet in tweets: for tweet in tweets:
if tweet.tweet_id not in self.known_ids: if tweet.tweet_id not in self.known_ids:
if send_messages: 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) self.known_ids.add(tweet.tweet_id)
except Exception: except Exception:
print('Polling twitter account failed:') print('Polling twitter account failed:')

View File

@ -6,8 +6,10 @@ import httpx
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from nio import RoomMessageText from nio import RoomMessageText
from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
""" """
Simple url fetch and spit out title module. Simple url fetch and spit out title module.
@ -152,3 +154,7 @@ class MatrixModule:
def help(self): 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" 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)))

View File

@ -1,10 +1,9 @@
from timeit import default_timer as timer from timeit import default_timer as timer
import os
import sys
import urllib.request
from urllib.request import urlopen from urllib.request import urlopen
from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
args.pop(0) args.pop(0)
@ -12,7 +11,7 @@ class MatrixModule:
url = args[0] url = args[0]
# check url # check url
if (not (url.startswith('http://') or url.startswith('https://'))): if not (url.startswith('http://') or url.startswith('https://')):
# print ("adding trailing https") # print ("adding trailing https")
url = "https://" + url url = "https://" + url
@ -34,7 +33,5 @@ class MatrixModule:
await bot.send_text(room, url + ": OK (" + str(retcode) + ") / " + "Size: " + str(length) + await bot.send_text(room, url + ": OK (" + str(retcode) + ") / " + "Size: " + str(length) +
" kB / Time: " + str(format(end - start, '.3g')) + " sec") " kB / Time: " + str(format(end - start, '.3g')) + " sec")
def help(self): def help(self):
return('check if IP or URL is accessable') return 'check if IP or URL is accessible'

View File

@ -1,7 +1,9 @@
import subprocess import subprocess
import os
class MatrixModule: from modules.common.module import BotModule
class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
args.pop(0) args.pop(0)

View File

@ -1,7 +1,8 @@
import subprocess import subprocess
import os from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
async def matrix_message(self, bot, room, event): async def matrix_message(self, bot, room, event):
args = event.body.split() args = event.body.split()
args.pop(0) args.pop(0)