From cbcf3624e1fae410651156b6b2d9e745065cd795 Mon Sep 17 00:00:00 2001 From: Frisk Date: Sun, 14 Jan 2024 14:24:51 +0100 Subject: [PATCH] Added buttons based on MarkusRost's contributions to RcGcDw project --- extensions/hooks/__init__.py | 1 + extensions/hooks/buttons.py | 77 ++++++++++++++++++++++++++++++++++++ src/api/context.py | 5 ++- src/config.py | 2 +- src/discussions.py | 2 +- src/misc.py | 12 ++++++ src/wiki.py | 28 +++++++------ 7 files changed, 110 insertions(+), 17 deletions(-) create mode 100644 extensions/hooks/buttons.py diff --git a/extensions/hooks/__init__.py b/extensions/hooks/__init__.py index 727931b..2fe1c00 100644 --- a/extensions/hooks/__init__.py +++ b/extensions/hooks/__init__.py @@ -16,3 +16,4 @@ #import extensions.hooks.example_hook #import extensions.hooks.usertalk #import extensions.hooks.edit_alerts +import extensions.hooks.buttons diff --git a/extensions/hooks/buttons.py b/extensions/hooks/buttons.py new file mode 100644 index 0000000..5c98be1 --- /dev/null +++ b/extensions/hooks/buttons.py @@ -0,0 +1,77 @@ +# This file is part of Recent changes Goat compatible Discord webhook (RcGcDw). +# +# RcGcDw is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# RcGcDw is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with RcGcDw. If not, see . +from typing import Optional + +from src.api.context import Context +from src.discord.message import DiscordMessage, DiscordMessageMetadata +from src.api.hook import post_hook + +# The webhook used for RcGcDb need to be controlled by a Discord application running https://github.com/Markus-Rost/rcgcdw-buttons +# You can use https://www.wikibot.de/interactions to create a Discord webhook to be used for RcGcDw that supports buttons. +# { +# "hooks": { +# "buttons": { +# "block": "Block user", +# "delete": "Delete", +# "filerevert": "Revert", +# "move": "Move back", +# "rollback": "Rollback", +# "undo": "Undo" +# } +# } +# } + + +def add_button(message: DiscordMessage, custom_id: str, label, style=2, emoji: Optional[dict] = None): + if len(custom_id) > 100 or not len(label): + return + if "components" not in message.webhook_object: + message.webhook_object["components"] = [{"type": 1, "components": []}] + if len(message.webhook_object["components"][-1]["components"]) >= 5: + message.webhook_object["components"].append({"type": 1, "components": []}) + message.webhook_object["components"][-1]["components"].append( + {"type": 2, "custom_id": custom_id, "style": style, "label": label, "emoji": emoji}) + + +@post_hook +def buttons_hook(message: DiscordMessage, metadata: DiscordMessageMetadata, context: Context, change: dict): + action_buttons = context.buttons or "" + if not len(action_buttons) or context.feed_type == "discussion": + return + BUTTON_PREFIX = context.client.WIKI_SCRIPT_PATH[len(context.client.WIKI_JUST_DOMAIN):] + if "block" in action_buttons and context.event != "suppressed": + add_button(message, + BUTTON_PREFIX + " block " + ("#" + str(change["userid"]) if change["userid"] else change["user"]), + context.gettext("Block user"), 4, {"id": None, "name": "🚧"}) + if context.feed_type != "recentchanges": + return + if "delete" in action_buttons and context.event in ("new", "upload/upload"): + add_button(message, BUTTON_PREFIX + " delete " + str(change["pageid"]), + context.gettext("Delete"), 4, {"id": None, "name": "🗑️"}) + # if "filerevert" in action_buttons and context.event in ("upload/overwrite", "upload/revert"): + # add_button(message, BUTTON_PREFIX + " file " + str(change["pageid"]) + " " + revision["archivename"].split("!")[0], + # action_buttons["filerevert"], 2, {"id": None, "name": "🔂"}) + if "move" in action_buttons and context.event in ("move/move", "move/move_redir"): + add_button(message, BUTTON_PREFIX + " move " + str(change["pageid"]) + " " + change["title"], + context.gettext("Move back"), 2, {"id": None, "name": "🔂"}) + if context.event != "edit": + return + if "rollback" in action_buttons: + add_button(message, BUTTON_PREFIX + " rollback " + str(change["pageid"]) + " " + ( + "#" + str(change["userid"]) if change["userid"] else change["user"]), + context.gettext("Rollback"), 1, {"id": None, "name": "🔁"}) + if "undo" in action_buttons: + add_button(message, BUTTON_PREFIX + " undo " + str(change["pageid"]) + " " + str(change["revid"]), + context.gettext("Undo"), 2, {"id": None, "name": "🔂"}) diff --git a/src/api/context.py b/src/api/context.py index ca9f2b6..bd56574 100644 --- a/src/api/context.py +++ b/src/api/context.py @@ -15,7 +15,7 @@ from __future__ import annotations import gettext -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from src.api.client import Client @@ -24,7 +24,7 @@ if TYPE_CHECKING: class Context: """Context object containing client and some metadata regarding specific formatter call, they are mainly used as a bridge between part that fetches the changes and API's formatters""" - def __init__(self, message_type: str, feed_type: str, webhook_urls: list[str], client: Client, language: gettext.GNUTranslations, settings: dict): + def __init__(self, message_type: str, feed_type: str, webhook_urls: list[str], client: Client, language: gettext.GNUTranslations, settings: dict, buttons: Optional[str]): self.client = client self.webhook_url = webhook_urls self.message_type = message_type @@ -39,6 +39,7 @@ class Context: self.pgettext = language.pgettext # Translation with context (ex. ctx.pgettext("From mediawiki module", "Blocked {} user")) self.npgettext = language.npgettext # Plural translation with context (ex. ctx.npgettext("From mediawiki module", "Edited {} time", "Edited {} times", edit_amoint) self.settings = settings + self.buttons = buttons def set_categories(self, cats): self.categories = cats diff --git a/src/config.py b/src/config.py index 0dad49d..cca038b 100644 --- a/src/config.py +++ b/src/config.py @@ -4,7 +4,7 @@ try: # load settings with open("settings.json", encoding="utf8") as sfile: settings = json.load(sfile) if "user-agent" in settings["header"]: - settings["header"]["user-agent"] = settings["header"]["user-agent"].format(version="1.9 Beta") # set the version in the useragent + settings["header"]["user-agent"] = settings["header"]["user-agent"].format(version="1.9.1 Beta") # set the version in the useragent except FileNotFoundError: logging.critical("No config file could be found. Please make sure settings.json is in the directory.") sys.exit(1) \ No newline at end of file diff --git a/src/discussions.py b/src/discussions.py index 59c24c2..edc8cd3 100644 --- a/src/discussions.py +++ b/src/discussions.py @@ -143,7 +143,7 @@ async def essential_feeds(change: dict, comment_pages: dict, wiki: Wiki, target: comment_page["fullUrl"] = "/".join(wiki.script_url.split("/", 3)[:3]) + comment_page["relativeUrl"] metadata = DiscordMessageMetadata("POST", rev_id=None, log_id=None, page_id=None) context = Context("embed" if target[0].display > 0 else "compact", "recentchanges", target[1], wiki.client, - langs[target[0].lang]["formatters"], prepare_settings(target[0].display)) + langs[target[0].lang]["formatters"], prepare_settings(target[0].display), "") context.set_comment_page(comment_page) discord_message: Optional[DiscordMessage] = None try: diff --git a/src/misc.py b/src/misc.py index 6fe3f11..56527e0 100644 --- a/src/misc.py +++ b/src/misc.py @@ -8,6 +8,7 @@ import base64, re import logging from typing import Callable from urllib.parse import urlparse, urlunparse +from src.config import settings logger = logging.getLogger("rcgcdw.misc") @@ -28,6 +29,17 @@ def get_domain(url: str) -> str: return ".".join(urlunparse((*parsed_url[0:2], "", "", "", "")).split(".")[-2:]) # something like gamepedia.com, fandom.com +def run_hooks(hooks, *arguments): + for hook in hooks: + try: + hook(*arguments) + except: + if settings.get("error_tolerance", 1) > 0: + logger.exception("On running a pre hook, ignoring pre-hook") + else: + raise + + class LinkParser(HTMLParser): new_string = "" diff --git a/src/wiki.py b/src/wiki.py index 858fffc..2475904 100644 --- a/src/wiki.py +++ b/src/wiki.py @@ -8,12 +8,12 @@ import asyncio import requests from src.api.util import default_message -from src.misc import prepare_settings +from src.misc import prepare_settings, run_hooks from src.discord.queue import messagequeue, QueueEntry from src.mw_messages import MWMessages from src.exceptions import * from src.queue_handler import dbmanager -from src.api.hooks import formatter_hooks +from src.api.hooks import formatter_hooks, pre_hooks, post_hooks from src.api.client import Client from src.api.context import Context from src.discord.message import DiscordMessage, DiscordMessageMetadata, StackedDiscordMessage @@ -25,7 +25,7 @@ from bs4 import BeautifulSoup from collections import OrderedDict, defaultdict, namedtuple from typing import Union, Optional, TYPE_CHECKING, List -Settings = namedtuple("Settings", ["lang", "display"]) +Settings = namedtuple("Settings", ["lang", "display", "buttons"]) logger = logging.getLogger("rcgcdb.wiki") # wiki_reamoval_reasons = {410: _("wiki deleted"), 404: _("wiki deleted"), 401: _("wiki inaccessible"), @@ -174,16 +174,15 @@ class Wiki: """This function generates all possible varations of outputs that we need to generate messages for. :returns defaultdict[namedtuple, list[str]] - where namedtuple is a named tuple with settings for given webhooks in list""" - Settings = namedtuple("Settings", ["lang", "display"]) target_settings: defaultdict[Settings, list[str]] = defaultdict(list) discussion_targets: defaultdict[Settings, list[str]] = defaultdict(list) - async for webhook in dbmanager.fetch_rows("SELECT webhook, lang, display, rcid, postid FROM rcgcdb WHERE wiki = $1", self.script_url): + async for webhook in dbmanager.fetch_rows("SELECT webhook, lang, display, rcid, postid, buttons FROM rcgcdb WHERE wiki = $1", self.script_url): if webhook['rcid'] == -1 and webhook['postid'] == '-1': await self.remove_wiki_from_db(4) if webhook['rcid'] != -1: - target_settings[Settings(webhook["lang"], webhook["display"])].append(webhook["webhook"]) + target_settings[Settings(webhook["lang"], webhook["display"], webhook["buttons"])].append(webhook["webhook"]) if webhook['postid'] != '-1': - discussion_targets[Settings(webhook["lang"], webhook["display"])].append(webhook["webhook"]) + discussion_targets[Settings(webhook["lang"], webhook["display"], webhook["buttons"])].append(webhook["webhook"]) self.rc_targets = target_settings self.discussion_targets = discussion_targets @@ -314,14 +313,14 @@ class Wiki: params = OrderedDict({"action": "query", "format": "json", "uselang": "content", "list": "tags|recentchanges", "meta": "allmessages|siteinfo", "utf8": 1, "tglimit": "max", "tgprop": "displayname", - "rcprop": "title|redirect|timestamp|ids|loginfo|parsedcomment|sizes|flags|tags|user", + "rcprop": "title|redirect|timestamp|ids|loginfo|parsedcomment|sizes|flags|tags|user|userid", "rclimit": amount, "rcshow": "!bot", "rctype": "edit|new|log|categorize", "ammessages": "recentchanges-page-added-to-category|recentchanges-page-removed-from-category|recentchanges-page-added-to-category-bundled|recentchanges-page-removed-from-category-bundled", "amenableparser": 1, "amincludelocal": 1, "siprop": "namespaces|general"}) else: params = OrderedDict({"action": "query", "format": "json", "uselang": "content", "list": "tags|recentchanges", "meta": "siteinfo", "utf8": 1, "rcshow": "!bot", - "rcprop": "title|redirect|timestamp|ids|loginfo|parsedcomment|sizes|flags|tags|user", + "rcprop": "title|redirect|timestamp|ids|loginfo|parsedcomment|sizes|flags|tags|user|userid", "rclimit": amount, "rctype": "edit|new|log|categorize", "siprop": "namespaces|general"}) try: response = await self.api_request(params=params) @@ -450,16 +449,18 @@ def process_cachable(response: dict, wiki_object: Wiki) -> None: wiki_object.recache_requested = False -async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, display_options: namedtuple("Settings", ["lang", "display"]), webhooks: list) -> Optional[DiscordMessage]: +async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, display_options: namedtuple("Settings", ["lang", "display", "buttons"]), webhooks: list) -> Optional[DiscordMessage]: """This function takes more vital information, communicates with a formatter and constructs DiscordMessage with it. It creates DiscordMessageMetadata object, LinkParser and Context. Prepares a comment """ from src.misc import LinkParser LinkParser = LinkParser(wiki.client.WIKI_JUST_DOMAIN) metadata = DiscordMessageMetadata("POST", rev_id=change.get("revid", None), log_id=change.get("logid", None), page_id=change.get("pageid", None), message_display=display_options.display) - context = Context("embed" if display_options.display > 0 else "compact", "recentchanges", webhooks, wiki.client, langs[display_options.lang]["formatters"], prepare_settings(display_options.display)) + context = Context("embed" if display_options.display > 0 else "compact", "recentchanges", webhooks, wiki.client, + langs[display_options.lang]["formatters"], prepare_settings(display_options.display), display_options.buttons) if ("actionhidden" in change or "suppressed" in change) and "suppressed" not in settings["ignored"]: # if event is hidden using suppression context.event = "suppressed" + run_hooks(pre_hooks, context, change) try: discord_message: Optional[DiscordMessage] = await asyncio.get_event_loop().run_in_executor( None, functools.partial(default_message("suppressed", context.message_type, formatter_hooks), context, change)) @@ -509,14 +510,14 @@ async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, displ wiki.delete_messages(dict(page_id=change.get("pageid"))) elif identification_string == "delete/event": logparams = change.get('logparams', {"ids": []}) - if settings["appearance"]["mode"] == "embed": + if context.message_type == "embed": wiki.redact_messages(context, logparams.get("ids", []), "log_id", logparams.get("new", {})) else: for logid in logparams.get("ids", []): wiki.delete_messages(dict(logid=logid)) elif identification_string == "delete/revision": logparams = change.get('logparams', {"ids": []}) - if settings["appearance"]["mode"] == "embed": + if context.message_type == "embed": wiki.redact_messages(context, logparams.get("ids", []), "rev_id", logparams.get("new", {})) if display_options.display == 3: wiki.redact_messages(context, wiki.find_middle_next(logparams.get("ids", []), change.get("pageid", -1)), "rev_id", @@ -524,6 +525,7 @@ async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, displ else: for revid in logparams.get("ids", []): wiki.delete_messages(dict(revid=revid)) + run_hooks(post_hooks, discord_message, metadata, context, change) if discord_message: # TODO How to react when none? (crash in formatter), probably bad handling atm discord_message.finish_embed() discord_message.metadata = metadata