From 7a643a3079fc8c8e6fe85f23fe2f5d869fb3fad5 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Wed, 6 Mar 2024 20:47:06 +0000 Subject: [PATCH 01/32] Take snip into account --- src/cogs/ytdl.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/src/cogs/ytdl.py b/src/cogs/ytdl.py index 13463df..42888f3 100644 --- a/src/cogs/ytdl.py +++ b/src/cogs/ytdl.py @@ -82,23 +82,34 @@ class YTDLCog(commands.Cog): await db.commit() return - async def save_link(self, message: discord.Message, webpage_url: str, format_id: str, attachment_index: int = 0): + async def save_link( + self, + message: discord.Message, + webpage_url: str, + format_id: str, + attachment_index: int = 0, + *, + snip: typing.Optional[str] = None + ): """ Saves a link to discord to prevent having to re-download it. :param message: The download message with the attachment. :param webpage_url: The "webpage_url" key of the metadata :param format_id: The "format_Id" key of the metadata :param attachment_index: The index of the attachment. Defaults to 0 + :param snip: The start and end time to snip the video. e.g. 00:00:00-00:10:00 :return: The created hash key """ + snip = snip or '*' await self._init_db() async with aiosqlite.connect("./data/ytdl.db") as db: - _hash = hashlib.md5(f"{webpage_url}:{format_id}".encode()).hexdigest() + _hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest() self.log.debug( - "Saving %r (%r:%r) with message %d>%d, index %d", + "Saving %r (%r:%r:%r) with message %d>%d, index %d", _hash, webpage_url, format_id, + snip, message.channel.id, message.id, attachment_index @@ -117,20 +128,27 @@ class YTDLCog(commands.Cog): await db.commit() return _hash - async def get_saved(self, webpage_url: str, format_id: str) -> typing.Optional[str]: + async def get_saved( + self, + webpage_url: str, + format_id: str, + snip: str + ) -> typing.Optional[str]: """ Attempts to retrieve the attachment URL of a previously saved download. :param webpage_url: The webpage url :param format_id: The format ID + :param snip: The start and end time to snip the video. e.g. 00:00:00-00:10:00 :return: the URL, if found and valid. """ await self._init_db() async with aiosqlite.connect("./data/ytdl.db") as db: - _hash = hashlib.md5(f"{webpage_url}:{format_id}".encode()).hexdigest() + _hash = hashlib.md5(f"{webpage_url}:{format_id}:{snip}".encode()).hexdigest() self.log.debug( - "Attempting to find a saved download for '%s:%s' (%r).", + "Attempting to find a saved download for '%s:%s:%s' (%r).", webpage_url, format_id, + snip, _hash ) cursor = await db.execute( @@ -160,7 +178,7 @@ class YTDLCog(commands.Cog): except IndexError: self.log.debug("Attachment index %d is out of range (%r)", attachment_index, message.attachments) return - + def convert_to_m4a(self, file: Path) -> Path: """ Converts a file to m4a format. @@ -229,7 +247,6 @@ class YTDLCog(commands.Cog): snip: typing.Annotated[ typing.Optional[str], discord.Option( - str, description="A start and end position to trim. e.g. 00:00:00-00:10:00.", required=False ) @@ -347,7 +364,7 @@ class YTDLCog(commands.Cog): colour=self.colours.get(domain, discord.Colour.og_blurple()) ).set_footer(text="Downloading (step 2/10)").set_thumbnail(url=thumbnail_url) ) - previous = await self.get_saved(webpage_url, extracted_info["format_id"]) + previous = await self.get_saved(webpage_url, extracted_info["format_id"], snip or '*') if previous: await ctx.edit( content=previous, @@ -467,7 +484,7 @@ class YTDLCog(commands.Cog): ) ) file = new_file - + if audio_only and file.suffix != ".m4a": self.log.info("Converting %r to m4a.", file) file = await asyncio.to_thread(self.convert_to_m4a, file) @@ -505,7 +522,7 @@ class YTDLCog(commands.Cog): url=webpage_url ) ) - await self.save_link(msg, webpage_url, chosen_format_id) + await self.save_link(msg, webpage_url, chosen_format_id, snip=snip or '*') except discord.HTTPException as e: self.log.error(e, exc_info=True) return await ctx.edit( From ec28e01bebd0f797161bfd3063d460597e0f1e11 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 11 Mar 2024 17:43:28 +0000 Subject: [PATCH 02/32] Add what-are-matthews-bank-details-for-the-80th-time command --- .gitignore | 3 ++- src/cogs/net.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 676323b..abc05f8 100644 --- a/.gitignore +++ b/.gitignore @@ -310,4 +310,5 @@ pyrightconfig.json # End of https://www.toptal.com/developers/gitignore/api/python,pycharm,visualstudiocode cookies.txt config.toml -chrome/ \ No newline at end of file +chrome/ +src/assets/sensitive/* \ No newline at end of file diff --git a/src/cogs/net.py b/src/cogs/net.py index 2d0af08..061594d 100644 --- a/src/cogs/net.py +++ b/src/cogs/net.py @@ -242,6 +242,16 @@ class NetworkCog(commands.Cog): paginator.add_line(f"Error: {e}") for page in paginator.pages: await ctx.respond(page) + + @commands.slash_command(name="what-are-matthews-bank-details-for-the-80th-time") + async def matthew_bank(self, ctx: discord.ApplicationContext): + """For the 80th time""" + f = Path.cwd() / "assets" / "sensitive" / "matthew-bank.webp" + if not f.exists(): + return await ctx.respond("Idk") + else: + await ctx.defer() + await ctx.respond(file=discord.File(f)) def setup(bot): From 8c8993d3e8baa7e9a909ef341b30136ebff174e0 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 11 Mar 2024 17:45:05 +0000 Subject: [PATCH 03/32] Fix missing import --- src/cogs/net.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cogs/net.py b/src/cogs/net.py index 061594d..14fa88b 100644 --- a/src/cogs/net.py +++ b/src/cogs/net.py @@ -4,6 +4,7 @@ import os import re import time import typing +from pathlib import Path import discord from discord.ext import commands From 7cd2032de9654408350c7d9a3033aece182e9d2b Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 11 Mar 2024 17:48:04 +0000 Subject: [PATCH 04/32] fuck you 32 char limit --- src/cogs/net.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cogs/net.py b/src/cogs/net.py index 14fa88b..7716193 100644 --- a/src/cogs/net.py +++ b/src/cogs/net.py @@ -244,7 +244,7 @@ class NetworkCog(commands.Cog): for page in paginator.pages: await ctx.respond(page) - @commands.slash_command(name="what-are-matthews-bank-details-for-the-80th-time") + @commands.slash_command(name="what-are-matthews-bank-details") async def matthew_bank(self, ctx: discord.ApplicationContext): """For the 80th time""" f = Path.cwd() / "assets" / "sensitive" / "matthew-bank.webp" From 273404a61542935a852232c2296aff31d50c9f55 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sun, 17 Mar 2024 16:08:14 +0000 Subject: [PATCH 05/32] Allow using proxy in /screenshot --- src/cogs/screenshot.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cogs/screenshot.py b/src/cogs/screenshot.py index b04801a..4b3ca7c 100644 --- a/src/cogs/screenshot.py +++ b/src/cogs/screenshot.py @@ -5,6 +5,7 @@ import logging import os import tempfile import time +import copy from urllib.parse import urlparse import discord @@ -76,7 +77,8 @@ class ScreenshotCog(commands.Cog): load_timeout: int = 10, render_timeout: int = None, eager: bool = None, - resolution: str = "1920x1080" + resolution: str = "1920x1080", + use_proxy: bool = False ): """Screenshots a webpage.""" await ctx.defer() @@ -104,11 +106,14 @@ class ScreenshotCog(commands.Cog): start_init = time.time() try: + options = copy.copy(self.chrome_options) + if use_proxy: + options.add_argument("--proxy-server=http://127.0.0.1:8888") service = await asyncio.to_thread(ChromeService) driver: webdriver.Chrome = await asyncio.to_thread( webdriver.Chrome, service=service, - options=self.chrome_options + options=options ) driver.set_page_load_timeout(load_timeout) if resolution: From 8ef909c33da2f0036ac16b5ca1ef53feece4fba1 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sun, 17 Mar 2024 16:15:59 +0000 Subject: [PATCH 06/32] Properly allow configuring screenshot proxy --- src/cogs/screenshot.py | 6 ++++-- src/conf.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cogs/screenshot.py b/src/cogs/screenshot.py index 4b3ca7c..0f28b24 100644 --- a/src/cogs/screenshot.py +++ b/src/cogs/screenshot.py @@ -16,6 +16,8 @@ from selenium import webdriver from selenium.webdriver.chrome.options import Options as ChromeOptions from selenium.webdriver.chrome.service import Service as ChromeService +from conf import CONFIG + class ScreenshotCog(commands.Cog): def __init__(self, bot: commands.Bot): @@ -107,8 +109,8 @@ class ScreenshotCog(commands.Cog): start_init = time.time() try: options = copy.copy(self.chrome_options) - if use_proxy: - options.add_argument("--proxy-server=http://127.0.0.1:8888") + if use_proxy and (server := CONFIG["screenshot"].get("proxy")): + options.add_argument("--proxy-server=" + server) service = await asyncio.to_thread(ChromeService) driver: webdriver.Chrome = await asyncio.to_thread( webdriver.Chrome, diff --git a/src/conf.py b/src/conf.py index 88bd862..41ee4c1 100644 --- a/src/conf.py +++ b/src/conf.py @@ -28,6 +28,7 @@ try: CONFIG.setdefault("jimmy", {}) CONFIG.setdefault("ollama", {}) CONFIG.setdefault("rss", {"meta": {"channel": None}}) + CONFIG.setdefault("screenshot", {}) CONFIG.setdefault( "server", { From bc16f3258cbdb0ac722abb70dc7f876bb451ae8a Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 18 Mar 2024 23:57:13 +0000 Subject: [PATCH 07/32] Add quota cog --- requirements.txt | 1 + src/cogs/quote_quota.py | 118 ++++++++++++++++++++++++++++++++++++++++ src/cogs/screenshot.py | 1 + src/main.py | 2 +- 4 files changed, 121 insertions(+), 1 deletion(-) create mode 100644 src/cogs/quote_quota.py diff --git a/requirements.txt b/requirements.txt index da18c0e..0163e13 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,3 +18,4 @@ humanize~=4.9 redis~=5.0 beautifulsoup4~=4.12 lxml~=5.1 +matplotlib~=3.8 diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py new file mode 100644 index 0000000..4a3c228 --- /dev/null +++ b/src/cogs/quote_quota.py @@ -0,0 +1,118 @@ +import asyncio +import re + +import discord +import io +import matplotlib.pyplot as plt +from datetime import timedelta +from discord.ext import commands +from typing import Iterable, Annotated + +from conf import CONFIG + + +class QuoteQuota(commands.Cog): + def __init__(self, bot): + self.bot = bot + self.quotes_channel_id = CONFIG["quote_a"].get("channel_id") + + @property + def quotes_channel(self) -> discord.TextChannel | None: + if self.quotes_channel_id: + c = self.bot.get_channel(self.quotes_channel_id) + if c: + return c + + @staticmethod + def generate_pie_chart( + usernames: Iterable[str], + counts: Iterable[int], + ) -> discord.File: + """ + Converts the given username and count tuples into a nice pretty pie chart. + + :param usernames: The usernames + :param counts: The number of times the username appears in the chat + :returns: The pie chart image + """ + fig, ax = plt.subplots() + ax.pie( + counts, + labels=usernames, + autopct='%1.1f%%', + ) + ax.legend(loc="upper right") + fig.tight_layout() + fig.title("Quote Quota") + fio = io.BytesIO() + fig.savefig(fio, format='jpg') + fio.seek(0) + return discord.File(fio, filename="pie.jpeg") + + @commands.slash_command() + async def quota( + self, + ctx: discord.ApplicationContext, + days: Annotated[ + int, + discord.Option( + int, + name="lookback", + description="How many days to look back on. Defaults to 7.", + default=7, + min_value=1, + max_value=365 + ) + ] + ): + """Checks the quote quota for the quotes channel.""" + now = discord.utils.utcnow() + oldest = now - timedelta(days=7) + await ctx.defer() + channel = self.quotes_channel + if not channel: + return await ctx.respond(":x: Cannot find quotes channel.") + + await ctx.respond("Gathering messages, this may take a moment.") + + authors = {} + filtered_messages = 0 + total = 0 + async for message in channel.history( + limit=None, + after=oldest, + oldest_first=False + ): + total += 1 + if not message.content: + filtered_messages += 1 + continue + if message.attachments: + regex = r".*\s*-\s*(\w+)" + else: + regex = r".+\s*-\s*(\w+)" + + if not (m := re.match(regex, message.content)): + filtered_messages += 1 + continue + name = m.group(1) + name = name.strip().title() + authors.setdefault(name, 0) + authors[name] += 1 + + file = await asyncio.to_thread( + self.generate_pie_chart, + list(authors.keys()), + list(authors.values()) + ) + return await ctx.edit( + content="{:,} messages (out of {:,}) were filtered (didn't follow format?)".format( + filtered_messages, + total + ), + file=file + ) + + +def setup(bot): + bot.add_cog(QuoteQuota(bot)) diff --git a/src/cogs/screenshot.py b/src/cogs/screenshot.py index 0f28b24..9b8d518 100644 --- a/src/cogs/screenshot.py +++ b/src/cogs/screenshot.py @@ -168,6 +168,7 @@ class ScreenshotCog(commands.Cog): end_save = time.time() if len(await asyncio.to_thread(file.getvalue)) > 24 * 1024 * 1024: + await ctx.edit(content="Compressing screenshot...") start_compress = time.time() file = await asyncio.to_thread(self.compress_png, file) fn = "screenshot.webp" diff --git a/src/main.py b/src/main.py index 82947c0..ef61146 100644 --- a/src/main.py +++ b/src/main.py @@ -133,7 +133,7 @@ bot = Client( debug_guilds=CONFIG["jimmy"].get("debug_guilds") ) -for ext in ("ytdl", "net", "screenshot", "ollama", "ffmeta"): +for ext in ("ytdl", "net", "screenshot", "ollama", "ffmeta", "quote_quota"): try: bot.load_extension(f"cogs.{ext}") except discord.ExtensionError as e: From b76160d57a2eebb4386d679d100bd14c7ab0b494 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Mon, 18 Mar 2024 23:59:46 +0000 Subject: [PATCH 08/32] Fix quote_a config not being defaulted --- src/cogs/quote_quota.py | 4 ++-- src/conf.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 4a3c228..d61ad50 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -67,9 +67,9 @@ class QuoteQuota(commands.Cog): ): """Checks the quote quota for the quotes channel.""" now = discord.utils.utcnow() - oldest = now - timedelta(days=7) + oldest = now - timedelta(days=days) await ctx.defer() - channel = self.quotes_channel + channel = self.quotes_channel or discord.utils.get(ctx.guild.text_channels, name="quotes") if not channel: return await ctx.respond(":x: Cannot find quotes channel.") diff --git a/src/conf.py b/src/conf.py index 41ee4c1..e260f8d 100644 --- a/src/conf.py +++ b/src/conf.py @@ -29,6 +29,7 @@ try: CONFIG.setdefault("ollama", {}) CONFIG.setdefault("rss", {"meta": {"channel": None}}) CONFIG.setdefault("screenshot", {}) + CONFIG.setdefault("quote_a", {"channel": None}) CONFIG.setdefault( "server", { From 580a6dc30583cd10c015f660e2bab22ff47fca91 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:01:20 +0000 Subject: [PATCH 09/32] I hate MPL --- src/cogs/quote_quota.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index d61ad50..0d3c5e3 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -43,7 +43,6 @@ class QuoteQuota(commands.Cog): ) ax.legend(loc="upper right") fig.tight_layout() - fig.title("Quote Quota") fio = io.BytesIO() fig.savefig(fio, format='jpg') fio.seek(0) From 777669c09b206790beb6f518b5bc1e8566be179a Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:02:41 +0000 Subject: [PATCH 10/32] Forget the tight layout --- src/cogs/quote_quota.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 0d3c5e3..e47a148 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -42,7 +42,6 @@ class QuoteQuota(commands.Cog): autopct='%1.1f%%', ) ax.legend(loc="upper right") - fig.tight_layout() fio = io.BytesIO() fig.savefig(fio, format='jpg') fio.seek(0) From bb71a681c84ca85d6f234fba7a8a16cf668bcf60 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:06:12 +0000 Subject: [PATCH 11/32] Filter out "me" and channel/mentions --- src/cogs/quote_quota.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index e47a148..42240ef 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -86,15 +86,18 @@ class QuoteQuota(commands.Cog): filtered_messages += 1 continue if message.attachments: - regex = r".*\s*-\s*(\w+)" + regex = r".*\s+-\s*@?([\w\s]+)" else: - regex = r".+\s*-\s*(\w+)" + regex = r".+\s*-\s*@?([\w\s]+)" - if not (m := re.match(regex, message.content)): + if not (m := re.match(regex, str(message.clean_content))): filtered_messages += 1 continue name = m.group(1) name = name.strip().title() + if name == "Me": + filtered_messages += 1 + continue authors.setdefault(name, 0) authors[name] += 1 From 834ab86ccacd1a8cd080a5306a996523125b689e Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:13:34 +0000 Subject: [PATCH 12/32] formatting! --- src/cogs/quote_quota.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 42240ef..95541d4 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -39,11 +39,10 @@ class QuoteQuota(commands.Cog): ax.pie( counts, labels=usernames, - autopct='%1.1f%%', + autopct='%1.1f%% (%d)', ) - ax.legend(loc="upper right") fio = io.BytesIO() - fig.savefig(fio, format='jpg') + fig.savefig(fio, format='jpg', bbox_inches='tight') fio.seek(0) return discord.File(fio, filename="pie.jpeg") From 0e5b4914939724e96e194376bfe46977e16c2492 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:14:50 +0000 Subject: [PATCH 13/32] fix formatting! --- src/cogs/quote_quota.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 95541d4..c1e7d44 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -35,11 +35,15 @@ class QuoteQuota(commands.Cog): :param counts: The number of times the username appears in the chat :returns: The pie chart image """ + + def pct(v: int): + return f"{v / sum(counts) * 100:.1f}% ({v})" + fig, ax = plt.subplots() ax.pie( counts, labels=usernames, - autopct='%1.1f%% (%d)', + autopct=pct, ) fio = io.BytesIO() fig.savefig(fio, format='jpg', bbox_inches='tight') From b5d05722d11ff0877c98a436e9a831e3a1a9405c Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:16:19 +0000 Subject: [PATCH 14/32] fix formatting? --- src/cogs/quote_quota.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index c1e7d44..5e01883 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -37,7 +37,7 @@ class QuoteQuota(commands.Cog): """ def pct(v: int): - return f"{v / sum(counts) * 100:.1f}% ({v})" + return f"{v:.1f}% ({(v / 100) * sum(counts):0f})" fig, ax = plt.subplots() ax.pie( From 451d9ba77e369a100ee5795a842ef3b78b7d1e5b Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:20:22 +0000 Subject: [PATCH 15/32] Improve filtering and add "other" --- src/cogs/quote_quota.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 5e01883..d5e14f6 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -12,6 +12,7 @@ from conf import CONFIG class QuoteQuota(commands.Cog): + def __init__(self, bot): self.bot = bot self.quotes_channel_id = CONFIG["quote_a"].get("channel_id") @@ -25,8 +26,8 @@ class QuoteQuota(commands.Cog): @staticmethod def generate_pie_chart( - usernames: Iterable[str], - counts: Iterable[int], + usernames: list[str], + counts: list[int], ) -> discord.File: """ Converts the given username and count tuples into a nice pretty pie chart. @@ -39,6 +40,19 @@ class QuoteQuota(commands.Cog): def pct(v: int): return f"{v:.1f}% ({(v / 100) * sum(counts):0f})" + other = [] + # Any authors with less than 5% of the total count will be grouped into "other" + for i, author in enumerate(usernames.copy()): + if (c := counts[i]) / sum(counts) < 0.05: + other.append(c) + counts[i] = -1 + usernames.remove(author) + if other: + usernames.append("Other") + counts.append(sum(other)) + # And now filter out any -1% counts + counts = [c for c in counts if c != -1] + fig, ax = plt.subplots() ax.pie( counts, @@ -46,7 +60,7 @@ class QuoteQuota(commands.Cog): autopct=pct, ) fio = io.BytesIO() - fig.savefig(fio, format='jpg', bbox_inches='tight') + fig.savefig(fio, format='jpg') fio.seek(0) return discord.File(fio, filename="pie.jpeg") From faf8996a7b31725ff3c51cbde7b06bf7b737388f Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:25:45 +0000 Subject: [PATCH 16/32] Increase pie size, add name mapping --- src/cogs/quote_quota.py | 55 +++++++++++++++++++++++++++++------------ 1 file changed, 39 insertions(+), 16 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index d5e14f6..b74eb35 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -16,6 +16,7 @@ class QuoteQuota(commands.Cog): def __init__(self, bot): self.bot = bot self.quotes_channel_id = CONFIG["quote_a"].get("channel_id") + self.names = CONFIG["quote_a"].get("names", {}) @property def quotes_channel(self) -> discord.TextChannel | None: @@ -28,36 +29,41 @@ class QuoteQuota(commands.Cog): def generate_pie_chart( usernames: list[str], counts: list[int], + no_other: bool = False ) -> discord.File: """ Converts the given username and count tuples into a nice pretty pie chart. :param usernames: The usernames :param counts: The number of times the username appears in the chat + :param no_other: Disables the "other" grouping :returns: The pie chart image """ def pct(v: int): - return f"{v:.1f}% ({(v / 100) * sum(counts):0f})" + return f"{v:.1f}% ({round((v / 100) * sum(counts))})" - other = [] - # Any authors with less than 5% of the total count will be grouped into "other" - for i, author in enumerate(usernames.copy()): - if (c := counts[i]) / sum(counts) < 0.05: - other.append(c) - counts[i] = -1 - usernames.remove(author) - if other: - usernames.append("Other") - counts.append(sum(other)) - # And now filter out any -1% counts - counts = [c for c in counts if c != -1] + if no_other is False: + other = [] + # Any authors with less than 5% of the total count will be grouped into "other" + for i, author in enumerate(usernames.copy()): + if (c := counts[i]) / sum(counts) < 0.05: + other.append(c) + counts[i] = -1 + usernames.remove(author) + if other: + usernames.append("Other") + counts.append(sum(other)) + # And now filter out any -1% counts + counts = [c for c in counts if c != -1] fig, ax = plt.subplots() ax.pie( counts, labels=usernames, autopct=pct, + startangle=90, + radius=2 ) fio = io.BytesIO() fig.savefig(fio, format='jpg') @@ -78,6 +84,15 @@ class QuoteQuota(commands.Cog): min_value=1, max_value=365 ) + ], + merge_other: Annotated[ + bool, + discord.Option( + bool, + name="merge_other", + description="Whether to merge authors with less than 5% of the total count into 'Other'.", + default=True + ) ] ): """Checks the quote quota for the quotes channel.""" @@ -113,15 +128,23 @@ class QuoteQuota(commands.Cog): name = m.group(1) name = name.strip().title() if name == "Me": - filtered_messages += 1 - continue + name = message.author.name.strip().casefold() + if name in self.names: + name = self.names[name] + else: + filtered_messages += 1 + continue + elif name in self.names: + name = self.names[name] + authors.setdefault(name, 0) authors[name] += 1 file = await asyncio.to_thread( self.generate_pie_chart, list(authors.keys()), - list(authors.values()) + list(authors.values()), + merge_other ) return await ctx.edit( content="{:,} messages (out of {:,}) were filtered (didn't follow format?)".format( From 8f6fd219407cef88a562f20e4211bf90e9109e79 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:29:21 +0000 Subject: [PATCH 17/32] idk AI told me to do this --- src/cogs/quote_quota.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index b74eb35..8977f93 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -66,6 +66,7 @@ class QuoteQuota(commands.Cog): radius=2 ) fio = io.BytesIO() + plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9) fig.savefig(fio, format='jpg') fio.seek(0) return discord.File(fio, filename="pie.jpeg") From ca1d531ad6954f789f4ae4533432c71aea7e4cce Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:30:45 +0000 Subject: [PATCH 18/32] again, just fit in the window please --- src/cogs/quote_quota.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 8977f93..7c69e95 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -63,7 +63,8 @@ class QuoteQuota(commands.Cog): labels=usernames, autopct=pct, startangle=90, - radius=2 + radius=1.5, + figsize=(10, 10) ) fio = io.BytesIO() plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9) From c53dd9172934d02fb6dc016bda07e041bfeadfe0 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:32:20 +0000 Subject: [PATCH 19/32] again, just fit in the window please please --- src/cogs/quote_quota.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 7c69e95..ff1c184 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -57,20 +57,19 @@ class QuoteQuota(commands.Cog): # And now filter out any -1% counts counts = [c for c in counts if c != -1] - fig, ax = plt.subplots() + fig, ax = plt.subplots(figsize=(10, 10)) ax.pie( counts, labels=usernames, autopct=pct, startangle=90, radius=1.5, - figsize=(10, 10) ) fio = io.BytesIO() plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9) - fig.savefig(fio, format='jpg') + fig.savefig(fio, format='png') fio.seek(0) - return discord.File(fio, filename="pie.jpeg") + return discord.File(fio, filename="pie.png") @commands.slash_command() async def quota( From 8519a3278b0259e074850d7e826b6ee8bcc2d362 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:32:50 +0000 Subject: [PATCH 20/32] Too big --- src/cogs/quote_quota.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index ff1c184..d93ade6 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -57,13 +57,13 @@ class QuoteQuota(commands.Cog): # And now filter out any -1% counts counts = [c for c in counts if c != -1] - fig, ax = plt.subplots(figsize=(10, 10)) + fig, ax = plt.subplots(figsize=(7, 7)) ax.pie( counts, labels=usernames, autopct=pct, startangle=90, - radius=1.5, + radius=1.3, ) fio = io.BytesIO() plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9) From f226a0ac4105f3023388d3f827a84cbe506b8645 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:37:06 +0000 Subject: [PATCH 21/32] Fix filtering even more --- src/cogs/quote_quota.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index d93ade6..dc28936 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -119,9 +119,9 @@ class QuoteQuota(commands.Cog): filtered_messages += 1 continue if message.attachments: - regex = r".*\s+-\s*@?([\w\s]+)" + regex = r".*\s*-\s*@?([\w\s]+)" else: - regex = r".+\s*-\s*@?([\w\s]+)" + regex = r".+\s+-\s*@?([\w\s]+)" if not (m := re.match(regex, str(message.clean_content))): filtered_messages += 1 @@ -137,6 +137,9 @@ class QuoteQuota(commands.Cog): continue elif name in self.names: name = self.names[name] + elif name.isdigit(): + filtered_messages += 1 + continue authors.setdefault(name, 0) authors[name] += 1 From 17b947f0e50f20be5f012911097d834401b4cc58 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:39:55 +0000 Subject: [PATCH 22/32] honestly what am I even doing with pie charts now --- src/cogs/quote_quota.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index dc28936..80695e8 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -63,10 +63,10 @@ class QuoteQuota(commands.Cog): labels=usernames, autopct=pct, startangle=90, - radius=1.3, + radius=1.2, ) + fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.3, hspace=0.4) fio = io.BytesIO() - plt.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9) fig.savefig(fio, format='png') fio.seek(0) return discord.File(fio, filename="pie.png") From 13c2acf43fb0b602527d410c8538dea8603a0979 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:42:04 +0000 Subject: [PATCH 23/32] Fix names not properly applying --- src/cogs/quote_quota.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 80695e8..eadbfa9 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -131,12 +131,12 @@ class QuoteQuota(commands.Cog): if name == "Me": name = message.author.name.strip().casefold() if name in self.names: - name = self.names[name] + name = self.names[name].title() else: filtered_messages += 1 continue - elif name in self.names: - name = self.names[name] + elif name.strip().casefold() in self.names: + name = self.names[name].title() elif name.isdigit(): filtered_messages += 1 continue From 328e5d38773703c14cc16051b1fb7f6bfb4f8113 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:43:23 +0000 Subject: [PATCH 24/32] Fix name raising error --- src/cogs/quote_quota.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index eadbfa9..a630ef6 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -127,20 +127,21 @@ class QuoteQuota(commands.Cog): filtered_messages += 1 continue name = m.group(1) - name = name.strip().title() - if name == "Me": + name = name.strip().casefold() + if name == "me": name = message.author.name.strip().casefold() if name in self.names: name = self.names[name].title() else: filtered_messages += 1 continue - elif name.strip().casefold() in self.names: + elif name in self.names: name = self.names[name].title() elif name.isdigit(): filtered_messages += 1 continue + name = name.title() authors.setdefault(name, 0) authors[name] += 1 From 916ba0ab42d5ddb8b6db503aa0433c05110fef75 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:46:10 +0000 Subject: [PATCH 25/32] Handle no messages errors --- src/cogs/quote_quota.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index a630ef6..6d93fbd 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -145,6 +145,18 @@ class QuoteQuota(commands.Cog): authors.setdefault(name, 0) authors[name] += 1 + if not authors: + if total: + return await ctx.edit( + content="No valid messages found in the last {!s} days. " + "Make sure quotes are formatted properly ending with ` - AuthorName`" + " (e.g. `\"This is my quote\" - Jimmy`)".format(days) + ) + else: + return await ctx.edit( + content="No messages found in the last {!s} days.".format(days) + ) + file = await asyncio.to_thread( self.generate_pie_chart, list(authors.keys()), From 4f001c885eb6769e0895ae9c65b58f038ad93d06 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Tue, 19 Mar 2024 00:48:24 +0000 Subject: [PATCH 26/32] Sort pie chart values --- src/cogs/quote_quota.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/cogs/quote_quota.py b/src/cogs/quote_quota.py index 6d93fbd..b48cabc 100644 --- a/src/cogs/quote_quota.py +++ b/src/cogs/quote_quota.py @@ -57,6 +57,18 @@ class QuoteQuota(commands.Cog): # And now filter out any -1% counts counts = [c for c in counts if c != -1] + mapping = {} + for i, author in enumerate(usernames): + mapping[author] = counts[i] + + # Sort the authors by count + new_mapping = {} + for author, count in sorted(mapping.items(), key=lambda x: x[1], reverse=True): + new_mapping[author] = count + + usernames = list(new_mapping.keys()) + counts = list(new_mapping.values()) + fig, ax = plt.subplots(figsize=(7, 7)) ax.pie( counts, From 50c648a6187108bef36035491206feb0e6156833 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 22 Mar 2024 09:08:03 +0000 Subject: [PATCH 27/32] Allow using system prompt and large user prompt --- src/cogs/ollama.py | 67 +++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/src/cogs/ollama.py b/src/cogs/ollama.py index 7828bf9..e5a40b3 100644 --- a/src/cogs/ollama.py +++ b/src/cogs/ollama.py @@ -8,6 +8,7 @@ import typing import base64 import io import redis +from discord import Interaction from discord.ui import View, button from fnmatch import fnmatch @@ -89,7 +90,7 @@ class ChatHistory: "threads:" + thread_id, json.dumps(self._internal[thread_id]) ) - def create_thread(self, member: discord.Member) -> str: + def create_thread(self, member: discord.Member, default: str | None = None) -> str: """ Creates a thread, returns its ID. """ @@ -100,7 +101,7 @@ class ChatHistory: "messages": [] } with open("./assets/ollama-prompt.txt") as file: - system_prompt = file.read() + system_prompt = default or file.read() self.add_message( key, "system", @@ -190,6 +191,32 @@ class ChatHistory: SERVER_KEYS = list(CONFIG["ollama"].keys()) +class OllamaGetPrompt(discord.ui.Modal): + + def __init__(self, ctx: discord.ApplicationContext, prompt_type: str = "User"): + super().__init__( + discord.ui.InputText( + style=discord.InputTextStyle.long, + label="%s prompt" % prompt_type, + placeholder="Enter your prompt here.", + ), + timeout=300, + title="Ollama %s prompt" % prompt_type, + ) + self.ctx = ctx + self.prompt_type = prompt_type + self.value = None + + async def interaction_check(self, interaction: discord.Interaction) -> bool: + return interaction.user == self.ctx.user + + async def callback(self, interaction: Interaction): + await interaction.response.defer() + self.ctx.interaction = interaction + self.value = self.children[0].value + self.stop() + + class Ollama(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot @@ -286,7 +313,28 @@ class Ollama(commands.Cog): if not self.history.get_thread(context): await ctx.respond("Invalid context key.") return - await ctx.defer() + + if query.startswith("$$"): + prompt = OllamaGetPrompt(ctx, "System") + await ctx.send_modal(prompt) + await prompt.wait() + system_query = prompt.value + if not system_query: + return await ctx.respond("No prompt provided. Aborting.") + else: + system_query = None + if query == "$": + prompt = OllamaGetPrompt(ctx) + await ctx.send_modal(prompt) + await prompt.wait() + query = prompt.value + if not query: + return await ctx.respond("No prompt provided. Aborting.") + + try: + await ctx.defer() + except discord.HTTPException: + pass model = model.casefold() try: @@ -294,7 +342,7 @@ class Ollama(commands.Cog): model = model + ":" + tag self.log.debug("Model %r already has a tag", model) except ValueError: - model = model + ":latest" + model += ":latest" self.log.debug("Resolved model to %r" % model) if image: @@ -315,7 +363,7 @@ class Ollama(commands.Cog): data = io.BytesIO() await image.save(data) data.seek(0) - image_data = base64.b64encode(data.read()).decode("utf-8") + image_data = base64.b64encode(data.read()).decode() else: image_data = None @@ -336,7 +384,12 @@ class Ollama(commands.Cog): async with aiohttp.ClientSession( base_url=server_config["base_url"], - timeout=aiohttp.ClientTimeout(0) + timeout=aiohttp.ClientTimeout( + connect=30, + sock_read=10800, + sock_connect=30, + total=10830 + ) ) as session: embed = discord.Embed( title="Checking server...", @@ -482,7 +535,7 @@ class Ollama(commands.Cog): self.log.debug("Beginning to generate response with key %r.", key) if context is None: - context = self.history.create_thread(ctx.user) + context = self.history.create_thread(ctx.user, system_query) elif context is not None and self.history.get_thread(context) is None: __thread = self.history.find_thread(context) if not __thread: From 3e0152ee24992618c187457652fd8bb112471e19 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 22 Mar 2024 09:14:52 +0000 Subject: [PATCH 28/32] Use a view instead --- src/cogs/ollama.py | 58 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/src/cogs/ollama.py b/src/cogs/ollama.py index e5a40b3..3d0b767 100644 --- a/src/cogs/ollama.py +++ b/src/cogs/ollama.py @@ -217,6 +217,41 @@ class OllamaGetPrompt(discord.ui.Modal): self.stop() +class PromptSelector(discord.ui.View): + def __init__(self, ctx: discord.ApplicationContext): + super().__init__(timeout=600, disable_on_timeout=True) + self.ctx = ctx + self.system_prompt = None + self.user_prompt = None + + async def interaction_check(self, interaction: Interaction) -> bool: + return interaction.user == self.ctx.user + + def update_ui(self): + if self.system_prompt is not None: + self.get_item("sys").style = discord.ButtonStyle.secondary # type: ignore + if self.user_prompt is not None: + self.get_item("usr").style = discord.ButtonStyle.secondary # type: ignore + + @discord.ui.button(label="Set System Prompt", style=discord.ButtonStyle.primary, custom_id="sys") + async def set_system_prompt(self, btn: discord.ui.Button, interaction: Interaction): + modal = OllamaGetPrompt(self.ctx, "System") + await interaction.response.send_modal(modal) + await modal.wait() + self.system_prompt = modal.value + + @discord.ui.button(label="Set System Prompt", style=discord.ButtonStyle.primary, custom_id="usr") + async def set_system_prompt(self, btn: discord.ui.Button, interaction: Interaction): + modal = OllamaGetPrompt(self.ctx) + await interaction.response.send_modal(modal) + await modal.wait() + self.user_prompt = modal.value + + @discord.ui.button(label="Done", style=discord.ButtonStyle.success, custom_id="done") + async def done(self, btn: discord.ui.Button, interaction: Interaction): + self.stop() + + class Ollama(commands.Cog): def __init__(self, bot: commands.Bot): self.bot = bot @@ -314,28 +349,17 @@ class Ollama(commands.Cog): await ctx.respond("Invalid context key.") return - if query.startswith("$$"): - prompt = OllamaGetPrompt(ctx, "System") - await ctx.send_modal(prompt) - await prompt.wait() - system_query = prompt.value - if not system_query: - return await ctx.respond("No prompt provided. Aborting.") - else: - system_query = None - if query == "$": - prompt = OllamaGetPrompt(ctx) - await ctx.send_modal(prompt) - await prompt.wait() - query = prompt.value - if not query: - return await ctx.respond("No prompt provided. Aborting.") - try: await ctx.defer() except discord.HTTPException: pass + if query == "$": + v = PromptSelector(ctx) + await ctx.respond("Select edit your prompts, as desired. Click done when you want to continue.", view=v) + await v.wait() + query = v.user_prompt or query + model = model.casefold() try: model, tag = model.split(":", 1) From 54e6076a9d351cbf3759adbbfb5f6b8334e1582c Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 22 Mar 2024 09:15:45 +0000 Subject: [PATCH 29/32] Fix view --- src/cogs/ollama.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/cogs/ollama.py b/src/cogs/ollama.py index 3d0b767..bdd9c98 100644 --- a/src/cogs/ollama.py +++ b/src/cogs/ollama.py @@ -241,7 +241,7 @@ class PromptSelector(discord.ui.View): self.system_prompt = modal.value @discord.ui.button(label="Set System Prompt", style=discord.ButtonStyle.primary, custom_id="usr") - async def set_system_prompt(self, btn: discord.ui.Button, interaction: Interaction): + async def set_user_prompt(self, btn: discord.ui.Button, interaction: Interaction): modal = OllamaGetPrompt(self.ctx) await interaction.response.send_modal(modal) await modal.wait() @@ -344,6 +344,7 @@ class Ollama(commands.Cog): ) ] ): + system_query = None if context is not None: if not self.history.get_thread(context): await ctx.respond("Invalid context key.") @@ -359,6 +360,7 @@ class Ollama(commands.Cog): await ctx.respond("Select edit your prompts, as desired. Click done when you want to continue.", view=v) await v.wait() query = v.user_prompt or query + system_query = v.system_prompt model = model.casefold() try: From b2013d2759506a2c9f63e193d5e0add53addcde3 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 22 Mar 2024 09:16:38 +0000 Subject: [PATCH 30/32] Fix view not updating --- src/cogs/ollama.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/cogs/ollama.py b/src/cogs/ollama.py index bdd9c98..4ffe789 100644 --- a/src/cogs/ollama.py +++ b/src/cogs/ollama.py @@ -239,13 +239,17 @@ class PromptSelector(discord.ui.View): await interaction.response.send_modal(modal) await modal.wait() self.system_prompt = modal.value + self.update_ui() + await interaction.edit_original_response(view=self) - @discord.ui.button(label="Set System Prompt", style=discord.ButtonStyle.primary, custom_id="usr") + @discord.ui.button(label="Set User Prompt", style=discord.ButtonStyle.primary, custom_id="usr") async def set_user_prompt(self, btn: discord.ui.Button, interaction: Interaction): modal = OllamaGetPrompt(self.ctx) await interaction.response.send_modal(modal) await modal.wait() self.user_prompt = modal.value + self.update_ui() + await interaction.edit_original_response(view=self) @discord.ui.button(label="Done", style=discord.ButtonStyle.success, custom_id="done") async def done(self, btn: discord.ui.Button, interaction: Interaction): From b199477a029db49b2740ee682e324b11fd945af4 Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Fri, 22 Mar 2024 09:18:08 +0000 Subject: [PATCH 31/32] Fix the modals --- src/cogs/ollama.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cogs/ollama.py b/src/cogs/ollama.py index 4ffe789..36dad41 100644 --- a/src/cogs/ollama.py +++ b/src/cogs/ollama.py @@ -212,7 +212,6 @@ class OllamaGetPrompt(discord.ui.Modal): async def callback(self, interaction: Interaction): await interaction.response.defer() - self.ctx.interaction = interaction self.value = self.children[0].value self.stop() @@ -365,6 +364,7 @@ class Ollama(commands.Cog): await v.wait() query = v.user_prompt or query system_query = v.system_prompt + await ctx.delete(delay=0.1) model = model.casefold() try: From 5da071d6a5dd64a0fc16970024d52f29f6640e1f Mon Sep 17 00:00:00 2001 From: nexy7574 Date: Sun, 31 Mar 2024 18:57:04 +0100 Subject: [PATCH 32/32] Remove references to web.py --- src/main.py | 20 +------ src/web.py | 160 ---------------------------------------------------- 2 files changed, 3 insertions(+), 177 deletions(-) delete mode 100644 src/web.py diff --git a/src/main.py b/src/main.py index ef61146..fcb6318 100644 --- a/src/main.py +++ b/src/main.py @@ -10,7 +10,6 @@ import random import httpx import uvicorn -from web import app from logging import FileHandler import discord @@ -104,25 +103,12 @@ class Client(commands.Bot): CONFIG["jimmy"].get("uptime_kuma_interval", 60.0) ) self.uptime_thread.start() - app.state.bot = self - config = uvicorn.Config( - app, - host=CONFIG["server"].get("host", "0.0.0.0"), - port=CONFIG["server"].get("port", 8080), - loop="asyncio", - lifespan="on", - server_header=False - ) - server = uvicorn.Server(config=config) - self.web = self.loop.create_task(asyncio.to_thread(server.serve())) await super().start(token, reconnect=reconnect) async def close(self) -> None: - if self.web: - self.web.cancel() - if self.thread: - self.thread.kill.set() - await asyncio.get_event_loop().run_in_executor(None, self.thread.join) + if self.uptime_thread: + self.uptime_thread.kill.set() + await asyncio.get_event_loop().run_in_executor(None, self.uptime_thread.join) await super().close() diff --git a/src/web.py b/src/web.py deleted file mode 100644 index a2ec382..0000000 --- a/src/web.py +++ /dev/null @@ -1,160 +0,0 @@ -import asyncio -import datetime -import logging -import textwrap - -import psutil -import time -import pydantic -from typing import Optional, Any -from conf import CONFIG -import discord -from discord.ext.commands import Paginator - -from fastapi import FastAPI, HTTPException, status, WebSocketException, WebSocket, WebSocketDisconnect, Header - -class BridgeResponse(pydantic.BaseModel): - status: str - pages: list[str] - - -class BridgePayload(pydantic.BaseModel): - secret: str - message: str - sender: str - - -class MessagePayload(pydantic.BaseModel): - class MessageAttachmentPayload(pydantic.BaseModel): - url: str - proxy_url: str - filename: str - size: int - width: Optional[int] = None - height: Optional[int] = None - content_type: str - ATTACHMENT: Optional[Any] = None - - event_type: Optional[str] = "create" - message_id: int - author: str - is_automated: bool = False - avatar: str - content: str - clean_content: str - at: float - attachments: list[MessageAttachmentPayload] = [] - reply_to: Optional["MessagePayload"] = None - - -app = FastAPI( - title="JimmyAPI", - version="2.0.0a1" -) -log = logging.getLogger("jimmy.web.api") -app.state.bot = None -app.state.bridge_lock = asyncio.Lock() -app.state.last_sender_ts = 0 - - -@app.get("/ping") -def ping(): - """Checks the bot is online and provides some uptime information""" - if not app.state.bot: - raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE) - return { - "ping": "pong", - "online": app.state.bot.is_ready(), - "latency": max(round(app.state.bot.latency, 2), 0.01), - "uptime": round(time.time() - psutil.Process().create_time()), - "uptime.sys": time.time() - psutil.boot_time() - } - - -@app.post("/bridge", status_code=201) -async def bridge_post_send_message(body: BridgePayload): - """Sends a message FROM matrix TO discord.""" - now = datetime.datetime.now(datetime.timezone.utc) - ts_diff = (now - app.state.last_sender_ts).total_seconds() - if not app.state.bot: - raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE) - - if body.secret != CONFIG["jimmy"].get("token"): - log.warning("Authentication failure: %s was not authenticated.", body.secret) - raise HTTPException(status.HTTP_401_UNAUTHORIZED) - - channel = app.state.bot.get_channel(CONFIG["server"]["channel"]) - if not channel or not channel.can_send(): - log.warning("Unable to send message: channel not found or not writable.") - raise HTTPException(status.HTTP_503_SERVICE_UNAVAILABLE) - - if len(body.message) > 4000: - log.warning( - "Unable to send message: message too long ({:,} characters long, 4000 max).".format(len(body.message)) - ) - raise HTTPException(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE) - - paginator = Paginator(prefix="", suffix="", max_size=1990) - for line in body["message"].splitlines(): - try: - paginator.add_line(line) - except ValueError: - paginator.add_line(textwrap.shorten(line, width=1900, placeholder="<...>")) - - if len(paginator.pages) > 1: - msg = None - if app.state.last_sender != body["sender"] or ts_diff >= 600: - msg = await channel.send(f"**{body['sender']}**:") - m = len(paginator.pages) - for n, page in enumerate(paginator.pages, 1): - await channel.send( - f"[{n}/{m}]\n>>> {page}", - allowed_mentions=discord.AllowedMentions.none(), - reference=msg, - silent=True, - suppress=n != m, - ) - app.state.last_sender = body["sender"] - else: - content = f"**{body['sender']}**:\n>>> {body['message']}" - if app.state.last_sender == body["sender"] and ts_diff < 600: - content = f">>> {body['message']}" - await channel.send(content, allowed_mentions=discord.AllowedMentions.none(), silent=True, suppress=False) - app.state.last_sender = body["sender"] - app.state.last_sender_ts = now - return {"status": "ok", "pages": len(paginator.pages)} - - -@app.websocket("/bridge/recv") -async def bridge_recv(ws: WebSocket, secret: str = Header(None)): - await ws.accept() - log.info("Websocket %s:%s accepted.", ws.client.host, ws.client.port) - if secret != app.state.bot.http.token: - log.warning("Closing websocket %r, invalid secret.", ws.client.host) - raise WebSocketException(code=1008, reason="Invalid Secret") - if app.state.ws_connected.locked(): - log.warning("Closing websocket %r, already connected." % ws) - raise WebSocketException(code=1008, reason="Already connected.") - queue: asyncio.Queue = app.state.bot.bridge_queue - - async with app.state.ws_connected: - while True: - try: - await ws.send_json({"status": "ping"}) - except (WebSocketDisconnect, WebSocketException): - log.info("Websocket %r disconnected.", ws) - break - - try: - data = await asyncio.wait_for(queue.get(), timeout=5) - except asyncio.TimeoutError: - continue - - try: - await ws.send_json(data) - log.debug("Sent data %r to websocket %r.", data, ws) - except (WebSocketDisconnect, WebSocketException): - log.info("Websocket %r disconnected." % ws) - break - finally: - queue.task_done()