mirror of
https://gitlab.com/chicken-riders/RcGcDb.git
synced 2025-02-23 00:54:09 +00:00
More work, added db trigger
This commit is contained in:
parent
e15613246e
commit
dd38334227
15
scripts/trigger.psql
Normal file
15
scripts/trigger.psql
Normal 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();
|
|
@ -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)
|
||||
|
|
0
src/discord/__init__.py
Normal file
0
src/discord/__init__.py
Normal file
|
@ -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):
|
116
src/discord/message.py
Normal file
116
src/discord/message.py
Normal 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
190
src/discord/queue.py
Normal 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
114
src/discord/redaction.py
Normal 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)
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
26
src/wiki.py
26
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"]
|
||||
|
|
Loading…
Reference in a new issue