commit 1f58c4b15f4e13076510bf67e54ccda22f08d453 Author: s3lph Date: Sun Jan 16 20:23:01 2022 +0100 Initial commit diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..26997df --- /dev/null +++ b/LICENSE @@ -0,0 +1,16 @@ +Copyright 2022 s3lph + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, +sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or +substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT +OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..ebc7711 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# SpaceAPI Bot Plugin for Maubot diff --git a/me.s3lph.spaceapi/base-config.yaml b/me.s3lph.spaceapi/base-config.yaml new file mode 100644 index 0000000..c60bade --- /dev/null +++ b/me.s3lph.spaceapi/base-config.yaml @@ -0,0 +1,13 @@ +command_prefix: spaceapi +poll: + interval: 60 + room: '#foo:example.org' + cache: 'spaceapi.cache.json' +spaceapi: https://example.org/spaceapi +messages: + # You can use the following fields: + # {lastchange}: Time the space state was last changed + open: | + The hackerspace is now OPEN. + closed: | + The hackerspace is now CLOSED. diff --git a/me.s3lph.spaceapi/maubot.yaml b/me.s3lph.spaceapi/maubot.yaml new file mode 100644 index 0000000..c576509 --- /dev/null +++ b/me.s3lph.spaceapi/maubot.yaml @@ -0,0 +1,10 @@ +maubot: 0.18.0 +id: me.s3lph.spaceapi +version: 0.1.0 +license: MIT +modules: + - spaceapi +main_class: SpaceapiBot +config: true +extra_files: + - base-config.yaml diff --git a/me.s3lph.spaceapi/spaceapi.py b/me.s3lph.spaceapi/spaceapi.py new file mode 100644 index 0000000..1437359 --- /dev/null +++ b/me.s3lph.spaceapi/spaceapi.py @@ -0,0 +1,131 @@ +from typing import Tuple, Type + +import json +import asyncio +import aiohttp +from datetime import datetime + +from maubot import Plugin +from maubot.handlers import command, web + +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper +from mautrix.types import MessageEvent, MessageType, TextMessageEventContent, RoomID, RoomAlias + + +class Config(BaseProxyConfig): + + def do_update(self, helper: ConfigUpdateHelper) -> None: + helper.copy("spaceapi") + helper.copy("poll") + helper.copy("messages") + helper.copy('command_prefix') + + +class SpaceapiBot(Plugin): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._room: RoomID = None + self._update_room = True + self._polling_task = None + + def get_command_prefix(self) -> str: + return self.config.get('command_prefix', 'spaceapi') + + @classmethod + def get_config_class(cls) -> Type[BaseProxyConfig]: + return Config + + def on_external_config_update(self) -> None: + self.config.load_and_update() + self._update_room = True + + async def start(self) -> None: + self.on_external_config_update() + await self._get_room() + if self._polling_task: + self._polling_task.cancel() + self._polling_task = asyncio.create_task(self.poll()) + + async def stop(self) -> None: + if self._polling_task: + self._polling_task.cancel() + self._polling_task = None + + async def _get_room(self) -> RoomID: + if self._room is not None and not self._update_room: + return self._room + room = self.config['poll']['room'] + if room.startswith('#'): + if 'resolve_room_alias' in dir(self.client): + self._room = (await self.client.resolve_room_alias(RoomAlias(room))).room_id + else: + self._room = (await self.client.get_room_alias(RoomAlias(room))).room_id + else: + self._room = RoomID(room) + self._update_room = False + return self._room + + async def update_spaceapi_status(self, http: aiohttp.ClientSession) -> Tuple[object, bool]: + try: + async with http.get(self.config['spaceapi']) as resp: + if resp.status != 200: + raise RuntimeError() + rdata = await resp.text() + now = json.loads(rdata) + except BaseException: + return None, False + with open(self.config['poll']['cache'], 'a+') as cache: + cache.seek(0) + try: + pdata = cache.read() + previous = json.loads(pdata) + except BaseException as e: + self.log.exception(e) + previous = json.loads(rdata) + cache.seek(0) + cache.truncate() + cache.write(rdata) + changed = (now['state']['open'] != previous['state']['open']) + return now, changed + + async def send_spaceapi_update(self, state: object, requester: MessageEvent) -> None: + if requester: + room = requester.room_id + else: + room = await self._get_room() + try: + fmt = { + 'lastchange': datetime.fromtimestamp(state['state']['lastchange']).strftime('%H:%M') + } + if state['state']['open']: + body = self.config['messages']['open'].format(**fmt) + else: + body = self.config['messages']['closed'].format(**fmt) + except BaseException as e: + self.log.exception(e) + if not requester: + return + body = self.config['messages']['error'] + msg = TextMessageEventContent(msgtype=MessageType.TEXT, body=body) + if requester: + await requester.reply(msg) + else: + await self.client.send_message(room, msg) + + @command.new(name=get_command_prefix) + async def cmd(self, evt: MessageEvent): + async with aiohttp.ClientSession(read_timeout=15, conn_timeout=5) as http: + state, _ = await self.update_spaceapi_status(http) + await self.send_spaceapi_update(state, evt) + + async def poll(self) -> None: + async with aiohttp.ClientSession(read_timeout=15, conn_timeout=5) as http: + while True: + await asyncio.sleep(self.config['poll'].get('interval', 60)) + try: + state, changed = await self.update_spaceapi_status(http) + if changed: + await self.send_spaceapi_update(state) + except BaseException as e: + self.log.exception(e)