From fda8e6c25a440a2646fde32ac38f11b56c14ea4f Mon Sep 17 00:00:00 2001 From: Frisk Date: Sat, 30 Oct 2021 13:53:35 +0200 Subject: [PATCH] Added #24 however still not usable --- setup.py | 1 + src/discord/message.py | 6 ++++++ src/discord/queue.py | 12 ++++++++++-- src/rcgcdw.py | 42 ++++++++++++++++++++++++++++++++++++++---- 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 24fbb7e..9607937 100644 --- a/setup.py +++ b/setup.py @@ -12,5 +12,6 @@ setup( keywords=['MediaWiki', 'recent changes', 'Discord', 'webhook'], package_dir={"": "src"}, install_requires=["beautifulsoup4 >= 4.6.0", "requests >= 2.18.4", "schedule >= 0.5.0", "lxml >= 4.2.1"], + extras_require=["matplotlib => 3.4.3"], python_requires="3.7" ) diff --git a/src/discord/message.py b/src/discord/message.py index 8f4fb16..74d98bc 100644 --- a/src/discord/message.py +++ b/src/discord/message.py @@ -17,6 +17,8 @@ import json import math import random from collections import defaultdict +from typing import Tuple, Dict, Union +from io import BytesIO from src.configloader import settings @@ -36,6 +38,7 @@ class DiscordMessage: self.message_type = message_type self.event_type = event_type + self.files: Dict[str, Tuple[str, Union[BytesIO, str], str]] = {} def __setitem__(self, key, value): """Set item is used only in embeds.""" @@ -63,6 +66,9 @@ class DiscordMessage: self.finish_embed() self.__setup_embed() + def add_file(self, filename: str, content: Union[str, BytesIO]): + self.files[filename] = (None if isinstance(content, str) else filename, content, "application/json" if isinstance(content, str) else "image/" + filename.split(".")[1]) + def finish_embed(self): if self.message_type != "embed": return diff --git a/src/discord/queue.py b/src/discord/queue.py index 9777287..bd1f844 100644 --- a/src/discord/queue.py +++ b/src/discord/queue.py @@ -129,10 +129,18 @@ def update_ratelimit(request): def send_to_discord_webhook(data: Optional[DiscordMessage], metadata: DiscordMessageMetadata): global rate_limit header = settings["header"] - header['Content-Type'] = 'application/json' + if data.files: + header['Content-Type'] = 'multipart/form-data' + else: + 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) + if data.files: + data.add_file("payload_json", repr(data)) + logger.debug(data.files) + req = requests.Request("POST", data.webhook_url+"?wait=" + ("true" if AUTO_SUPPRESSION_ENABLED else "false"), files=data.files, **standard_args) + else: + 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": diff --git a/src/rcgcdw.py b/src/rcgcdw.py index 612fc95..db750d5 100644 --- a/src/rcgcdw.py +++ b/src/rcgcdw.py @@ -18,13 +18,13 @@ # WARNING! SHITTY CODE AHEAD. ENTER ONLY IF YOU ARE SURE YOU CAN TAKE IT # You have been warned - -import time, logging.config, requests, datetime, math, os.path, schedule, sys, re, importlib +import io +import time, logging.config, requests, datetime, math, os.path, schedule, sys, importlib import src.misc from collections import defaultdict, Counter, OrderedDict -from typing import Optional +from typing import Optional, List import src.api.client from src.api.context import Context from src.api.hooks import formatter_hooks, pre_hooks, post_hooks @@ -42,15 +42,23 @@ ngettext = rcgcdw.ngettext TESTING = True if "--test" in sys.argv else False # debug mode, pipeline testing AUTO_SUPPRESSION_ENABLED = settings.get("auto_suppression", {"enabled": False}).get("enabled") +PLOTTING_ENABLED = settings.get("event_appearance", {}).get("daily_overview", {}).get("plot", False) if AUTO_SUPPRESSION_ENABLED: from src.discord.redaction import delete_messages, redact_messages + # Prepare logging logging.config.dictConfig(settings["logging"]) logger = logging.getLogger("rcgcdw") logger.debug("Current settings: {settings}".format(settings=settings)) +if PLOTTING_ENABLED: + try: + import matplotlib.pyplot as plt + from matplotlib.ticker import MultipleLocator + except ImportError: + logger.error("matplotlib is not installed. Install matplotlib with pip install matplotlib if you want to use plots in daily overview.") def load_extensions(): """Loads all of the extensions, can be a local import because all we need is them to register""" @@ -121,6 +129,27 @@ def daily_overview_sync(data: dict) -> dict: return data_output +def overview_plot_generation(hours: defaultdict[int, int]) -> io.BytesIO: + """Generates plot image using matplotlib""" + output = io.BytesIO() + current_hour, axes_data = datetime.datetime.utcnow().hour, [[], []] + for __ in range(24): + axes_data[0].append(current_hour) + axes_data[1].append(hours.get(current_hour, 0)) + current_hour += 1 + if current_hour > 23: + current_hour = 0 + fig, ax = plt.subplots() + plt.bar(axes_data[0], axes_data[1]) + plt.xlabel(_('Hour (UTC)')) + plt.ylabel(_('Actions')) + ax.set_xlim(-0.5, 23.5) + ax.set_xticklabels([str(axes_data[0][0])] + [str(x) for x in axes_data[0]]) + ax.xaxis.set_major_locator(MultipleLocator(1)) + plt.savefig(output, format="png") + return output + + def day_overview(): try: result = day_overview_request() @@ -146,7 +175,7 @@ def day_overview(): if "actionhidden" in item or "suppressed" in item or "userhidden" in item: continue # while such actions have type value (edit/new/log) many other values are hidden and therefore can crash with key error, let's not process such events activity = add_to_dict(activity, item["user"]) - hours = add_to_dict(hours, datetime.datetime.strptime(item["timestamp"], "%Y-%m-%dT%H:%M:%SZ").hour) + hours = add_to_dict(hours, datetime.datetime.strptime(item["timestamp"], "%Y-%m-%dT%H:%M:%SZ").hour) # Edits can overlap between days at same hour if overview executes in non full hour, but let's ignore that if item["type"] == "edit": edits += 1 changed_bytes += item["newlen"] - item["oldlen"] @@ -190,6 +219,11 @@ def day_overview(): ) for name, value in fields: embed.add_field(name, value, inline=True) + if PLOTTING_ENABLED: + file = overview_plot_generation(hours) + file_name = "_" + datetime.datetime.now().strftime("%Y-%m-%d_%H:%M") + "_wiki_overview.png" + embed.add_file(file_name, file) + embed["image"]["url"] = "attachment://" + file_name embed.finish_embed() send_to_discord(embed, meta=DiscordMessageMetadata("POST"))