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
.vscode
.idea
# ignore Pipfile.lock
Pipfile.lock

7
bot.py
View File

@ -9,10 +9,11 @@ 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
# Couple of custom exceptions
@ -125,7 +126,8 @@ class Bot:
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')
await self.send_text(room,
f'Module {command} experienced difficulty: {sys.exc_info()[0]} - see log for details')
traceback.print_exc(file=sys.stderr)
async def invite_cb(self, room, event):
@ -144,6 +146,7 @@ class Bot:
def load_module(self, modulename):
try:
print("load module: " + modulename)
module = importlib.import_module('modules.' + modulename)
module = reload(module)
cls = getattr(module, 'MatrixModule')

View File

@ -1,7 +1,9 @@
import urllib.request
from datetime import datetime, timedelta
from datetime import datetime
from modules.common.module import BotModule
class MatrixModule(BotModule):
class MatrixModule:
def matrix_start(self, bot):
self.starttime = datetime.now()
@ -23,7 +25,8 @@ class MatrixModule:
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.')
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
@ -44,7 +47,8 @@ class MatrixModule:
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}')
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}')
@ -55,4 +59,4 @@ class MatrixModule:
await bot.send_text(room, 'Unknown command, sorry.')
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 random import randrange
class PollingService:
def __init__(self):
self.known_ids = set()
@ -47,15 +46,16 @@ 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" \
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)
@ -111,4 +111,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

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)

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):
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)
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 nio import RoomMessageUnknown
from modules.common.module import BotModule
class MatrixModule:
class MatrixModule(BotModule):
bot = None
def matrix_start(self, bot):
@ -58,4 +59,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

@ -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,9 +17,10 @@ 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:
class MatrixModule(BotModule):
def matrix_start(self, bot):
self.bot = bot
self.SCOPES = ['https://www.googleapis.com/auth/calendar.readonly']
@ -135,7 +137,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()

View File

@ -1,13 +1,15 @@
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__()
@ -21,7 +23,9 @@ 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:

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:

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:

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:

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

View File

@ -1,6 +1,8 @@
from twitterscraper import query_tweets_from_user
from modules.common.pollingservice import PollingService
# https://github.com/taspinar/twitterscraper/tree/master/twitterscraper
class MatrixModule(PollingService):
@ -15,7 +17,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

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

View File

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

View File

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