More work, added db trigger

This commit is contained in:
Frisk 2022-07-26 15:48:44 +02:00
parent e15613246e
commit dd38334227
No known key found for this signature in database
GPG key ID: 213F7C15068AF8AC
11 changed files with 464 additions and 19 deletions

15
scripts/trigger.psql Normal file
View file

@ -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();

View file

@ -18,7 +18,7 @@ from src.misc import get_paths, get_domain
from src.msgqueue import messagequeue, send_to_discord from src.msgqueue import messagequeue, send_to_discord
from src.queue_handler import DBHandler from src.queue_handler import DBHandler
from src.wiki import Wiki, process_cats, process_mwmsgs, essential_info, essential_feeds 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.wiki_ratelimiter import RateLimiter
from src.irc_feed import AioIRCCat from src.irc_feed import AioIRCCat
from src.domain_manager import domains from src.domain_manager import domains
@ -233,6 +233,7 @@ async def main_loop():
logger.debug("Connection type: {}".format(db.connection_pool)) logger.debug("Connection type: {}".format(db.connection_pool))
await populate_wikis() await populate_wikis()
# START LISTENER CONNECTION # START LISTENER CONNECTION
# We are here
domains.run_all_domains() domains.run_all_domains()
try: try:
signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT) signals = (signal.SIGHUP, signal.SIGTERM, signal.SIGINT)

0
src/discord/__init__.py Normal file
View file

View file

@ -40,7 +40,7 @@ async def wiki_removal(wiki_url, status):
async def webhook_removal_monitor(webhook_url: str, reason: int): 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)) 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: class DiscordMessage:
"""A class defining a typical Discord JSON representation of webhook payload.""" """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): def __init__(self, message_type: str, event_type: str, webhook_url: list, wiki, content=None):

116
src/discord/message.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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

190
src/discord/queue.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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

114
src/discord/redaction.py Normal file
View file

@ -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 <http://www.gnu.org/licenses/>.
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)

View file

@ -17,7 +17,7 @@ class Domain:
def __init__(self, name: str): def __init__(self, name: str):
self.name = name # This should be always in format of topname.extension for example fandom.com self.name = name # This should be always in format of topname.extension for example fandom.com
self.task: Optional[asyncio.Task] = None 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.rate_limiter: src.wiki_ratelimiter = src.wiki_ratelimiter.RateLimiter()
self.irc: Optional[src.irc_feed.AioIRCCat] = None 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""" :parameter first (optional) - bool indicating if wikis should be added as first or last in the ordered dict"""
wiki.set_domain(self) wiki.set_domain(self)
if wiki.script_url in self.wikis: 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 self.wikis[wiki.script_url] = wiki
if first: if first:
self.wikis.move_to_end(wiki.script_url, last=False) self.wikis.move_to_end(wiki.script_url, last=False)
@ -88,7 +88,7 @@ class Domain:
async def regular_scheduler(self): async def regular_scheduler(self):
while True: 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 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 @cache
def calculate_sleep_time(self, queue_length: int): def calculate_sleep_time(self, queue_length: int):

View file

@ -1,9 +1,10 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING, Optional from typing import TYPE_CHECKING, Optional
from urllib.parse import urlparse, urlunparse from urllib.parse import urlparse, urlunparse
import logging
import asyncpg import asyncpg
from exceptions import NoDomain
from src.config import settings from src.config import settings
from src.domain import Domain from src.domain import Domain
from src.irc_feed import AioIRCCat from src.irc_feed import AioIRCCat
@ -12,6 +13,7 @@ from src.irc_feed import AioIRCCat
if TYPE_CHECKING: if TYPE_CHECKING:
from src.wiki import Wiki from src.wiki import Wiki
logger = logging.getLogger("rcgcdb.domain_manager")
class DomainManager: class DomainManager:
def __init__(self): def __init__(self):

View file

@ -54,3 +54,10 @@ class MediaWikiError(Exception):
self.message = f"MediaWiki returned the following errors: {errors}!" self.message = f"MediaWiki returned the following errors: {errors}!"
super().__init__(self.message) super().__init__(self.message)
class NoDomain(Exception):
"""When given domain does not exist"""
pass
class WikiExists(Exception):
"""When given wiki already exists"""
pass

View file

@ -21,7 +21,6 @@ from src.misc import parse_link
from src.i18n import langs from src.i18n import langs
from src.wiki_ratelimiter import RateLimiter from src.wiki_ratelimiter import RateLimiter
from statistics import Statistics, Log, LogType from statistics import Statistics, Log, LogType
import src.discord
import asyncio import asyncio
from src.config import settings from src.config import settings
# noinspection PyPackageRequirements # noinspection PyPackageRequirements
@ -266,21 +265,22 @@ def prepare_settings(display_mode: int) -> dict:
return template 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 from src.misc import LinkParser
LinkParser = LinkParser() LinkParser = LinkParser()
metadata = src.discord.DiscordMessageMetadata("POST", rev_id=change.get("revid", None), log_id=change.get("logid", None), metadata = discord.discord.DiscordMessageMetadata("POST", rev_id=change.get("revid", None), log_id=change.get("logid", None),
page_id=change.get("pageid", 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)) 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 if ("actionhidden" in change or "suppressed" in change) and "suppressed" not in settings["ignored"]: # if event is hidden using suppression
context.event = "suppressed" context.event = "suppressed"
try: 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: except NoFormatter:
return return
except: except:
if settings.get("error_tolerance", 1) > 0: 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: else:
raise raise
else: else:
@ -312,12 +312,12 @@ async def rc_processor(wiki: Wiki, change: dict, changed_categories: dict, displ
return return
context.event = identification_string context.event = identification_string
try: try:
discord_message: Optional[src.discord.DiscordMessage] = default_message(identification_string, formatter_hooks)(context, discord_message: Optional[discord.discord.DiscordMessage] = default_message(identification_string, formatter_hooks)(context,
change) change)
except: except:
if settings.get("error_tolerance", 1) > 0: if settings.get("error_tolerance", 1) > 0:
discord_message: Optional[ 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: else:
raise raise
if identification_string in ("delete/delete", "delete/delete_redir"): # TODO Move it into a hook? if identification_string in ("delete/delete", "delete/delete_redir"): # TODO Move it into a hook?
@ -429,8 +429,8 @@ class Wiki_old:
@staticmethod @staticmethod
async def remove(wiki_url, reason): async def remove(wiki_url, reason):
logger.info("Removing a wiki {}".format(wiki_url)) logger.info("Removing a wiki {}".format(wiki_url))
await src.discord.wiki_removal(wiki_url, reason) await discord.discord.wiki_removal(wiki_url, reason)
await src.discord.wiki_removal_monitor(wiki_url, reason) await discord.discord.wiki_removal_monitor(wiki_url, reason)
async with db.pool().acquire() as connection: async with db.pool().acquire() as connection:
result = await connection.execute('DELETE FROM rcgcdw WHERE wiki = $1', wiki_url) 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)) 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 # 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, 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.""" """Prepares essential information for both embed and compact message format."""
_ = langs[target[0][0]]["wiki"].gettext _ = langs[target[0][0]]["wiki"].gettext
changed_categories = changed_categories.get(change["revid"], None) 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) 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.""" """Prepares essential information for both embed and compact message format."""
appearance_mode = feeds_embed_formatter if target[0][1] > 0 else feeds_compact_formatter appearance_mode = feeds_embed_formatter if target[0][1] > 0 else feeds_compact_formatter
identification_string = change["_embedded"]["thread"][0]["containerType"] identification_string = change["_embedded"]["thread"][0]["containerType"]