diff --git a/base-config.yaml b/base-config.yaml index ce96758..e496539 100644 --- a/base-config.yaml +++ b/base-config.yaml @@ -2,15 +2,8 @@ # DO NOT CHANGE THIS UNLESS YOU KNOW WHAT YOU ARE DOING command_prefix: report -# Feed URL -# This should point to a helpful page about the feeds. Required for RSS to function -feed_url: "https://example.org" - -# Which regions the bot should pay attention to. -# Pass through empty list to allow all senders for a region. -# Add a list of MatrixIDs to whitelist senders for a region. -# Use __global__ to refer to all regions. -# Code looks to see if a sender is allowed for either a specific region or __global__. +# Which regions the bot should pay attention to +# Additionally, optionally whitelist specific accounts for each region allowed_regions: foo: - "@reports:fiftyfiftyonerarizona.org" @@ -20,14 +13,13 @@ allowed_regions: buzz: aaa: -# List of regions to serve feeds to. -# YOU MUST SPECIFY EVERY REGION. -# __global__ is a global feed, not serving feeds for all regions. -serve_regions: - - "__global__" - - "Arizona" - - "California" +# Bot-specific configurations per region +region_configs: + __global__: true + arizona: true + california: true + somethingyoudontwantanrssfor: true -# If this bot should send a reaction to the message. -# Will send 👍 on successful storage in database, and 👎 on failiure. +# If this bot should send a reaction to the message +# if the notification was successful send_reaction: true \ No newline at end of file diff --git a/compile.bash b/compile.bash deleted file mode 100644 index 66c85a3..0000000 --- a/compile.bash +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Get filename -id=$(cat maubot.yaml | grep "id:" | tr ' ' '\n' | tail -n1) -version=$(cat maubot.yaml | grep "version:" | tr ' ' '\n' | tail -n1) -filename="$id-$version.mbp" - -# Compress to zip -zip $filename * \ No newline at end of file diff --git a/consumerrss.py b/consumerrss.py index a8e37e3..a075d6e 100644 --- a/consumerrss.py +++ b/consumerrss.py @@ -1,41 +1,18 @@ from maubot import Plugin, MessageEvent from maubot.handlers import command -from maubot.handlers import web # Needed for configuration from typing import Type from mautrix.util.config import BaseProxyConfig, ConfigUpdateHelper -from mautrix.util.async_db import UpgradeTable, Scheme, Connection -from aiohttp.web import Request, Response, json_response import aiohttp -import datetime - -import xml.etree.ElementTree as et - -upgrade_table = UpgradeTable() -@upgrade_table.register(description="Initial revision") -async def upgrade_v1(conn: Connection) -> None: - # Table contains: - # Unique ID (event ID) (Primary) - # Timestamp (TIMESTAMP type) - # Region (string) - # Message (string) - await conn.execute( - """CREATE TABLE reports ( - id TEXT PRIMARY KEY, - timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL, - region TEXT NOT NULL, - message TEXT - )""" - ) # Ensures a running instance gets an updated config from the Maubot interface class Config(BaseProxyConfig): def do_update(self, helper: ConfigUpdateHelper) -> None: helper.copy("command_prefix") helper.copy("allowed_regions") - helper.copy("serve_regions") + helper.copy("region_configs") helper.copy("send_reaction") class ConsumerRSS(Plugin): @@ -48,124 +25,83 @@ class ConsumerRSS(Plugin): def get_config_class(cls) -> Type[BaseProxyConfig]: return Config - # Maubot system needs this to register the table - @classmethod - def get_db_upgrade_table(cls) -> UpgradeTable | None: - return upgrade_table - # Get !command_name setting from config to register it def get_command_name(self) -> str: return self.config["command_prefix"] # Checks if a sender if allowed to send for a particular region - def validate_sender(self, region: str, sender: str): - # Mautrix isn't documented, like at all, so I'm just gonna catch the - # error because IDK how to see if a map is inside a map. - try: allowed_list = self.config["allowed_regions"][region] - except: return False - - if allowed_list is None: return True # Empty list - if sender in allowed_list: return True - - # Sender not allowed in region config - return False + def validateSender(self, region: str, sender: str): + # Check that config value exists + if not self.config["allowed_regions"]: return False + # Check that region is allowed + if not region in self.config["allowed_regions"]: return False + # All senders allowed for this region + if len(self.config["allowed_regions"][region]) == 0: return True + # Check that sender is allowed for region + if not sender in self.config["allowed_regions"][region]: return False + return True - # Does the necessary config checks for the given event - # Returns a region name if sender is allowed for a region, or None - def validate_report(self, evt: MessageEvent, message: str): + # Does the necesary config checks for the given event + # Returns list of regions to process (strings) + # Currently just the specified region and "__global__" + def validateReport(self, evt: MessageEvent, message: str): # Split command (minus !command_name) into tokens tokens = message.split() region = tokens[0].lower() - allowed_globally = self.validate_sender("__global__", evt.sender) - allowed_region = self.validate_sender(region, evt.sender) - if allowed_globally or allowed_region: return region + # Each command must have a state/territory designation and a message + if len(tokens) < 2: return None - return None + self.log.debug(region) + + # This is a list of regions to process for this specific message + # This is only used to consider __global__ + regions_to_process = [] + if (self.validateSender("__global__", evt.sender)): + regions_to_process.append("__global__") + if (self.validateSender(region, evt.sender)): + regions_to_process.append(region) + + return regions_to_process # What gets called when !command_name message is sent @command.new(name=get_command_name, help="Report Something") @command.argument("message", pass_raw=True) async def report(self, evt: MessageEvent, message: str) -> None: - try: - # Check if sender is allowed for a region - region = self.validate_report(evt, message) - if region is None: return + # If all have passed + ntfy_posts_passed = None + + # Iterate through each endpoint that the message should be pushed to + # (if any) + for region in self.validateReport(evt, message): + # Detect no regions in reaction + if ntfy_posts_passed is None: ntfy_posts_passed = True - # Feed information + # Grab region-specific conrfig + region_config = self.config["region_configs"][region] + + # Create notification text split_message = message.split() - id = evt.event_id - timestamp = dt = datetime.datetime.fromtimestamp(evt.timestamp/1000) - self.log.debug(timestamp) - region = split_message[0] - message = ' '.join(split_message[1:]) + text = "[" + split_message[0] + "] " + ' '.join(message.split()[1:]) - # Insert into database - self.log.debug(await self.database.execute("INSERT INTO reports (id, timestamp, region, message) VALUES ($1, $2, $3, $4)", id, timestamp, region, message)) + # Build URL + url = region_config["server_url"] + "/" + region_config["server_topic"] - # Mark as inserted - await evt.react("👍") - except Exception as error: - self.log.fatal( f"An error occurred: {error}") - - @web.get("/feed.rss") - async def getfeed(self, req: Request) -> Response: - try: - # Get region from query - query = req.query - if not "region" in query: - return Response(text="Request must include ?region= query", status=405) - region = query["region"] + # Consider authentication + authentication = None + if region_config["server_use_authentication"]: + authentication = aiohttp.BasicAuth(region_config["server_username"], region_config["server_password"]) - # Check region against served regions - if not region in self.config["serve_regions"]: - return Response(text="Specified region not served on this consumer", status=404) - - if region == "__global__": - feed_items = await self.database.fetch("SELECT * FROM reports ORDER BY timestamp DESC LIMIT 50") - else: - feed_items = await self.database.fetch("SELECT * FROM reports where region=$1 ORDER BY timestamp DESC LIMIT 50", region) - self.log.debug(feed_items) - - # Build feed - feed = et.Element("rss") - feed.set("version", "2.0") - - # Channel metadata - channel = et.SubElement(feed, "channel") - et.SubElement(channel, "title").text = f"ICE reports {region}" - et.SubElement(channel, "description").text = f"ICE reports for {region}" - link = et.SubElement(channel, 'link') - link.text = self.config["feed_url"] - self_link = et.SubElement(channel, 'atom:link') - self_link.set('rel', 'self') - self_link.set('href', self.config["feed_url"]) - self_link.set('type', 'application/rss+xml') + # Send notification + async with self.http.post(url, data=text, auth=authentication) as response: + if not response.status == 200: + ntfy_posts_passed = False + + # Send reaction based on successful or failedPOSTs + if self.config["send_reaction"]: + if ntfy_posts_passed is True: + await evt.react("👍") + elif ntfy_posts_passed is False: + await evt.react("👎") - - # Add items - for item in feed_items: - # Data from feed - id = item["id"] - timestamp = item["timestamp"] - region = item["region"] - message = "[" + region + "] " + item["message"] - # Item - item_element = et.SubElement(channel, "item") - # GUID is different because the isPermaLink has to be false - guid = et.SubElement(item_element, 'guid') - guid.text = id - guid.set('isPermaLink', 'false') - # Other ite,s - et.SubElement(item_element, "title").text = message - et.SubElement(item_element, "description").text = message - et.SubElement(item_element, "pubDate").text = timestamp.strftime("%a, %d %b %Y %H:%M:%S +0000") - - - - # Return feed - return aiohttp.web.Response(body=et.tostring(feed, encoding="unicode"), content_type='application/rss+xml') - - except Exception as error: - self.log.fatal( f"An error occurred: {error}") - return Response(text="Internal server error", status=500) +# That's all, folks \ No newline at end of file diff --git a/maubot.yaml b/maubot.yaml index d1bcd26..59ce33b 100644 --- a/maubot.yaml +++ b/maubot.yaml @@ -1,13 +1,12 @@ maubot: 0.1.0 id: org.fiftyfiftyonearizona.reports.consumerrss -version: 1.2.0 +version: 1.1.0 license: MIT modules: - consumerrss main_class: ConsumerRSS config: true database: true -database_type: asyncpg webapp: true extra_files: - base-config.yaml \ No newline at end of file diff --git a/readme.md b/readme.md index 260aa8e..e23b655 100644 --- a/readme.md +++ b/readme.md @@ -2,14 +2,6 @@ Please see https://git.fiftyfiftyonearizona.org/webmaster/matrix-report-documentation -This plugin supports the following: +This plugin supports whitelisting state/territory designators, allowed senders, and reaction receipts. It also has separate RSS feeds for each reigon, and a global feed. -* whitelisting regions, or allowing all regions. -* allowed senders per region, or all regions. -* reaction receipt upon successful storage in database. -* Whitelisting which regions to both listen to and serve RSS feeds store. -* Configuring the feed's atom:link - -Please see [base-config.yaml](base-config.yaml) for explanation of behavior. - -The feed is fully valid and contains items with non-URL GUIDs. The title and body content are the same. Items are timestamped per the original Matrix event. \ No newline at end of file +Feeds will be available at `http://your.maubot.instance/_matrix/maubot/plugin//.rss`. where `reigon` is non-case-sensitive. Please see https://docs.mau.fi/maubot/dev/handlers/web.html to double check what this path should be exactly. \ No newline at end of file