From d6b1062fe86264ba921ffff0b4fc6b368cc45af5 Mon Sep 17 00:00:00 2001 From: Scott Wallace Date: Tue, 14 Jul 2020 17:34:39 +0100 Subject: [PATCH] Initial commit --- .gitignore | 2 + LICENSE | 21 +++++++ README.md | 5 ++ base-config.yaml | 9 +++ maubot.yaml | 8 +++ shameotron.py | 141 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 186 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 base-config.yaml create mode 100644 maubot.yaml create mode 100644 shameotron.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5f561f5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.mbp +*.zip diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d447398 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Tulir Asokan + +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..09cbcfc --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# echo +A simple [maubot](https://github.com/maubot/maubot) that displays everyone's homeserver software version. + +## Usage +* `!shame` - Reply with a list of homeservers and their respective running versions. diff --git a/base-config.yaml b/base-config.yaml new file mode 100644 index 0000000..b65f255 --- /dev/null +++ b/base-config.yaml @@ -0,0 +1,9 @@ +# The https://github.com/matrix-org/matrix-federation-tester instance to use. +# {server} is replaced with the server name. +federation_tester: https://matrix.org/federationtester/api/report?server_name={server} +# +# Instances to ignore as they might be permanently erroring and causing slow responses +# dead_servers: +# - host1.example.com +# - host2.example.com +# - host.example.org diff --git a/maubot.yaml b/maubot.yaml new file mode 100644 index 0000000..1d7b3d6 --- /dev/null +++ b/maubot.yaml @@ -0,0 +1,8 @@ +maubot: 0.1.0 +id: sh.wallace.matrix.shameotron +version: 0.0.2 +license: MIT +modules: +- shameotron +main_class: ShameOTron +database: false diff --git a/shameotron.py b/shameotron.py new file mode 100644 index 0000000..51d5c2d --- /dev/null +++ b/shameotron.py @@ -0,0 +1,141 @@ +""" +[Maubot](https://mau.dev/maubot/maubot) plugin to shame room members into +upgrading their Matrix homeservers to the latest version. +""" +import json + +from typing import Dict, List, Type + +import requests + +from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper +from mautrix.types import TextMessageEventContent, MessageType, Format, \ + RelatesTo, RelationType, EventID, RoomID, UserID +from mautrix.util import markdown + +from maubot import Plugin, MessageEvent +from maubot.handlers import command + + +class Config(BaseProxyConfig): + """ + Config class + """ + def do_update(self, helper: ConfigUpdateHelper) -> None: + """ + Class method to update the config + """ + helper.copy('federation_tester') + helper.copy('dead_servers') + + +class ShameOTron(Plugin): + """ + Main class for the Shame-o-Tron + """ + async def start(self) -> None: + """ + Class method for plugin startup + """ + self.on_external_config_update() + + + @classmethod + def get_config_class(cls) -> Type[Config]: + """ + Class method for getting the config + """ + return Config + + + async def _edit(self, room_id: RoomID, event_id: EventID, text: str) -> None: + """ + Class method to update an existing message event + """ + content = TextMessageEventContent(msgtype=MessageType.NOTICE, body=text, format=Format.HTML, + formatted_body=markdown.render(text)) + content.set_edit(event_id) + await self.client.send_message(room_id, content) + + + async def _load_members(self, room_id: RoomID) -> Dict[str, List[UserID]]: + """ + Class method to return the servers and room members + """ + users = await self.client.get_joined_members(room_id) + servers: Dict[str, List[UserID]] = {} + for user in users: + _, server = self.client.parse_user_id(user) + servers.setdefault(server, []).append(user) + return servers + + + async def query_homeserver_version(self, host): + """ + Function to query the Federation Tester to retrieve the running version + for a server + + host: (str) Server to get version for + + Returns: (str) Version string of the server + """ + try: + req = requests.get( + self.config["federation_tester"].format(server=host), + timeout=10000 + ) + except requests.exceptions.Timeout: + return '[TIMEOUT]' + + data = json.loads(req.text) + + if not data['FederationOK']: + return '[OFFLINE]' + + try: + return data['Version']['version'] + except KeyError: + return '[ERROR]' + + + @command.new('shame', help='Show versions of all homeservers in the room') + async def shame_handler(self, evt: MessageEvent) -> None: + """ + Class method to handle the `!shame` command + """ + event_id = await evt.reply('Loading member list...') + member_servers = await self._load_members(evt.room_id) + + # Filter out the "dead servers" + dead_servers = self.config['dead_servers'] + if dead_servers: + # Return a unique list + member_servers = sorted( + list( + set(member_servers.keys() - set(dead_servers)) + ) + ) + + await self._edit( + evt.room_id, + event_id, + 'Member list loaded, fetching versions... please wait...' + ) + + versions = [] + for host in member_servers: + versions.append( + (host, await self.query_homeserver_version(host)) + ) + + await self._edit( + evt.room_id, + event_id, + ( + '#### Homeserver versions\n' + + '\n'.join( + f'* {host}: [{version}]({self.config["federation_tester"].format(server=host)})' + for host, version in versions + ) + ) + )