diff --git a/Dockerfile b/Dockerfile index bf5a64f..6257824 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,5 +15,8 @@ COPY config config COPY modules modules VOLUME /bot/config +RUN useradd -m HomeBot && chown HomeBot -R /bot && apt install curl jq -y +USER HomeBot +WORKDIR /bot CMD [ "python", "-u", "./bot.py" ] diff --git a/modules/cam.py b/modules/cam.py new file mode 100644 index 0000000..53a5c01 --- /dev/null +++ b/modules/cam.py @@ -0,0 +1,24 @@ +from modules.common.module import BotModule +import requests + + +class MatrixModule(BotModule): + def __init__(self,name): + super().__init__(name) + self.motionurl = 'http://192.168.1.220:8080' + + async def matrix_message(self, bot, room, event): + args = event.body.split() + args.pop(0) + + if args[0] == 'config': + if args[1] == 'list': + req_url = self.motionurl + for arg in args: + req_url = f'{req_url}/{arg}' + + resp = requests.get(req_url) + await bot.send_text(resp.content) + + def help(self): + return 'Echoes back what user has said' diff --git a/modules/flog.py b/modules/flog.py deleted file mode 100644 index 867e89b..0000000 --- a/modules/flog.py +++ /dev/null @@ -1,270 +0,0 @@ -from logging import log -import sys -import traceback -import json -import time -import datetime -import requests -import urllib3 - -from datetime import datetime, timedelta -from random import randrange - -from modules.common.module import BotModule - -urllib3.disable_warnings() - -# API docs at: https://gitlab.com/lemoidului/ogn-flightbook/-/blob/master/doc/API.md -class FlightBook: - def __init__(self): - self.base_url = 'https://flightbook.glidernet.org/api' - self.AC_TYPES = [ '?', 'Glider', 'Towplane', \ - 'Helicopter', 'Parachute', 'Drop plane', 'Hang glider', \ - 'Paraglider', 'Powered', 'Jet', 'UFO', 'Balloon', \ - 'Airship', 'UAV', '?', 'Static object' ] - self.logged_flights = dict() # station -> [index of flight] - self.device_cache = dict() # Registration -> [address, CN] - - def get_flights(self, icao): - log_url = f'{self.base_url}/logbook/{icao}' - data = None - with requests.Session() as session: - response = session.get(log_url, headers={'Connection': 'close'}, verify=False) - data = response.json() - - # print(json.dumps(data, sort_keys=True, indent=4)) - self.update_device_cache(data) - return data - - def update_device_cache(self, data): - devices = data['devices'] - for device in devices: - if device["address"] and device["registration"]: - cache_entry = [device["address"], device["competition"]] - self.device_cache[device["registration"]] = cache_entry - - def address_for_registration(self, registration): - for reg in self.device_cache.keys(): - if reg.lower() == registration.lower(): - return self.device_cache[reg][0] - return None - - def address_for_cn(self, cn): - for reg in self.device_cache.keys(): - if self.device_cache[reg][1] == cn.upper(): - return self.device_cache[reg][0] - return None - - def format_time(self, time): - if not time: - return '··:··' - time = time.replace('h', ':') - return time - - def flight2string(self, flight, data): - devices = data['devices'] - device = devices[flight['device']] - start = self.format_time(flight["start"]) - end = self.format_time(flight["stop"]) - duration = ' ' - if flight["duration"]: - duration = time.strftime('%H:%M', time.gmtime(flight["duration"])) - maxalt = '' - if flight["max_alt"]: - maxalt = str(flight["max_alt"]) + 'm' - - identity = f'{device.get("registration") or ""} {device.get("aircraft") or ""} {device.get("competition") or ""} {maxalt}' - identity = ' '.join(identity.split()) - return f'{start} - {end} {duration} {identity}' - - def print_flights(self, data, showtow=False): - print(f'✈ Flights at {data["airfield"]["name"]} ({data["airfield"]["code"]}) {data["date"]}:') - flights = data['flights'] - for flight in flights: - if not showtow and flight["towing"]: - continue - print(self.flight2string(flight, data)) - - def test(): - fb = FlightBook() - data = fb.get_flights('LFMX') - fb.print_flights(data) - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.service_name = 'FLOG' - self.station_rooms = dict() # Roomid -> ogn station - self.live_rooms = [] # Roomid's with live enabled - self.logged_flights = dict() # Station -> number of flights - self.first_poll = True - self.enabled = False - self.fb = FlightBook() - - def matrix_start(self, bot): - super().matrix_start(bot) - self.add_module_aliases(bot, ['sar']) - - async def matrix_poll(self, bot, pollcount): - if pollcount % (6 * 5) == 0: # Poll every 5 min - await self.poll_implementation(bot) - - async def poll_implementation(self, bot): - for roomid in self.live_rooms: - station = self.station_rooms[roomid] - data = self.fb.get_flights(station) - if not data: - self.logger.warning(f"FLOG: Failed to get flights at {station}!") - return - flights = data['flights'] - - if len(flights) == 0 or (not station in self.logged_flights): - self.logged_flights[station] = [] - #print('Reset flight count for station ' + station) -# else: -# print(f'Got {len(flights)} flights at {station}') - - flightindex = 0 - for flight in flights: - if flight["towing"]: - continue - if flight["stop"]: - if not flightindex in self.logged_flights[station]: - if not self.first_poll: - await bot.send_text(bot.get_room_by_id(roomid), self.fb.flight2string(flight, data)) - self.logged_flights[station].append(flightindex) - # print(f'Logged flights at {station} now {self.logged_flights[station]}') - flightindex = flightindex + 1 - self.first_poll = False - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 1 and args[0] == "!flog": - if room.room_id in self.station_rooms: - station = self.station_rooms[room.room_id] - await self.show_flog(bot, room, station) - else: - await bot.send_text(room, 'No OGN station set for this room - set it first.') - - elif len(args) == 2 and args[0] == "!flog": - if args[1] == 'rmstation': - bot.must_be_admin(room, event) - del self.station_rooms[room.room_id] - self.live_rooms.remove(room.room_id) - await bot.send_text(room, f'Cleared OGN station for this room') - - elif args[1] == 'status': - print(self.logged_flights) - print(self.fb.device_cache) - bot.must_be_admin(room, event) - await bot.send_text(room, f'OGN station for this room: {self.station_rooms.get(room.room_id)}, live updates enabled: {room.room_id in self.live_rooms}') - - elif args[1] == 'poll': - bot.must_be_admin(room, event) - await self.poll_implementation(bot) - - elif args[1] == 'live': - bot.must_be_admin(room, event) - self.live_rooms.append(room.room_id) - bot.save_settings() - await bot.send_text(room, f'Sending live updates for station {self.station_rooms.get(room.room_id)} to this room') - - elif args[1] == 'rmlive': - bot.must_be_admin(room, event) - self.live_rooms.remove(room.room_id) - bot.save_settings() - await bot.send_text(room, f'Not sending live updates for station {self.station_rooms.get(room.room_id)} to this room anymore') - - else: - # Assume parameter is a station name - station = args[1] - await self.show_flog(bot, room, station) - elif len(args) == 2 and args[0] == "!sar": - registration = args[1] - address = self.fb.address_for_registration(registration) - if not address: - cn = args[1] - address = self.fb.address_for_cn(cn) - - coords = None - if address: - coords = self.get_coords_for_address(address) - if coords: - await bot.send_location(room, f'{registration} ({coords["utc"]})', coords["lat"], coords["lng"]) - else: - await bot.send_text(room, f'No Flarm ID found for {registration}!') - - elif len(args) == 3 and args[0] == "!flog": - if args[1] == 'station': - bot.must_be_admin(room, event) - - station = args[2] - self.station_rooms[room.room_id] = station - self.logger.info(f'Station now for this room {self.station_rooms.get(room.room_id)}') - - bot.save_settings() - await bot.send_text(room, f'Set OGN station {station} to this room') - - - def get_coords_for_address(self, address): - # https://flightbook.glidernet.org/api/live/address/~91DADF5B86 - url = f'{self.fb.base_url}/live/address/{address}' - data = None - with requests.Session() as session: - response = session.get(url, headers={'Connection': 'close'}, verify=False) - data = response.json() - - # print(json.dumps(data, sort_keys=True, indent=4)) - return data - - - def text_flog(self, data, showtow): - out = "" - if len(data["flights"]) == 0: - out = f'No known flights today at {data["airfield"]["name"]}' - else: - out = f'Flights at {data["airfield"]["name"]} ({data["airfield"]["code"]}) {data["date"]}:' + "\n" - flights = data['flights'] - for flight in flights: - if not showtow and flight["towing"]: - continue - out = out + self.fb.flight2string(flight, data) + "\n" - return out - - def html_flog(self, data, showtow): - out = "" - if len(data["flights"]) == 0: - out = f'No known flights today at {data["airfield"]["name"]}' - else: - out = f'✈ Flights at {data["airfield"]["name"]} ({data["airfield"]["code"]}) {data["date"]}:' + "\n" - flights = data['flights'] - out = out + "" - return out - - async def show_flog(self, bot, room, station): - data = self.fb.get_flights(station) - if data: - await bot.send_html(room, self.html_flog(data, False), self.text_flog(data, False)) - else: - await bot.send_text(room, f"Failed to get flight log for {station}") - - def get_settings(self): - data = super().get_settings() - data['station_rooms'] = self.station_rooms - data['live_rooms'] = self.live_rooms - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get('station_rooms'): - self.station_rooms = data['station_rooms'] - if data.get('live_rooms'): - self.live_rooms = data['live_rooms'] - - def help(self): - return ('Open Glider Network Field Log') diff --git a/modules/gfycat.py b/modules/gfycat.py deleted file mode 100644 index 52efdbc..0000000 --- a/modules/gfycat.py +++ /dev/null @@ -1,100 +0,0 @@ -import urllib.request -import urllib.parse -import urllib.error - -import requests -from nio import AsyncClient, UploadError -from nio import UploadResponse - -from collections import namedtuple -from modules.common.module import BotModule - -class gfycat(object): - """ - A very simple module that allows you to - 1. search a gif on gfycat from a remote location - """ - - # Urls - url = "https://api.gfycat.com" - - def __init__(self): - super(gfycat, self).__init__() - - def __fetch(self, url, param): - import json - try: - # added simple User-Ajent string to avoid CloudFlare block this request - headers = {'User-Agent': 'Mozilla/5.0'} - req = urllib.request.Request(url+param, headers=headers) - connection = urllib.request.urlopen(req).read() - except urllib.error.HTTPError as err: - raise ValueError(err.read()) - result = namedtuple("result", "raw json") - return result(raw=connection, json=json.loads(connection)) - - def search(self, param): - result = self.__fetch(self.url, "/v1/gfycats/search?search_text=%s" % urllib.parse.quote_plus(param)) - if "errorMessage" in result.json: - raise ValueError("%s" % self.json["errorMessage"]) - return _gfycatSearch(result) - -class _gfycatUtils(object): - - """ - A utility class that provides the necessary common - for all the other classes - """ - - def __init__(self, param, json): - super(_gfycatUtils, self).__init__() - # This can be used for other functions related to this class - self.res = param - self.js = json - - def raw(self): - return self.res.raw - - def json(self): - return self.js - - def __len__(self): - return len(self.js) - - def get(self, what): - try: - return self.js[what] - except KeyError as error: - return ("Sorry, can't find %s" % error) - -class _gfycatSearch(_gfycatUtils): - - """ - This class will provide more information for an existing url - """ - - def __init__(self, param): - super(_gfycatSearch, self).__init__(param, param.json["gfycats"]) - -class MatrixModule(BotModule): - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) > 1: - gif_url = "No image found" - query = event.body[len(args[0])+1:] - try: - gifs = gfycat().search(query) - if len(gifs) < 1: - await bot.send_text(room, gif_url) - return - - gif_url = gifs.get(0)["content_urls"]["largeGif"]["url"] - await bot.upload_and_send_image(room, gif_url) - except Exception as exc: - gif_url = str(exc) - await bot.send_text(room, gif_url) - else: - await bot.send_text(room, 'Usage: !gfycat ') - - def help(self): - return ('Gfycat bot') diff --git a/modules/ghproj.py b/modules/ghproj.py deleted file mode 100644 index 3450db4..0000000 --- a/modules/ghproj.py +++ /dev/null @@ -1,134 +0,0 @@ -from github import Github -import re -import json - -from modules.common.module import BotModule - -# Helper class with reusable code for github project stuff -class GithubProject: - # New format to support array of colors: domains={"koneet":["#BFDADC","#0CBBF0","#0CBBF0","#E15D19","#ED49CF"],"tilat":["#0E8A16","#1E8A16"]} - def get_domains(description): - p = re.compile('domains=\{.*\}') - matches = json.loads(p.findall(description)[0][8:]) - return matches - - def get_domain(reponame, domain): - g = Github() - repo = g.get_repo(reponame) - domains = GithubProject.get_domains(repo.description) - if(not len(domains)): - return None, None - domain_colors = domains.get(domain, None) - if not domain_colors: - return None, None - - open_issues = repo.get_issues(state='open') - domain_labels = [] - labels = repo.get_labels() - for label in labels: - for domain_color in domain_colors: - if label.color == domain_color[1:]: - domain_labels.append(label) - - domain_issues = dict() - domain_ok = [] - for label in domain_labels: - label_issues = [] - for issue in open_issues: - if label in issue.labels: - label_issues.append(issue) - if len(label_issues): - domain_issues[label.name] = label_issues - else: - domain_ok.append(label.name) - - return domain_issues, domain_ok - - def domain_to_string(reponame, issues, ok): - text_out = reponame + ":\n" - for label in issues.keys(): - text_out = text_out + f'{label}: ' - for issue in issues[label]: - # todo: add {issue.html_url} when URL previews can be disabled - text_out = text_out + f'[{issue.title}] ' - text_out = text_out + f'\n' - - text_out = text_out + " OK : " + ', '.join(ok) - return text_out - - def domain_to_html(reponame, issues, ok): - html_out = f'{reponame}:
' - for label in issues.keys(): - html_out = html_out + f'🚧 {label}: ' - for issue in issues[label]: - # todo: add {issue.html_url} when URL previews can be disabled - html_out = html_out + f'[{issue.title}] ' - html_out = html_out + f'
' - - html_out = html_out + " OK ☑️ " + ', '.join(ok) - return html_out - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.repo_rooms = dict() - - async def matrix_message(self, bot, room, event): - args = event.body.split() - args.pop(0) - - if len(args) == 1: - if args[0] == 'rmrepo': - bot.must_be_admin(room, event) - del self.repo_rooms[room.room_id] - await bot.send_text(room, 'Github repo removed from this room.') - bot.save_settings() - return - if args[0] == 'repo': - await bot.send_text(room, f'Github repo for this room is {self.repo_rooms.get(room.room_id, "not set")}.') - return - - domain = args[0] - reponame = self.repo_rooms.get(room.room_id, None) - if reponame: - issues, ok = GithubProject.get_domain(reponame, domain) - if issues or ok: - await self.send_domain_status(bot, room, reponame, issues, ok) - else: - await bot.send_text(room, f'No labels with domain {domain} found.') - else: - await bot.send_text(room, f'No github repo set for this room. Use setrepo to set it.') - return - - if len(args) == 2: - if args[0] == 'setrepo': - bot.must_be_admin(room, event) - - reponame = args[1] - self.logger.info(f'Adding repo {reponame} to room id {room.room_id}') - - self.repo_rooms[room.room_id] = reponame - await bot.send_text(room, f'Github repo {reponame} set to this room.') - bot.save_settings() - return - - await bot.send_text(room, 'Unknown command') - - async def send_domain_status(self, bot, room, reponame, issues, ok): - text_out = GithubProject.domain_to_string(reponame, issues, ok) - html_out = GithubProject.domain_to_html(reponame, issues, ok) - await bot.send_html(room, html_out, text_out) - - - def help(self): - return 'Github asset management' - - def get_settings(self): - data = super().get_settings() - data["repo_rooms"] = self.repo_rooms - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("repo_rooms"): - self.repo_rooms = data["repo_rooms"] diff --git a/modules/giphy.py b/modules/giphy.py deleted file mode 100644 index a4f7224..0000000 --- a/modules/giphy.py +++ /dev/null @@ -1,60 +0,0 @@ -import urllib.request -import urllib.parse -import urllib.error - -import os -import giphypop -import requests -from nio import AsyncClient, UploadError -from nio import UploadResponse - -from collections import namedtuple -from modules.common.module import BotModule - -class MatrixModule(BotModule): - api_key = None - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 3 and args[1] == 'apikey': - bot.must_be_owner(event) - - self.api_key = args[2] - bot.save_settings() - await bot.send_text(room, 'Api key set') - elif len(args) > 1: - gif_url = "No image found" - query = event.body[len(args[0])+1:] - try: - g = giphypop.Giphy(api_key=self.api_key) - gifs = [] - try: - for x in g.search(phrase=query, limit=1): - gifs.append(x) - except Exception: - pass - if len(gifs) < 1: - await bot.send_text(room, gif_url) - return - - gif_url = gifs[0].media_url - await bot.upload_and_send_image(room, gif_url) - return - except Exception as exc: - gif_url = str(exc) - await bot.send_text(room, gif_url) - else: - await bot.send_text(room, 'Usage: !giphy ') - - def get_settings(self): - data = super().get_settings() - data["api_key"] = self.api_key - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("api_key"): - self.api_key = data["api_key"] - - def help(self): - return ('Giphy bot') diff --git a/modules/ig.py b/modules/ig.py deleted file mode 100644 index 83de68d..0000000 --- a/modules/ig.py +++ /dev/null @@ -1,41 +0,0 @@ -import sys -import traceback -from datetime import datetime, timedelta -from random import randrange - -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, name): - super().__init__(name) - self.instagram = Instagram() - self.service_name = 'Instagram' - self.enabled = False - - async def poll_implementation(self, bot, account, roomid, send_messages): - try: - medias = self.instagram.get_medias(account, 5) - self.logger.info(f'Polling instagram account {account} for room {roomid} - got {len(medias)} posts.') - for media in medias: - if send_messages: - if media.identifier not in self.known_ids: - await bot.send_html(bot.get_room_by_id(roomid), - f'Instagram {account}: {media.caption}', - f'{account}: {media.caption} {media.link}') - self.known_ids.add(media.identifier) - - except InstagramNotFoundException: - self.logger.error(f"{account} does not exist - deleting from room") - self.account_rooms[roomid].remove(account) - bot.save_settings() - except Exception: - self.logger.error('Polling instagram account failed:') - traceback.print_exc(file=sys.stderr) - - polldelay = timedelta(minutes=30 + randrange(30)) - self.next_poll_time[roomid] = datetime.now() + polldelay diff --git a/modules/jitsi.py b/modules/jitsi.py deleted file mode 100644 index aa44cb5..0000000 --- a/modules/jitsi.py +++ /dev/null @@ -1,45 +0,0 @@ -from nio import RoomMessageUnknown, UnknownEvent - -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - bot = None - - def matrix_start(self, bot): - super().matrix_start(bot) - self.bot = bot - bot.client.add_event_callback(self.unknownevent_cb, (UnknownEvent,)) - - def matrix_stop(self, bot): - super().matrix_stop(bot) - bot.remove_callback(self.unknownevent_cb) - - async def unknownevent_cb(self, room, event): - try: - if 'type' in event.source and event.source['type'] == 'im.vector.modular.widgets' and event.source['content']['type'] == 'jitsi': - # Todo: Domain not found in Element Android events! - domain = event.source['content']['data']['domain'] - conferenceId = event.source['content']['data']['conferenceId'] - isAudioOnly = event.source['content']['data']['isAudioOnly'] - sender = event.source['sender'] - sender_response = await self.bot.client.get_displayname(event.sender) - sender = sender_response.displayname - # This is just a guess - is this the proper way to generate URL? Probably not. - jitsiUrl = f'https://{domain}/{conferenceId}' - - calltype = 'video call' - if isAudioOnly: - calltype = 'audio call' - - plainMessage = f'{sender} started a {calltype}: {jitsiUrl}' - htmlMessage = f'{sender} started a {calltype}' - await self.bot.send_html(room, htmlMessage, plainMessage) - except Exception as e: - self.logger.error(f"Failed parsing Jitsi event. Error: {e}") - - async def matrix_message(self, bot, room, event): - pass - - def help(self): - return 'Sends text links when user starts a Jitsi video or audio call in room' diff --git a/modules/loc.py b/modules/loc.py deleted file mode 100644 index a17e976..0000000 --- a/modules/loc.py +++ /dev/null @@ -1,148 +0,0 @@ -from geopy.geocoders import Nominatim -from nio import RoomMessageUnknown - -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.bot = None - self.enabled_rooms = [] - - 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) - - ''' - Location events are like: https://spec.matrix.org/v1.2/client-server-api/#mlocation - { - "content": { - "body": "geo:61.49342512194717,23.765914658307736", - "geo_uri": "geo:61.49342512194717,23.765914658307736", - "msgtype": "m.location", - "org.matrix.msc1767.text": "geo:61.49342512194717,23.765914658307736", - "org.matrix.msc3488.asset": { - "type": "m.pin" - }, - "org.matrix.msc3488.location": { - "description": "geo:61.49342512194717,23.765914658307736", - "uri": "geo:61.49342512194717,23.765914658307736" - }, - "org.matrix.msc3488.ts": 1653837929839 - }, - "room_id": "!xsBGdLYGrfYhGfLtHG:hacklab.fi", - "type": "m.room.message" - } - - BUT sometimes there's ; separating altitude?? - { - "content": { - "body": "geo:61.4704211,23.4864855;36.900001525878906", - "geo_uri": "geo:61.4704211,23.4864855;36.900001525878906", - "msgtype": "m.location", - "org.matrix.msc1767.text": "geo:61.4704211,23.4864855;36.900001525878906", - "org.matrix.msc3488.asset": { - "type": "m.self" - }, - "org.matrix.msc3488.location": { - "description": "geo:61.4704211,23.4864855;36.900001525878906", - "uri": "geo:61.4704211,23.4864855;36.900001525878906" - }, - "org.matrix.msc3488.ts": 1653931683087 - }, - "origin_server_ts": 1653931683998, - "sender": "@cos:hacklab.fi", - "type": "m.room.message", - "unsigned": { - "age": 70 - }, - "event_id": "$6xXutKF9EppPMMdc4aQLZjHyd8My0rIZuNZEcuSIPws", - "room_id": "!CLofqdurVWZCMpFnqM:hacklab.fi" -} - ''' - - async def unknown_cb(self, room, event): - if event.msgtype != 'm.location': - return - if room.room_id not in self.enabled_rooms: - return - location_text = event.content['body'] - - # Fallback if body is empty - if (len(location_text) == 0) or ('geo:' in location_text): - location_text = 'location' - - sender_response = await self.bot.client.get_displayname(event.sender) - sender = sender_response.displayname - - geo_uri = event.content['geo_uri'] - try: - geo_uri = geo_uri[4:] # Strip geo: - - if ';' in geo_uri: # Strip altitude, if present - geo_uri = geo_uri.split(';')[0] - latlon = geo_uri.split(',') - - # Sanity checks to avoid url manipulation - float(latlon[0]) - float(latlon[1]) - except Exception: - self.bot.send_text(room, "Error: Invalid location " + geo_uri) - return - - osm_link = f"https://www.openstreetmap.org/?mlat={latlon[0]}&mlon={latlon[1]}" - - plain = f'{sender} sent {location_text} {osm_link} 🚩' - html = f'{sender} sent {location_text} 🚩' - - await self.bot.send_html(room, html, plain) - - async def matrix_message(self, bot, room, event): - args = event.body.split() - args.pop(0) - if len(args) == 0: - await bot.send_text(room, 'Usage: !loc ') - return - elif len(args) == 1: - if args[0] == 'enable': - bot.must_be_admin(room, event) - self.enabled_rooms.append(room.room_id) - self.enabled_rooms = list(dict.fromkeys(self.enabled_rooms)) # Deduplicate - await bot.send_text(room, "Ok, sending locations events here as text versions") - bot.save_settings() - return - if args[0] == 'disable': - bot.must_be_admin(room, event) - self.enabled_rooms.remove(room.room_id) - await bot.send_text(room, "Ok, disabled here") - bot.save_settings() - return - - query = event.body[4:] - geolocator = Nominatim(user_agent=bot.appid) - self.logger.info('loc: looking up %s ..', query) - location = geolocator.geocode(query) - self.logger.info('loc rx %s', location) - if location: - await bot.send_location(room, location.address, location.latitude, location.longitude, "m.pin") - else: - 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' - - def get_settings(self): - data = super().get_settings() - data["enabled_rooms"] = self.enabled_rooms - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("enabled_rooms"): - self.enabled_rooms = data["enabled_rooms"] diff --git a/modules/md.py b/modules/md.py deleted file mode 100644 index c0c9621..0000000 --- a/modules/md.py +++ /dev/null @@ -1,149 +0,0 @@ -from mastodon import Mastodon - -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - apps = dict() # instance url <-> [app_id, app_secret] - logins = dict() # mxid <-> [username, accesstoken, instanceurl] - roomlogins = dict() # roomid <-> [username, accesstoken, instanceurl] - public = False - - async def matrix_message(self, bot, room, event): - args = event.body.split() - args.pop(0) - if len(args) >= 1: - if args[0] == "toot": - toot_body = " ".join(args[1:]) - accesstoken = None - if room.room_id in self.roomlogins.keys(): - bot.must_be_admin(room, event) - username = self.roomlogins[room.room_id][0] - accesstoken = self.roomlogins[room.room_id][1] - instanceurl = self.roomlogins[room.room_id][2] - elif event.sender in self.logins.keys(): - if not self.public: - bot.must_be_owner(event) - username = self.logins[event.sender][0] - accesstoken = self.logins[event.sender][1] - instanceurl = self.logins[event.sender][2] - if accesstoken: - toottodon = Mastodon( - access_token = accesstoken, - api_base_url = instanceurl - ) - tootdict = toottodon.toot(toot_body) - await bot.send_text(room, tootdict['url']) - else: - await bot.send_text(room, f'{event.sender} has not logged in yet with the bot. Please do so.') - return - - if len(args) == 4: - if args[0] == "login": - if not self.public: - bot.must_be_owner(event) - mxid = event.sender - instanceurl = args[1] - username = args[2] - password = args[3] - await self.register_app_if_necessary(bot, room, instanceurl) - await self.login_to_account(bot, room, mxid, None, instanceurl, username, password) - return - if len(args) == 5: - if args[0] == "roomlogin": - if not self.public: - bot.must_be_owner(event) - roomalias = args[1] - instanceurl = args[2] - username = args[3] - password = args[4] - roomid = await bot.get_room_by_alias(roomalias) - if roomid: - await self.register_app_if_necessary(bot, room, instanceurl) - await self.login_to_account(bot, room, None, roomid, instanceurl, username, password) - else: - await bot.send_text(room, f'Unknown room alias {roomalias} - invite bot to the room first.') - return - if len(args) == 1: - if args[0] == "status": - out = f'App registered on {len(self.apps)} instances, public use enabled: {self.public}\n' - out = out + f'{len(self.logins)} users logged in:\n' - for login in self.logins.keys(): - out = out + f' - {login} as {self.logins[login][0]} on {self.logins[login][2]}\n' - out = out + f'{len(self.roomlogins)} per-room logins:\n' - for roomlogin in self.roomlogins: - out = out + f' - {roomlogin} as {self.roomlogins[roomlogin][0]} on {self.roomlogins[roomlogin][2]}\n' - - await bot.send_text(room, out) - if args[0] == "logout": - if event.sender in self.logins.keys(): - # TODO: Is there a way to invalidate the access token with API? - del self.logins[event.sender] - bot.save_settings() - await bot.send_text(room, f'{event.sender} login data removed from the bot.') - if args[0] == "roomlogout": - bot.must_be_admin(room, event) - if room.room_id in self.roomlogins.keys(): - del self.roomlogins[room.room_id] - bot.save_settings() - await bot.send_text(room, f'Login data for this room removed from the bot.') - else: - await bot.send_text(room, f'No login found for room id {room.room_id}.') - if args[0] == "clear": - bot.must_be_owner(event) - self.logins = dict() - self.roomlogins = dict() - bot.save_settings() - await bot.send_text(room, f'All Mastodon logins cleared') - if args[0] == "setpublic": - bot.must_be_owner(event) - self.public = True - bot.save_settings() - await bot.send_text(room, f'Mastodon usage is now public use') - if args[0] == "setprivate": - bot.must_be_owner(event) - self.public = False - bot.save_settings() - await bot.send_text(room, f'Mastodon usage is now restricted to bot owners') - - async def register_app_if_necessary(self, bot, room, instanceurl): - if not instanceurl in self.apps.keys(): - app = Mastodon.create_app(f'Hemppa The Bot - {bot.client.user}', api_base_url = instanceurl) - self.apps[instanceurl] = [app[0], app[1]] - bot.save_settings() - await bot.send_text(room, f'Registered Mastodon app on {instanceurl}') - - async def login_to_account(self, bot, room, mxid, roomid, instanceurl, username, password): - mastodon = Mastodon(client_id = self.apps[instanceurl][0], client_secret = self.apps[instanceurl][1], api_base_url = instanceurl) - access_token = mastodon.log_in(username, password) - print('login_To_account', mxid, roomid) - if mxid: - self.logins[mxid] = [username, access_token, instanceurl] - await bot.send_text(room, f'Logged Matrix user {mxid} into {instanceurl} as {username}') - elif roomid: - self.roomlogins[roomid] = [username, access_token, instanceurl] - await bot.send_text(room, f'Set room {roomid} Mastodon user to {username} on {instanceurl}') - - bot.save_settings() - - def get_settings(self): - data = super().get_settings() - data['apps'] = self.apps - data['logins'] = self.logins - data['roomlogins'] = self.roomlogins - data['public'] = self.public - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("apps"): - self.apps = data["apps"] - if data.get("logins"): - self.logins = data["logins"] - if data.get("roomlogins"): - self.roomlogins = data["roomlogins"] - if data.get("public"): - self.public = data["public"] - - def help(self): - return ('Mastodon') diff --git a/modules/metar.py b/modules/metar.py deleted file mode 100644 index 44ffb18..0000000 --- a/modules/metar.py +++ /dev/null @@ -1,20 +0,0 @@ -import urllib.request - -from modules.common.module import BotModule - - -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" - response = urllib.request.urlopen(metar_url) - lines = response.readlines() - await bot.send_text(room, lines[1].decode("utf-8").strip()) - else: - await bot.send_text(room, 'Usage: !metar ') - - def help(self): - return ('Metar data access (usage: !metar )') diff --git a/modules/mumble.py b/modules/mumble.py deleted file mode 100644 index cd54051..0000000 --- a/modules/mumble.py +++ /dev/null @@ -1,96 +0,0 @@ -from modules.common.module import BotModule -import random -import socket -from struct import pack, unpack -import time - -# Modified from https://gist.github.com/azlux/315c924af4800ffbc2c91db3ab8a59bc - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.host = None - self.port = 64738 - - def set_settings(self, data): - super().set_settings(data) - if data.get('host'): - self.host = data['host'] - if data.get('port'): - self.port = data['port'] - - def get_settings(self): - data = super().get_settings() - data['host'] = self.host - data['port'] = self.port - return data - - async def matrix_message(self, bot, room, event): - args = event.body.split() - - if len(args) > 1 and args[1] in ['set', 'setserver']: - bot.must_be_owner(event) - self.logger.info(f"room: {room.name} sender: {event.sender} is setting the server settings") - if len(args) < 3: - self.host = None - return await bot.send_text(room, f'Usage: !{args[0]} {args[1]} [host] ([port])') - self.host = args[2] - if len(args) > 3: - self.port = int(args[3]) - if not self.port: - self.port = 64738 - bot.save_settings() - return await bot.send_text(room, f'Set server settings: host: {self.host} port: {self.port}') - - self.logger.info(f"room: {room.name} sender: {event.sender} wants mumble info") - if not self.host: - return await bot.send_text(room, f'No mumble host info set!') - - try: - ret = self.mumble_ping() - # https://wiki.mumble.info/wiki/Protocol - # [0,1,2,3] = version - version = '.'.join(map(str, ret[1:4])) - # [4] = identifier passed to the server (used here to get ping time) - ping = int(time.time() * 1000) - ret[4] - # [7] = bandwidth - # [5] = users - # [6] = max users - await bot.send_text(room, f'{self.host}:{self.port} (v{version}): {ret[5]} / {ret[6]} (ping: {ping}ms)') - except socket.gaierror as e: - self.logger.error(f"room: {room.name}: mumble_ping failed: {e}") - await bot.send_text(room, f'Could not get get mumble server info: {e}') - - def mumble_ping(self): - addrinfo = socket.getaddrinfo(self.host, self.port, 0, 0, socket.SOL_UDP) - - for (family, socktype, proto, canonname, sockaddr) in addrinfo: - s = socket.socket(family, socktype, proto=proto) - s.settimeout(2) - - buf = pack(">iQ", 0, int(time.time() * 1000)) - try: - s.sendto(buf, sockaddr) - except (socket.gaierror, socket.timeout) as e: - continue - - try: - data, addr = s.recvfrom(1024) - except socket.timeout: - continue - - return unpack(">bbbbQiii", data) - - def help(self): - return 'Show info about a mumble server' - - def long_help(self): - text = self.help() + ( - '\n- "!mumble": Get the status of the configured mumble server') - - if bot and event and bot.is_owner(event): - text += ( - '\nOwner commands:' - '\n- "!mumble set [host] ([port])": Set use the following host and port' - '\n- If no port is given, defaults to 64738') - return text diff --git a/modules/mxma.py b/modules/mxma.py deleted file mode 100644 index 7a4a980..0000000 --- a/modules/mxma.py +++ /dev/null @@ -1,30 +0,0 @@ -from modules.common.module import BotModule -import requests, json -import traceback - -from modules.common.pollingservice import PollingService - -class MatrixModule(PollingService): - def __init__(self, name): - super().__init__(name) - self.service_name = 'MXMA' - self.poll_interval_min = 5 - self.poll_interval_random = 2 - self.owner_only = True - self.send_all = True - self.enabled = False - - async def poll_implementation(self, bot, account, roomid, send_messages): - try: - response = requests.get(url=account, timeout=5) - if response.status_code == 200: - if 'messages' in response.json(): - messages = response.json()['messages'] - for message in messages: - success = await bot.send_msg(message['to'], message['title'], message['message']) - except Exception: - self.logger.error('Polling MXMA failed:') - traceback.print_exc(file=sys.stderr) - - def help(self): - return 'Matrix messaging API' diff --git a/modules/notam.py b/modules/notam.py deleted file mode 100644 index 1c5012c..0000000 --- a/modules/notam.py +++ /dev/null @@ -1,47 +0,0 @@ -import re -import urllib.request - -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 2 and len(args[1]) == 4: - icao = args[1].upper() - notam = self.get_notam(icao) - await bot.send_text(room, notam) - else: - await bot.send_text(room, 'Usage: !notam ') - - def help(self): - return ('NOTAM data access (usage: !notam ) - Currently Finnish airports only') - - # 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.') - - icao_first_letter = icao[2] - if icao_first_letter < 'M': - notam_url = "https://www.ais.fi/ais/bulletins/envfra.htm" - else: - notam_url = "https://www.ais.fi/ais/bulletins/envfrm.htm" - - response = urllib.request.urlopen(notam_url) - lines = response.readlines() - lines = b''.join(lines) - lines = lines.decode("ISO-8859-1") - # Strip EN-ROUTE from end - lines = lines[0:lines.find('')] - - startpos = lines.find('') - if startpos > -1: - endpos = lines.find('

', startpos) - if endpos == -1: - endpos = len(lines) - notam = lines[startpos:endpos] - notam = re.sub('<[^<]+?>', ' ', notam) - if len(notam) > 4: - return notam - return f'Cannot parse notam for {icao} at {notam_url}' diff --git a/modules/printing.py b/modules/printing.py deleted file mode 100644 index 3e5dcc6..0000000 --- a/modules/printing.py +++ /dev/null @@ -1,116 +0,0 @@ -from modules.common.module import BotModule -from nio import RoomMessageMedia -from typing import Optional -import sys -import traceback -import cups -import httpx -import aiofiles -import os - -# Credit: https://medium.com/swlh/how-to-boost-your-python-apps-using-httpx-and-asynchronous-calls-9cfe6f63d6ad -async def download_file(url: str, filename: Optional[str] = None) -> str: - filename = filename or url.split("/")[-1] - filename = f"/tmp/{filename}" - client = httpx.AsyncClient() - async with client.stream("GET", url) as resp: - resp.raise_for_status() - async with aiofiles.open(filename, "wb") as f: - async for data in resp.aiter_bytes(): - if data: - await f.write(data) - await client.aclose() - return filename - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.printers = dict() # roomid <-> printername - self.bot = None - self.paper_size = 'A4' # Todo: configurable - self.enabled = False - - async def file_cb(self, room, event): - try: - if self.bot.should_ignore_event(event): - return - if room.room_id in self.printers: - printer = self.printers[room.room_id] - self.logger.debug(f'RX file - MXC {event.url} - from {event.sender}') - https_url = await self.bot.client.mxc_to_http(event.url) - self.logger.debug(f'HTTPS URL {https_url}') - filename = await download_file(https_url) - self.logger.debug(f'RX filename {filename}') - conn = cups.Connection () - conn.printFile(printer, filename, f"Printed from Matrix - {filename}", {'fit-to-page': 'TRUE', 'PageSize': self.paper_size}) - await self.bot.send_text(room, f'Printing file on {printer}..') - os.remove(filename) # Not sure if we should wait first? - else: - self.logger.debug(f'No printer configured for room {room.room_id}') - except: - self.logger.warning(f"File callback failure") - traceback.print_exc(file=sys.stderr) - await self.bot.send_text(room, f'Printing failed, sorry. See log for details.') - - def matrix_start(self, bot): - super().matrix_start(bot) - bot.client.add_event_callback(self.file_cb, RoomMessageMedia) - self.bot = bot - - def matrix_stop(self, bot): - super().matrix_stop(bot) - bot.remove_callback(self.file_cb) - self.bot = None - - async def matrix_message(self, bot, room, event): - bot.must_be_owner(event) - args = event.body.split() - args.pop(0) - conn = cups.Connection () - printers = conn.getPrinters () - - if len(args) == 1: - if args[0] == 'list': - msg = f"Available printers:\n" - for printer in printers: - print(printer, printers[printer]["device-uri"]) - msg += f' - {printer} / {printers[printer]["device-uri"]}' - for roomid, printerid in self.printers.items(): - if printerid == printer: - msg += f' <- room {roomid}' - msg += '\n' - await bot.send_text(room, msg) - elif args[0] == 'rmroomprinter': - del self.printers[room.room_id] - await bot.send_text(room, f'Deleted printer from this room.') - bot.save_settings() - - if len(args) == 2: - if args[0] == 'setroomprinter': - printer = args[1] - if printer in printers: - await bot.send_text(room, f'Printing with {printer} here.') - self.printers[room.room_id] = printer - bot.save_settings() - else: - await bot.send_text(room, f'No printer called {printer} in your CUPS.') - if args[0] == 'setpapersize': - self.paper_size = args[1] - bot.save_settings() - await bot.send_text(room, f'Paper size set to {self.paper_size}.') - - def help(self): - return 'Print files from Matrix' - - def get_settings(self): - data = super().get_settings() - data["printers"] = self.printers - data["paper_size"] = self.paper_size - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("printers"): - self.printers = data["printers"] - if data.get("paper_size"): - self.paper_size = data["paper_size"] diff --git a/modules/pt.py b/modules/pt.py deleted file mode 100644 index 4b4440c..0000000 --- a/modules/pt.py +++ /dev/null @@ -1,80 +0,0 @@ -from typing import Text -import urllib -import urllib.request -from urllib.parse import urlencode, quote_plus -import json -import time - -from modules.common.module import BotModule - -class PeerTubeClient: - def __init__(self): - self.instance_url = 'https://sepiasearch.org/' - - def search(self, search_string, count=0): - if count == 0: - count = 15 # Pt default, could also remove from params.. - params = urlencode({'search': search_string, 'count': count}, quote_via=quote_plus) - search_url = self.instance_url + 'api/v1/search/videos?' + params - response = urllib.request.urlopen(search_url) - data = json.loads(response.read().decode("utf-8")) - return data - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.instance_url = 'https://sepiasearch.org/' - - def matrix_start(self, bot): - super().matrix_start(bot) - self.add_module_aliases(bot, ['ptall']) - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 3: - if args[1] == "setinstance": - bot.must_be_owner(event) - self.instance_url = args[2] - bot.save_settings() - await bot.send_text(room, 'Instance url set to ' + self.instance_url, bot_ignore=True) - return - - if len(args) == 2: - if args[1] == "showinstance": - await bot.send_text(room, 'Using instance at ' + self.instance_url, bot_ignore=True) - return - - if len(args) > 1: - query = event.body[len(args[0])+1:] - p = PeerTubeClient() - p.instance_url = self.instance_url - count = 1 - if args[0] == '!ptall': - count = 0 - data = p.search(query, count) - if len(data['data']) > 0: - for video in data['data']: - video_url = video.get("url") or self.instance_url + 'videos/watch/' + video["uuid"] - duration = time.strftime('%H:%M:%S', time.gmtime(video["duration"])) - instancedata = video["account"]["host"] - html = f'{video["name"]} {video["description"] or ""} [{duration}] @ {instancedata}' - text = f'{video_url} : {video["name"]} {video.get("description") or ""} [{duration}]' - await bot.send_html(room, html, text, bot_ignore=True) - else: - await bot.send_text(room, 'Sorry, no videos found found.', bot_ignore=True) - - else: - await bot.send_text(room, 'Usage: !pt or !ptall to return all results') - - def get_settings(self): - data = super().get_settings() - data['instance_url'] = self.instance_url - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("instance_url"): - self.instance_url = data["instance_url"] - - def help(self): - return ('PeerTube search') diff --git a/modules/rasp.py b/modules/rasp.py deleted file mode 100644 index 1a2c3f5..0000000 --- a/modules/rasp.py +++ /dev/null @@ -1,19 +0,0 @@ -from modules.common.module import BotModule -import urllib.request - -class MatrixModule(BotModule): - async def matrix_message(self, bot, room, event): - args = event.body.split() - args.pop(0) - day = 0 - hour = 12 - if len(args) >= 1: - day = int(args[0]) - 1 - if len(args) == 2: - hour = int(args[1]) - - imgurl = 'http://ennuste.ilmailuliitto.fi/' + str(day) + '/wstar_bsratio.curr.' + str(hour) + '00lst.d2.png' - await bot.upload_and_send_image(room, imgurl, f"RASP Day {day+1} at {hour}:00", no_cache=True) - - def help(self): - return 'RASP Gliding Weather forecast, Finland only' diff --git a/modules/relay.py b/modules/relay.py deleted file mode 100644 index e9a2153..0000000 --- a/modules/relay.py +++ /dev/null @@ -1,109 +0,0 @@ -from modules.common.module import BotModule -from nio import RoomMessageText - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.bridges = dict() - self.bot = None - self.enabled = False - - async def message_cb(self, room, event): - if self.bot.should_ignore_event(event): - return - - if event.body.startswith('!'): - return - - source_id = None - target_id = None - - for src_id, tgt_id in self.bridges.items(): - if room.room_id == src_id: - source_id = src_id - target_id = tgt_id - elif room.room_id == tgt_id: - source_id = tgt_id - target_id = src_id - - if not source_id or not target_id: - return - - target_room = self.bot.get_room_by_id(target_id) - if(target_room): - sendernick = target_room.user_name(event.sender) - if not sendernick: - sendernick = event.sender - await self.bot.send_text(target_room, f'<{sendernick}> {event.body}', msgtype="m.text", bot_ignore=True) - else: - self.logger.warning(f"Bot doesn't seem to be in bridged room {target_id}") - - def matrix_start(self, bot): - super().matrix_start(bot) - bot.client.add_event_callback(self.message_cb, RoomMessageText) - self.bot = bot - - def matrix_stop(self, bot): - super().matrix_stop(bot) - bot.remove_callback(self.message_cb) - self.bot = None - - async def matrix_message(self, bot, room, event): - bot.must_be_admin(room, event) - args = event.body.split() - args.pop(0) - if len(args) == 1: - if args[0] == 'list': - i = 1 - msg = f"Active relay bridges ({len(self.bridges)}):\n" - for src_id, tgt_id in self.bridges.items(): - srcroom = self.bot.get_room_by_id(src_id) - tgtroom = self.bot.get_room_by_id(tgt_id) - - if srcroom: - srcroom = srcroom.display_name - else: - srcroom = f'??? {src_id}' - - if tgtroom: - tgtroom = tgtroom.display_name - else: - tgtroom = f'??? {tgt_id}' - - msg += f'{i}: {srcroom} <-> {tgtroom}' - i = i + 1 - await bot.send_text(room, msg) - - if len(args) == 2: - if args[0] == 'bridge': - roomid = args[1] - room_to_bridge = bot.get_room_by_id(roomid) - if room_to_bridge: - await bot.send_text(room, f'Bridging {room_to_bridge.display_name} here.') - self.bridges[room.room_id] = roomid - bot.save_settings() - else: - await bot.send_text(room, f'I am not on room with id {roomid} (note: use id, not alias)!') - elif args[0] == 'unbridge': - idx = int(args[1]) - 1 - i = 0 - for src_id, tgt_id in self.bridges.items(): - if i == idx: - del self.bridges[src_id] - await bot.send_text(room, f'Unbridged {src_id} and {tgt_id}.') - bot.save_settings() - return - i = i + 1 - - def help(self): - return 'Simple relaybot between two Matrix rooms' - - def get_settings(self): - data = super().get_settings() - data["bridges"] = self.bridges - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("bridges"): - self.bridges = data["bridges"] diff --git a/modules/spaceapi.py b/modules/spaceapi.py deleted file mode 100644 index e598cb8..0000000 --- a/modules/spaceapi.py +++ /dev/null @@ -1,66 +0,0 @@ -from modules.common.pollingservice import PollingService -from urllib.request import urlopen -import json -import time - -class MatrixModule(PollingService): - def __init__(self, name): - super().__init__(name) - self.accountroomid_laststatus = {} - self.template = '{spacename} is now {open_closed}' - self.i18n = {'open': 'open 🔓', 'closed': 'closed 🔒'} - - async def poll_implementation(self, bot, account, roomid, send_messages): - self.logger.debug(f'polling space api {account}.') - spacename, is_open = MatrixModule.open_status(account) - - open_str = self.i18n['open'] if is_open else self.i18n['closed'] - text = self.template.format(spacename=spacename, open_closed=open_str) - self.logger.debug(text) - - last_status = self.accountroomid_laststatus.get(account+roomid, False) - if send_messages and last_status != is_open: - await bot.send_text(bot.get_room_by_id(roomid), text) - self.accountroomid_laststatus[account+roomid] = is_open - bot.save_settings() - - @staticmethod - def open_status(spaceurl): - with urlopen(spaceurl, timeout=5) as response: - js = json.load(response) - - return js['space'], js['state']['open'] - - def get_settings(self): - data = super().get_settings() - data['laststatus'] = self.accountroomid_laststatus - data['template'] = self.template - data['i18n'] = self.i18n - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get('laststatus'): - self.accountroomid_laststatus = data['laststatus'] - if data.get('template'): - self.template = data['template'] - if data.get('i18n'): - self.i18n = data['i18n'] - - def help(self): - return "Notify about Space-API status changes (open or closed)." - - def long_help(self, bot, event, **kwargs): - text = self.help() + \ - ' This is a polling service. Therefore there are additional ' + \ - 'commands: list, debug, poll, clear, add URL, del URL\n' + \ - '!spaceapi add URL: to add a space-api endpoint\n' + \ - '!spaceapi list: to list the endpoint configured for this room.\n' + \ - f'I will look for changes roughly every {self.poll_interval_min} ' + \ - 'minutes. Find out more about Space-API at https://spaceapi.io/.' - if bot.is_owner(event): - text += '\nA template and I18N can be configured via settings of ' + \ - 'the module. Use "!bot export spacepi", then change the ' + \ - 'settings and import again with "!bot import spacepi SETTINGS".' - - return text diff --git a/modules/taf.py b/modules/taf.py deleted file mode 100644 index dfe018a..0000000 --- a/modules/taf.py +++ /dev/null @@ -1,23 +0,0 @@ -import urllib.request - -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 2: - icao = args[1] - taf_url = "https://aviationweather.gov/adds/dataserver_current/httpparam?dataSource=tafs&requestType=retrieve&format=csv&hoursBeforeNow=3&timeType=issue&mostRecent=true&stationString=" + icao.upper() - response = urllib.request.urlopen(taf_url) - lines = response.readlines() - if len(lines) > 6: - taf = lines[6].decode("utf-8").split(',')[0] - await bot.send_text(room, taf.strip()) - else: - await bot.send_text(room, 'Cannot find taf for ' + icao) - else: - await bot.send_text(room, 'Usage: !taf ') - - def help(self): - return ('Taf data access (usage: !taf )') diff --git a/modules/tautulli.py b/modules/tautulli.py deleted file mode 100644 index 425fd68..0000000 --- a/modules/tautulli.py +++ /dev/null @@ -1,247 +0,0 @@ -import time -import urllib.request -import urllib.parse -import urllib.error - -import aiohttp.web -import requests -import os -import json -import asyncio -from aiohttp import web -from future.moves.urllib.parse import urlencode -from nio import MatrixRoom - -from modules.common.module import BotModule - -import nest_asyncio -nest_asyncio.apply() - -rooms = dict() -global_bot = None - -send_entry_lock = asyncio.Lock() - - -async def send_entry(blob, content_type, fmt_params, rooms): - async with send_entry_lock: - for room_id in rooms: - room = MatrixRoom(room_id=room_id, own_user_id=os.getenv("BOT_OWNERS"), - encrypted=rooms[room_id]) - if blob and content_type: - await global_bot.upload_and_send_image(room, blob, text="", blob=True, blob_content_type=content_type) - - await global_bot.send_html(room, msg_template_html.format(**fmt_params), - msg_template_plain.format(**fmt_params)) - - -def get_image(img=None, width=1000, height=1500): - """ - Return image data as array. - Array contains the image content type and image binary - - Parameters required: img { Plex image location } - Optional parameters: width { the image width } - height { the image height } - Output: array - """ - - pms_url = os.getenv("PLEX_MEDIA_SERVER_URL") - pms_token = os.getenv("PLEX_MEDIA_SERVER_TOKEN") - if not pms_url or not pms_token: - return None - - width = width or 1000 - height = height or 1500 - - if img: - params = {'url': 'http://127.0.0.1:32400%s' % (img), 'width': width, 'height': height, 'format': "png"} - - uri = pms_url + '/photo/:/transcode?%s' % urlencode(params) - - headers = {'X-Plex-Token': pms_token} - - session = requests.Session() - try: - r = session.request("GET", uri, headers=headers) - r.raise_for_status() - except Exception: - return None - - response_status = r.status_code - response_content = r.content - response_headers = r.headers - if response_status in (200, 201): - return response_content, response_headers['Content-Type'] - - -def get_from_entry(entry): - blob = None - content_type = "" - if "art" in entry: - pms_image = get_image(entry["art"], 600, 300) - if pms_image: - (blob, content_type) = pms_image - - fmt_params = { - "title": entry["title"], - "year": entry["year"], - "audience_rating": entry["audience_rating"], - "directors": ", ".join(entry["directors"]), - "actors": ", ".join(entry["actors"]), - "summary": entry["summary"], - "tagline": entry["tagline"], - "genres": ", ".join(entry["genres"]) - } - - return (blob, content_type, fmt_params) - - -msg_template_html = """ - {title} -({year})- Rating: {audience_rating}
- Director(s): {directors}
- Actors: {actors}
- {summary}
- {tagline}
- Genre(s): {genres}

""" - -msg_template_plain = """*{title} -({year})- Rating: {audience_rating}* - Director(s): {directors} - Actors: {actors} - {summary} - {tagline} - Genre(s): {genres} - -""" - - -class WebServer: - def __init__(self, host, port): - self.host = host - self.port = port - self.app = web.Application() - self.app.router.add_post('/notify', self.notify) - - async def run(self): - if not self.host or not self.port: - return - - loop = asyncio.get_event_loop() - runner = web.AppRunner(self.app) - loop.run_until_complete(runner.setup()) - site = web.TCPSite(runner, host=self.host, port=self.port) - loop.run_until_complete(site.start()) - - async def notify(self, request: web.Request) -> web.Response: - try: - data = await request.json() - if "genres" in data: - data["genres"] = data["genres"].split(",") - - if "actors" in data: - data["actors"] = data["actors"].split(",") - - if "directors" in data: - data["directors"] = data["directors"].split(",") - - global rooms - (blob, content_type, fmt_params) = get_from_entry(data) - await send_entry(blob, content_type, fmt_params, rooms) - - except Exception as exc: - message = str(exc) - return web.HTTPBadRequest(body=message) - - return web.Response() - - -class MatrixModule(BotModule): - httpd = None - rooms = dict() - api_key = None - - def __init__(self, name): - super().__init__(name) - self.httpd = WebServer(os.getenv("TAUTULLI_NOTIFIER_ADDR"), os.getenv("TAUTULLI_NOTIFIER_PORT")) - - def matrix_start(self, bot): - super().matrix_start(bot) - global global_bot - global_bot = bot - loop = asyncio.get_event_loop() - loop.run_until_complete(self.httpd.run()) - - def matrix_stop(self, bot): - super().matrix_stop(bot) - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 3 and args[1] == 'apikey': - bot.must_be_owner(event) - - self.api_key = args[2] - bot.save_settings() - await bot.send_text(room, 'Api key set') - elif len(args) == 2: - media_type = args[1] - if media_type != "movie" and media_type != "show" and media_type != "artist": - await bot.send_text(room, "media type '%s' provided not valid" % media_type) - return - - try: - url = "{}/api/v2?apikey={}&cmd=get_recently_added&count=10".format(os.getenv("TAUTULLI_URL"), self.api_key) - req = urllib.request.Request(url + "&media_type=" + media_type) - connection = urllib.request.urlopen(req).read() - entries = json.loads(connection) - if "response" not in entries and "data" not in entries["response"] and "recently_added" not in entries["response"]["data"]: - await bot.send_text(room, "no recently added for %s" % media_type) - return - - for entry in entries["response"]["data"]["recently_added"]: - (blob, content_type, fmt_params) = get_from_entry(entry) - await send_entry(blob, content_type, fmt_params, {room.room_id: room}) - - except urllib.error.HTTPError as err: - raise ValueError(err.read()) - except Exception as exc: - message = str(exc) - await bot.send_text(room, message) - elif len(args) == 4: - if args[1] == "add" or args[1] == "remove": - room_id = args[2] - encrypted = args[3] - if args[1] == "add": - self.rooms[room_id] = encrypted == "encrypted" - await bot.send_text(room, f"Added {room_id} to rooms notification list") - else: - del self.rooms[room_id] - await bot.send_text(room, f"Removed {room_id} to rooms notification list") - - bot.save_settings() - global rooms - rooms = self.rooms - else: - await bot.send_text(room, 'Usage: !tautulli | %room_id% %encrypted%') - else: - await bot.send_text(room, 'Usage: !tautulli | %room_id% %encrypted%') - - def get_settings(self): - data = super().get_settings() - data["api_key"] = self.api_key - data["rooms"] = self.rooms - global rooms - rooms = self.rooms - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("rooms"): - self.rooms = data["rooms"] - global rooms - rooms = self.rooms - if data.get("api_key"): - self.api_key = data["api_key"] - - def help(self): - return ('Tautulli recently added bot') - diff --git a/modules/teamup.py b/modules/teamup.py deleted file mode 100644 index 2333a17..0000000 --- a/modules/teamup.py +++ /dev/null @@ -1,165 +0,0 @@ -import time -from datetime import datetime - -from pyteamup import Calendar - -# -# TeamUp calendar notifications -# -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - def __init__(self, name): - super().__init__(name) - self.api_key = None - self.calendar_rooms = dict() # Roomid -> [calid, calid..] - self.calendars = dict() # calid -> Calendar - self.enabled = False - - async def matrix_poll(self, bot, pollcount): - if self.api_key: - if pollcount % (6 * 5) == 0: # Poll every 5 min - await self.poll_all_calendars(bot) - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 1: - if self.calendar_rooms.get(room.room_id): - for calendarid in self.calendar_rooms.get(room.room_id): - calendar = self.calendars[calendarid] - events = calendar.get_event_collection() - for event in events: - s = '' + str(event.start_dt.day) + \ - '.' + str(event.start_dt.month) - if not event.all_day: - s = s + ' ' + \ - event.start_dt.strftime( - "%H:%M") + ' (' + str(event.duration) + ' min)' - s = s + ' ' + event.title + \ - " " + (event.notes or '') - await bot.send_html(room, s, s) - elif len(args) == 2: - if args[1] == 'list': - await bot.send_text(room, f'Calendars in this room: {self.calendar_rooms.get(room.room_id) or []}') - elif args[1] == 'poll': - bot.must_be_owner(event) - await self.poll_all_calendars(bot) - elif len(args) == 3: - if args[1] == 'add': - bot.must_be_admin(room, event) - - calid = args[2] - self.logger.info(f'Adding calendar {calid} to room id {room.room_id}') - - if self.calendar_rooms.get(room.room_id): - if calid not in self.calendar_rooms[room.room_id]: - self.calendar_rooms[room.room_id].append(calid) - else: - await bot.send_text(room, 'This teamup calendar already added in this room!') - return - else: - self.calendar_rooms[room.room_id] = [calid] - - self.logger.info(f'Calendars now for this room {self.calendar_rooms.get(room.room_id)}') - - bot.save_settings() - self.setup_calendars() - await bot.send_text(room, 'Added new teamup calendar to this room') - if args[1] == 'del': - bot.must_be_admin(room, event) - - calid = args[2] - self.logger.info(f'Removing calendar {calid} from room id {room.room_id}') - - if self.calendar_rooms.get(room.room_id): - self.calendar_rooms[room.room_id].remove(calid) - - self.logger.info(f'Calendars now for this room {self.calendar_rooms.get(room.room_id)}') - - bot.save_settings() - self.setup_calendars() - await bot.send_text(room, 'Removed teamup calendar from this room') - if args[1] == 'apikey': - bot.must_be_owner(event) - - self.api_key = args[2] - bot.save_settings() - self.setup_calendars() - await bot.send_text(room, 'Api key set') - - def help(self): - return ('Polls teamup calendar.') - - async def poll_all_calendars(self, bot): - delete_rooms = [] - for roomid in self.calendar_rooms: - if roomid in bot.client.rooms: - calendars = self.calendar_rooms[roomid] - for calendarid in calendars: - events, timestamp = self.poll_server( - self.calendars[calendarid]) - self.calendars[calendarid].timestamp = timestamp - for event in events: - await bot.send_text(bot.get_room_by_id(roomid), 'Calendar: ' + self.eventToString(event)) - else: - delete_rooms.append(roomid) - - for roomid in delete_rooms: - self.calendar_rooms.pop(roomid, None) - - def poll_server(self, calendar): - events, timestamp = calendar.get_changed_events(calendar.timestamp) - return events, timestamp - - def to_datetime(self, dts): - try: - return datetime.strptime(dts, '%Y-%m-%dT%H:%M:%S') - except ValueError: - pos = len(dts) - 3 - dts = dts[:pos] + dts[pos + 1:] - return datetime.strptime(dts, '%Y-%m-%dT%H:%M:%S%z') - - def eventToString(self, event): - startdt = self.to_datetime(event['start_dt']) - if len(event['title']) == 0: - event['title'] = '(empty name)' - - if (event['delete_dt']): - s = event['title'] + ' deleted.' - else: - s = event['title'] + " " + (event['notes'] or '') + \ - ' ' + str(startdt.day) + '.' + str(startdt.month) - if not event['all_day']: - s = s + ' ' + \ - startdt.strftime("%H:%M") + \ - ' (' + str(event['duration']) + ' min)' - # todo: proper html stripper.. - s = s.replace('

', '') - s = s.replace('

', '\n') - return s - - def setup_calendars(self): - self.calendars = dict() - if self.api_key: - for roomid in self.calendar_rooms: - calendars = self.calendar_rooms[roomid] - for calid in calendars: - self.calendars[calid] = Calendar(calid, self.api_key) - self.calendars[calid].timestamp = int(time.time()) - - def get_settings(self): - 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'): - self.api_key = data['apikey'] - if self.api_key and len(self.api_key) == 0: - self.api_key = None - self.setup_calendars() diff --git a/modules/wa.py b/modules/wa.py deleted file mode 100644 index 5b38fb1..0000000 --- a/modules/wa.py +++ /dev/null @@ -1,134 +0,0 @@ -import urllib.request -import wolframalpha -from html import escape -import json -from modules.common.module import BotModule - - -class MatrixModule(BotModule): - app_id = '' - - def matrix_start(self, bot): - super().matrix_start(bot) - self.add_module_aliases(bot, ['wafull']) - - async def matrix_message(self, bot, room, event): - args = event.body.split() - if len(args) == 3: - if args[1] == "appid": - bot.must_be_owner(event) - self.app_id = args[2] - bot.save_settings() - await bot.send_text(room, 'App id set') - return - - if len(args) > 1: - if self.app_id == '': - await bot.send_text(room, 'Please get and set a appid: https://products.wolframalpha.com/simple-api/documentation/') - return - - query = event.body[len(args[0])+1:] - client = wolframalpha.Client(self.app_id) - res = client.query(query) - result = "?SYNTAX ERROR" - if res['@success']: - self.logger.debug(f"room: {room.name} sender: {event.sender} sent a valid query to wa") - else: - self.logger.info(f"wa error: {res['@error']}") - short, full = self.parse_api_response(res) - if full[0] and 'full' in args[0]: - html, plain = full - elif short[0]: - html, plain = short - else: - plain = 'Could not find response for ' + query - html = plain - await bot.send_html(room, html, plain) - else: - await bot.send_text(room, 'Usage: !wa ') - - def get_settings(self): - data = super().get_settings() - data['app_id'] = self.app_id - return data - - def set_settings(self, data): - super().set_settings(data) - if data.get("app_id"): - self.app_id = data["app_id"] - - def parse_api_response(self, res): - """Parses the pods from wa and prepares texts to send to matrix - - :param res: the result from wolframalpha.Client - :type res: dict - :return: a tuple of tuples: ((primary_html, primary_plaintext), (full_html, full_plaintext)) - :rtype: tuple - """ - htmls = [] - texts = [] - primary = None - fallback = None - - pods = res.get('pod') - if not pods: - return (('(data not available)', '(data not available)'), ) * 2 - - # workaround for bug(?) in upstream wa package - if hasattr(pods, 'get'): - pods = [pods] - for pod in res['pod']: - pod_htmls = [] - pod_texts = [] - spods = pod.get('subpod') - if not spods: - continue - - # workaround for bug(?) in upstream wa package - if hasattr(spods, 'get'): - spods = [spods] - for spod in spods: - title = spod.get('@title') - text = spod.get('plaintext') - if not text: - continue - - if title: - html = f'{escape(title)}: {escape(text)}' - text = f'{title}: {text}' - else: - html = escape(text) - pod_htmls += html.split('\n') - pod_texts += text.split('\n') - - if pod_texts: - title = pod.get('@title') - pod_html = '\n'.join([f'

{escape(title)}\n

    '] - + [f'
  • {s}
  • ' for s in pod_htmls] - + ['

']) - pod_text = '\n'.join([title] - + [f'- {s}' for s in pod_texts]) - htmls.append(pod_html) - texts.append(pod_text) - if not primary and self.is_primary(pod): - primary = (f'{escape(title)}: ' + ' | '.join(pod_htmls), - f'{title}: ' + ' | '.join(pod_texts)) - else: - fallback = fallback or (' | '.join(pod_htmls), ' | '.join(pod_texts)) - - return (primary or fallback, ('\n'.join(htmls), '\n'.join(texts))) - - def is_primary(self, pod): - return pod.get('@primary') or 'Definition' in pod.get('@title') or 'Result' in pod.get('@title') - - def help(self): - return ('Wolfram Alpha search') - - def long_help(self, bot=None, event=None, **kwargs): - text = self.help() + ( - '\n- "!wa [query]": Query WolframAlpha and return the primary pod' - '\n- "!wafull [query]": Query WolframAlpha and return all pods' - ) - if bot and event and bot.is_owner(event): - text += '\n- "!wa appid [appid]": Set appid' - return text