From dd38334227cdf05323870a48b75d0dc3f4ae88f8 Mon Sep 17 00:00:00 2001 From: Frisk Date: Tue, 26 Jul 2022 15:48:44 +0200 Subject: [PATCH] More work, added db trigger --- scripts/trigger.psql | 15 +++ src/bot.py | 3 +- src/discord/__init__.py | 0 src/{ => discord}/discord.py | 2 +- src/discord/message.py | 116 +++++++++++++++++++++ src/discord/queue.py | 190 +++++++++++++++++++++++++++++++++++ src/discord/redaction.py | 114 +++++++++++++++++++++ src/domain.py | 6 +- src/domain_manager.py | 4 +- src/exceptions.py | 7 ++ src/wiki.py | 26 ++--- 11 files changed, 464 insertions(+), 19 deletions(-) create mode 100644 scripts/trigger.psql create mode 100644 src/discord/__init__.py rename src/{ => discord}/discord.py (98%) create mode 100644 src/discord/message.py create mode 100644 src/discord/queue.py create mode 100644 src/discord/redaction.py diff --git a/scripts/trigger.psql b/scripts/trigger.psql new file mode 100644 index 0000000..72dd3c3 --- /dev/null +++ b/scripts/trigger.psql @@ -0,0 +1,15 @@ +create or replace function public.webhook_notify() returns trigger as +$BODY$ +begin + IF (TG_OP = 'DELETE') THEN + perform pg_notify('webhookupdates', concat('REMOVE ', old.wiki)); + ELSIF (TG_OP = 'INSERT') then + perform pg_notify('webhookupdates', concat('ADD ', new.wiki)); + end if; + return new; +end; +$BODY$ + language plpgsql; + +CREATE TRIGGER RCGCDB_WEBHOOK_UPDATE BEFORE INSERT OR DELETE ON rcgcdw + FOR EACH ROW EXECUTE FUNCTION webhook_notify(); \ No newline at end of file diff --git a/src/bot.py b/src/bot.py index ac9a522..d67e975 100644 --- a/src/bot.py +++ b/src/bot.py @@ -18,7 +18,7 @@ from src.misc import get_paths, get_domain from src.msgqueue import messagequeue, send_to_discord from src.queue_handler import DBHandler from src.wiki import Wiki, process_cats, process_mwmsgs, essential_info, essential_feeds -from src.discord import DiscordMessage, generic_msg_sender_exception_logger, stack_message_list +from src.discord.discord import DiscordMessage, generic_msg_sender_exception_logger, stack_message_list from src.wiki_ratelimiter import RateLimiter from src.irc_feed import AioIRCCat from src.domain_manager import domains @@ -233,6 +233,7 @@ async def main_loop(): logger.debug("Connection type: {}".format(db.connection_pool)) await populate_wikis() # START LISTENER CONNECTION + # We are here domains.run_all_domains() try: signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) diff --git a/src/discord/__init__.py b/src/discord/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/discord.py b/src/discord/discord.py similarity index 98% rename from src/discord.py rename to src/discord/discord.py index db7ed3f..df86a9f 100644 --- a/src/discord.py +++ b/src/discord/discord.py @@ -40,7 +40,7 @@ async def wiki_removal(wiki_url, status): async def webhook_removal_monitor(webhook_url: str, reason: int): await send_to_discord_webhook_monitoring(DiscordMessage("compact", "webhook/remove", None, content="The webhook {} has been removed due to {}.".format("https://discord.com/api/webhooks/" + webhook_url, reason), wiki=None)) - +#What about Discord message that would hold all embeds in a list and only combine them when sending the webhook? Saving stacked message would be easier then class DiscordMessage: """A class defining a typical Discord JSON representation of webhook payload.""" def __init__(self, message_type: str, event_type: str, webhook_url: list, wiki, content=None): diff --git a/src/discord/message.py b/src/discord/message.py new file mode 100644 index 0000000..8f4fb16 --- /dev/null +++ b/src/discord/message.py @@ -0,0 +1,116 @@ +# 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 . + +import json +import math +import random +from collections import defaultdict + +from src.configloader import settings + + +class DiscordMessage: + """A class defining a typical Discord JSON representation of webhook payload.""" + def __init__(self, message_type: str, event_type: str, webhook_url: str, content=None): + self.webhook_object = dict(allowed_mentions={"parse": []}, avatar_url=settings["avatars"].get(message_type, "")) + self.webhook_url = webhook_url + + if message_type == "embed": + self.__setup_embed() + elif message_type == "compact": + if settings["event_appearance"].get(event_type, {"emoji": None})["emoji"]: + content = settings["event_appearance"][event_type]["emoji"] + " " + content + self.webhook_object["content"] = content + + self.message_type = message_type + self.event_type = event_type + + def __setitem__(self, key, value): + """Set item is used only in embeds.""" + try: + self.embed[key] = value + except NameError: + raise TypeError("Tried to assign a value when message type is plain message!") + + def __getitem__(self, item): + return self.embed[item] + + def __repr__(self): + """Return the Discord webhook object ready to be sent""" + return json.dumps(self.webhook_object) + + def __setup_embed(self): + self.embed = defaultdict(dict) + if "embeds" not in self.webhook_object: + self.webhook_object["embeds"] = [self.embed] + else: + self.webhook_object["embeds"].append(self.embed) + self.embed["color"] = None + + def add_embed(self): + self.finish_embed() + self.__setup_embed() + + def finish_embed(self): + if self.message_type != "embed": + return + if self.embed["color"] is None: + if settings["event_appearance"].get(self.event_type, {"color": None})["color"] is None: + self.embed["color"] = random.randrange(1, 16777215) + else: + self.embed["color"] = settings["event_appearance"][self.event_type]["color"] + else: + self.embed["color"] = math.floor(self.embed["color"]) + if not self.embed["author"].get("icon_url", None) and settings["event_appearance"].get(self.event_type, {"icon": None})["icon"]: + self.embed["author"]["icon_url"] = settings["event_appearance"][self.event_type]["icon"] + if len(self.embed["title"]) > 254: + self.embed["title"] = self.embed["title"][0:253] + "…" + + def set_author(self, name, url, icon_url=""): + self.embed["author"]["name"] = name + self.embed["author"]["url"] = url + self.embed["author"]["icon_url"] = icon_url + + def add_field(self, name, value, inline=False): + if "fields" not in self.embed: + self.embed["fields"] = [] + self.embed["fields"].append(dict(name=name, value=value, inline=inline)) + + def set_avatar(self, url): + self.webhook_object["avatar_url"] = url + + def set_name(self, name): + self.webhook_object["username"] = name + + def set_link(self, link): + self.embed["link"] = link + + +class DiscordMessageRaw(DiscordMessage): + def __init__(self, content: dict, webhook_url: str): + self.webhook_object = content + self.webhook_url = webhook_url + +class DiscordMessageMetadata: + def __init__(self, method, log_id = None, page_id = None, rev_id = None, webhook_url = None, new_data = None): + self.method = method + self.page_id = page_id + self.log_id = log_id + self.rev_id = rev_id + self.webhook_url = webhook_url + self.new_data = new_data + + def dump_ids(self) -> (int, int, int): + return self.page_id, self.rev_id, self.log_id diff --git a/src/discord/queue.py b/src/discord/queue.py new file mode 100644 index 0000000..771507c --- /dev/null +++ b/src/discord/queue.py @@ -0,0 +1,190 @@ +# 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 . + +import re +import sys +import time +import logging +from typing import Optional, Union, Tuple + +import requests + +from src.configloader import settings +from src.discord.message import DiscordMessage, DiscordMessageMetadata, DiscordMessageRaw + +AUTO_SUPPRESSION_ENABLED = settings.get("auto_suppression", {"enabled": False}).get("enabled") +if AUTO_SUPPRESSION_ENABLED: + from src.fileio.database import add_entry as add_message_redaction_entry + +rate_limit = 0 + +logger = logging.getLogger("rcgcdw.discord.queue") + +class MessageQueue: + """Message queue class for undelivered messages""" + def __init__(self): + self._queue: list[Tuple[Union[DiscordMessage, DiscordMessageRaw], DiscordMessageMetadata]] = [] + + def __repr__(self): + return self._queue + + def __len__(self): + return len(self._queue) + + def __iter__(self): + return iter(self._queue) + + def clear(self): + self._queue.clear() + + def add_message(self, message: Tuple[Union[DiscordMessage, DiscordMessageRaw], DiscordMessageMetadata]): + self._queue.append(message) + + def cut_messages(self, item_num: int): + self._queue = self._queue[item_num:] + + @staticmethod + def compare_message_to_dict(metadata: DiscordMessageMetadata, to_match: dict): + """Compare DiscordMessageMetadata fields and match them against dictionary""" + for name, val in to_match.items(): + if getattr(metadata, name, None) != val: + return False + return True + + def delete_all_with_matching_metadata(self, **properties): + """Deletes all of the messages that have matching metadata properties (useful for message redaction)""" + for index, item in reversed(list(enumerate(self._queue))): + if self.compare_message_to_dict(item[1], properties): + self._queue.pop(index) + + def resend_msgs(self): + if self._queue: + logger.info( + "{} messages waiting to be delivered to Discord due to Discord throwing errors/no connection to Discord servers.".format( + len(self._queue))) + for num, item in enumerate(self._queue): + logger.debug( + "Trying to send a message to Discord from the queue with id of {} and content {}".format(str(num), + str(item))) + if send_to_discord_webhook(item[0], metadata=item[1]) < 2: + logger.debug("Sending message succeeded") + else: + logger.debug("Sending message failed") + break + else: + self.clear() + logger.debug("Queue emptied, all messages delivered") + self.cut_messages(num) + logger.debug(self._queue) + + +messagequeue = MessageQueue() + + +def handle_discord_http(code, formatted_embed, result): + if 300 > code > 199: # message went through + return 0 + elif code == 400: # HTTP BAD REQUEST result.status_code, data, result, header + logger.error( + "Following message has been rejected by Discord, please submit a bug on our bugtracker adding it:") + logger.error(formatted_embed) + logger.error(result.text) + return 1 + elif code == 401 or code == 404: # HTTP UNAUTHORIZED AND NOT FOUND + if result.request.method == "POST": # Ignore not found for DELETE and PATCH requests since the message could already be removed by admin + logger.error("Webhook URL is invalid or no longer in use, please replace it with proper one.") + sys.exit(1) + else: + return 0 + elif code == 429: + logger.error("We are sending too many requests to the Discord, slowing down...") + return 2 + elif 499 < code < 600: + logger.error( + "Discord have trouble processing the event, and because the HTTP code returned is {} it means we blame them.".format( + code)) + return 3 + else: + logger.error("There was an unexpected HTTP code returned from Discord: {}".format(code)) + return 1 + + +def update_ratelimit(request): + """Updates rate limit time""" + global rate_limit + rate_limit = 0 if int(request.headers.get('x-ratelimit-remaining', "-1")) > 0 else int(request.headers.get( + 'x-ratelimit-reset-after', 0)) + rate_limit += settings.get("discord_message_cooldown", 0) + + +def send_to_discord_webhook(data: Optional[DiscordMessage], metadata: DiscordMessageMetadata): + global rate_limit + header = settings["header"] + header['Content-Type'] = 'application/json' + standard_args = dict(headers=header) + if metadata.method == "POST": + req = requests.Request("POST", data.webhook_url+"?wait=" + ("true" if AUTO_SUPPRESSION_ENABLED else "false"), data=repr(data), **standard_args) + elif metadata.method == "DELETE": + req = requests.Request("DELETE", metadata.webhook_url, **standard_args) + elif metadata.method == "PATCH": + req = requests.Request("PATCH", data.webhook_url, data=repr(data), **standard_args) + try: + time.sleep(rate_limit) + rate_limit = 0 + req = req.prepare() + result = requests.Session().send(req, timeout=10) + update_ratelimit(result) + if AUTO_SUPPRESSION_ENABLED and metadata.method == "POST": + if 199 < result.status_code < 300: # check if positive error log + try: + add_message_redaction_entry(*metadata.dump_ids(), repr(data), result.json().get("id")) + except ValueError: + logger.error("Couldn't get json of result of sending Discord message.") + else: + pass + except requests.exceptions.Timeout: + logger.warning("Timeouted while sending data to the webhook.") + return 3 + except requests.exceptions.ConnectionError: + logger.warning("Connection error while sending the data to a webhook") + return 3 + else: + return handle_discord_http(result.status_code, data, result) + + +def send_to_discord(data: Optional[DiscordMessage], meta: DiscordMessageMetadata): + if data is not None: + for regex in settings["disallow_regexes"]: + if data.webhook_object.get("content", None): + if re.search(re.compile(regex), data.webhook_object["content"]): + logger.info("Message {} has been rejected due to matching filter ({}).".format(data.webhook_object["content"], regex)) + return # discard the message without anything + else: + for to_check in [data.webhook_object.get("description", ""), data.webhook_object.get("title", ""), *[x["value"] for x in data["fields"]], data.webhook_object.get("author", {"name": ""}).get("name", "")]: + if re.search(re.compile(regex), to_check): + logger.info("Message \"{}\" has been rejected due to matching filter ({}).".format( + to_check, regex)) + return # discard the message without anything + if messagequeue: + messagequeue.add_message((data, meta)) + else: + code = send_to_discord_webhook(data, metadata=meta) + if code == 3: + messagequeue.add_message((data, meta)) + elif code == 2: + time.sleep(5.0) + messagequeue.add_message((data, meta)) + elif code is None or code < 2: + pass \ No newline at end of file diff --git a/src/discord/redaction.py b/src/discord/redaction.py new file mode 100644 index 0000000..8c70ae9 --- /dev/null +++ b/src/discord/redaction.py @@ -0,0 +1,114 @@ +# 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 . + +import logging +import json +from typing import List, Union + +from src.configloader import settings +from src.discord.message import DiscordMessageMetadata, DiscordMessageRaw +from src.discord.queue import send_to_discord, messagequeue +from src.fileio.database import db_cursor, db_connection +from src.i18n import redaction as redaction_translation + +logger = logging.getLogger("rcgcdw.discord.redaction") # TODO Figure out why does this logger do not work +_ = redaction_translation.gettext +#ngettext = redaction_translation.ngettext + + +def delete_messages(matching_data: dict): + """Delete messages that match given data""" + sql_conditions = "" + for key, value in matching_data.items(): + sql_conditions += "{} = ? AND".format(key) + else: + sql_conditions = sql_conditions[0:-4] # remove last AND statement + to_delete = db_cursor.execute("SELECT msg_id FROM event WHERE {CON}".format(CON=sql_conditions), list(matching_data.values())) + if len(messagequeue) > 0: + messagequeue.delete_all_with_matching_metadata(**matching_data) + msg_to_remove = [] + logger.debug("Deleting messages for data: {}".format(matching_data)) + for message in to_delete: + webhook_url = "{main_webhook}/messages/{message_id}".format(main_webhook=settings["webhookURL"], message_id=message[0]) + msg_to_remove.append(message[0]) + logger.debug("Removing following message: {}".format(message[0])) + send_to_discord(None, DiscordMessageMetadata("DELETE", webhook_url=webhook_url)) + for msg in msg_to_remove: + db_cursor.execute("DELETE FROM messages WHERE message_id = ?", (msg,)) + db_connection.commit() + + +def redact_messages(ids, entry_type: int, to_censor: dict): # : Union[List[Union[str, int]], set[Union[int, str]]] + """Redact past Discord messages + + ids: list of ints + entry_type: int - 0 for revdel, 1 for logdel + to_censor: dict - logparams of message parts to censor""" + for event_id in ids: + if entry_type == 0: + message = db_cursor.execute("SELECT content, message_id FROM messages INNER JOIN event ON event.msg_id = messages.message_id WHERE event.revid = ?;", (event_id, )) + else: + message = db_cursor.execute( + "SELECT content, message_id FROM messages INNER JOIN event ON event.msg_id = messages.message_id WHERE event.logid = ?;", + (event_id,)) + if settings["appearance"]["mode"] == "embed": + if message is not None: + row = message.fetchone() + try: + message = json.loads(row[0]) + new_embed = message["embeds"][0] + except ValueError: + logger.error("Couldn't loads JSON for message data. What happened? Data: {}".format(row[0])) + return + except TypeError: + logger.error("Couldn't find entry in the database for RevDel to censor information. This is probably because the script has been recently restarted or cache cleared.") + return + if "user" in to_censor and "url" in new_embed["author"]: + new_embed["author"]["name"] = _("hidden") + new_embed["author"].pop("url") + if "action" in to_censor and "url" in new_embed: + new_embed["title"] = _("~~hidden~~") + new_embed.pop("url") + if "content" in to_censor and "fields" in new_embed: + new_embed.pop("fields") + if "comment" in to_censor: + new_embed["description"] = _("~~hidden~~") + message["embeds"][0] = new_embed + db_cursor.execute("UPDATE messages SET content = ? WHERE message_id = ?;", (json.dumps(message), row[1],)) + db_connection.commit() + logger.debug(message) + send_to_discord(DiscordMessageRaw(message, settings["webhookURL"]+"/messages/"+str(row[1])), DiscordMessageMetadata("PATCH")) + else: + logger.debug("Could not find message in the database.") + + +def find_middle_next(ids: List[str], pageid: int) -> set: + """To address #235 RcGcDw should now remove diffs in next revs relative to redacted revs to protect information in revs that revert revdeleted information. + + :arg ids - list + :arg pageid - int + + :return list""" + ids = [int(x) for x in ids] + result = set() + ids.sort() # Just to be sure, sort the list to make sure it's always sorted + messages = db_cursor.execute("SELECT revid FROM event WHERE pageid = ? AND revid >= ? ORDER BY revid", (pageid, ids[0],)) + all_in_page = [x[0] for x in messages.fetchall()] + for id in ids: + try: + result.add(all_in_page[all_in_page.index(id)+1]) + except (KeyError, ValueError): + logger.debug(f"Value {id} not in {all_in_page} or no value after that.") + return result - set(ids) diff --git a/src/domain.py b/src/domain.py index 3fd7a5e..9575b89 100644 --- a/src/domain.py +++ b/src/domain.py @@ -17,7 +17,7 @@ class Domain: def __init__(self, name: str): self.name = name # This should be always in format of topname.extension for example fandom.com self.task: Optional[asyncio.Task] = None - self.wikis: OrderedDict[str, src.wiki.Wiki] = OrderedDict() + self.wikis: OrderedDict[str, src.wiki.Wiki] = OrderedDict() # TODO Check if we can replace with https://docs.python.org/3/library/asyncio-queue.html self.rate_limiter: src.wiki_ratelimiter = src.wiki_ratelimiter.RateLimiter() self.irc: Optional[src.irc_feed.AioIRCCat] = None @@ -56,7 +56,7 @@ class Domain: :parameter first (optional) - bool indicating if wikis should be added as first or last in the ordered dict""" wiki.set_domain(self) if wiki.script_url in self.wikis: - raise WikiExists("Wiki {} exists in domain {}".format(wiki.script_url, self.name)) + self.wikis[wiki.script_url].update_targets() self.wikis[wiki.script_url] = wiki if first: self.wikis.move_to_end(wiki.script_url, last=False) @@ -88,7 +88,7 @@ class Domain: async def regular_scheduler(self): while True: await asyncio.sleep(self.calculate_sleep_time(len(self))) # To make sure that we don't spam domains with one wiki every second we calculate a sane timeout for domains with few wikis - await self.run_wiki_scan(self.wikis.pop()) + await self.run_wiki_scan(next(iter(self.wikis.values()))) @cache def calculate_sleep_time(self, queue_length: int): diff --git a/src/domain_manager.py b/src/domain_manager.py index 5048e5d..3bd8112 100644 --- a/src/domain_manager.py +++ b/src/domain_manager.py @@ -1,9 +1,10 @@ from __future__ import annotations from typing import TYPE_CHECKING, Optional from urllib.parse import urlparse, urlunparse - +import logging import asyncpg +from exceptions import NoDomain from src.config import settings from src.domain import Domain from src.irc_feed import AioIRCCat @@ -12,6 +13,7 @@ from src.irc_feed import AioIRCCat if TYPE_CHECKING: from src.wiki import Wiki +logger = logging.getLogger("rcgcdb.domain_manager") class DomainManager: def __init__(self): diff --git a/src/exceptions.py b/src/exceptions.py index 69d83b1..70b2611 100644 --- a/src/exceptions.py +++ b/src/exceptions.py @@ -54,3 +54,10 @@ class MediaWikiError(Exception): self.message = f"MediaWiki returned the following errors: {errors}!" super().__init__(self.message) +class NoDomain(Exception): + """When given domain does not exist""" + pass + +class WikiExists(Exception): + """When given wiki already exists""" + pass \ No newline at end of file diff --git a/src/wiki.py b/src/wiki.py index 4b6faa4..b3ed5aa 100644 --- a/src/wiki.py +++ b/src/wiki.py @@ -21,7 +21,6 @@ from src.misc import parse_link from src.i18n import langs from src.wiki_ratelimiter import RateLimiter from statistics import Statistics, Log, LogType -import src.discord import asyncio from src.config import settings # noinspection PyPackageRequirements @@ -266,21 +265,22 @@ def prepare_settings(display_mode: int) -> dict: return template -async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, display_options: namedtuple("Settings", ["lang", "display"]), webhooks: list) -> tuple[src.discord.DiscordMessage, src.discord.DiscordMessageMetadata]: +async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, display_options: namedtuple("Settings", ["lang", "display"]), webhooks: list) -> tuple[ + discord.discord.DiscordMessage, discord.discord.DiscordMessageMetadata]: from src.misc import LinkParser LinkParser = LinkParser() - metadata = src.discord.DiscordMessageMetadata("POST", rev_id=change.get("revid", None), log_id=change.get("logid", None), - page_id=change.get("pageid", None)) + metadata = discord.discord.DiscordMessageMetadata("POST", rev_id=change.get("revid", None), log_id=change.get("logid", None), + page_id=change.get("pageid", None)) context = Context("embed" if display_options.display > 0 else "compact", "recentchanges", webhook, wiki.client, langs[display_options.lang]["rc_formatters"], prepare_settings(display_options.display)) if ("actionhidden" in change or "suppressed" in change) and "suppressed" not in settings["ignored"]: # if event is hidden using suppression context.event = "suppressed" try: - discord_message: Optional[src.discord.DiscordMessage] = default_message("suppressed", display_options.display, formatter_hooks)(context, change) + discord_message: Optional[discord.discord.DiscordMessage] = default_message("suppressed", display_options.display, formatter_hooks)(context, change) except NoFormatter: return except: if settings.get("error_tolerance", 1) > 0: - discord_message: Optional[src.discord.DiscordMessage] = None # It's handled by send_to_discord, we still want other code to run + discord_message: Optional[discord.discord.DiscordMessage] = None # It's handled by send_to_discord, we still want other code to run else: raise else: @@ -312,12 +312,12 @@ async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, displ return context.event = identification_string try: - discord_message: Optional[src.discord.DiscordMessage] = default_message(identification_string, formatter_hooks)(context, - change) + discord_message: Optional[discord.discord.DiscordMessage] = default_message(identification_string, formatter_hooks)(context, + change) except: if settings.get("error_tolerance", 1) > 0: discord_message: Optional[ - src.discord.DiscordMessage] = None # It's handled by send_to_discord, we still want other code to run + discord.discord.DiscordMessage] = None # It's handled by send_to_discord, we still want other code to run else: raise if identification_string in ("delete/delete", "delete/delete_redir"): # TODO Move it into a hook? @@ -429,8 +429,8 @@ class Wiki_old: @staticmethod async def remove(wiki_url, reason): logger.info("Removing a wiki {}".format(wiki_url)) - await src.discord.wiki_removal(wiki_url, reason) - await src.discord.wiki_removal_monitor(wiki_url, reason) + await discord.discord.wiki_removal(wiki_url, reason) + await discord.discord.wiki_removal_monitor(wiki_url, reason) async with db.pool().acquire() as connection: result = await connection.execute('DELETE FROM rcgcdw WHERE wiki = $1', wiki_url) logger.warning('{} rows affected by DELETE FROM rcgcdw WHERE wiki = "{}"'.format(result, wiki_url)) @@ -512,7 +512,7 @@ async def process_mwmsgs(wiki_response: dict, local_wiki: Wiki, mw_msgs: dict): # db_wiki: webhook, wiki, lang, display, rcid, postid async def essential_info(change: dict, changed_categories, local_wiki: Wiki, target: tuple, paths: tuple, request: dict, - rate_limiter: RateLimiter) -> src.discord.DiscordMessage: + rate_limiter: RateLimiter) -> discord.discord.DiscordMessage: """Prepares essential information for both embed and compact message format.""" _ = langs[target[0][0]]["wiki"].gettext changed_categories = changed_categories.get(change["revid"], None) @@ -546,7 +546,7 @@ async def essential_info(change: dict, changed_categories, local_wiki: Wiki, tar return await appearance_mode(identification_string, change, parsed_comment, changed_categories, local_wiki, target, paths, rate_limiter, additional_data=additional_data) -async def essential_feeds(change: dict, comment_pages: dict, db_wiki, target: tuple) -> src.discord.DiscordMessage: +async def essential_feeds(change: dict, comment_pages: dict, db_wiki, target: tuple) -> discord.discord.DiscordMessage: """Prepares essential information for both embed and compact message format.""" appearance_mode = feeds_embed_formatter if target[0][1] > 0 else feeds_compact_formatter identification_string = change["_embedded"]["thread"][0]["containerType"]