college-bot-v2/src/cogs/quote_quota.py

313 lines
12 KiB
Python
Raw Normal View History

2024-03-18 23:57:13 +00:00
import asyncio
2024-04-15 19:50:17 +01:00
import logging
2024-03-18 23:57:13 +00:00
import re
import discord
import io
import matplotlib.pyplot as plt
2024-04-15 18:24:55 +01:00
from datetime import timedelta, datetime
2024-03-18 23:57:13 +00:00
from discord.ext import commands
2024-04-15 19:47:58 +01:00
from typing import Callable, Iterable, Annotated
2024-03-18 23:57:13 +00:00
from conf import CONFIG
class QuoteQuota(commands.Cog):
2024-03-19 00:20:22 +00:00
2024-03-18 23:57:13 +00:00
def __init__(self, bot):
self.bot = bot
self.quotes_channel_id = CONFIG["quote_a"].get("channel_id")
2024-03-19 00:25:45 +00:00
self.names = CONFIG["quote_a"].get("names", {})
2024-04-15 19:50:17 +01:00
self.log = logging.getLogger("jimmy.cogs.quote_quota")
2024-03-18 23:57:13 +00:00
@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
2024-04-16 00:46:26 +01:00
def generate_pie_chart(usernames: list[str], counts: list[int], no_other: bool = False) -> discord.File:
2024-03-18 23:57:13 +00:00
"""
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
2024-03-19 00:25:45 +00:00
:param no_other: Disables the "other" grouping
2024-03-18 23:57:13 +00:00
:returns: The pie chart image
"""
2024-03-19 00:14:50 +00:00
def pct(v: int):
2024-03-19 00:25:45 +00:00
return f"{v:.1f}% ({round((v / 100) * sum(counts))})"
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]
2024-03-19 00:20:22 +00:00
2024-03-19 00:48:24 +00:00
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())
2024-03-19 00:32:50 +00:00
fig, ax = plt.subplots(figsize=(7, 7))
2024-03-18 23:57:13 +00:00
ax.pie(
counts,
labels=usernames,
2024-03-19 00:14:50 +00:00
autopct=pct,
2024-03-19 00:25:45 +00:00
startangle=90,
radius=1.2,
2024-03-18 23:57:13 +00:00
)
fig.subplots_adjust(left=0.1, bottom=0.1, right=0.9, top=0.9, wspace=0.3, hspace=0.4)
2024-03-18 23:57:13 +00:00
fio = io.BytesIO()
2024-04-16 00:46:26 +01:00
fig.savefig(fio, format="png")
2024-03-18 23:57:13 +00:00
fio.seek(0)
return discord.File(fio, filename="pie.png")
2024-03-18 23:57:13 +00:00
@commands.slash_command()
async def quota(
2024-04-16 00:46:26 +01:00
self,
ctx: discord.ApplicationContext,
days: Annotated[
int,
discord.Option(
2024-03-18 23:57:13 +00:00
int,
2024-04-16 00:46:26 +01:00
name="lookback",
description="How many days to look back on. Defaults to 7.",
default=7,
min_value=1,
max_value=365,
),
],
merge_other: Annotated[
bool,
discord.Option(
2024-03-19 00:25:45 +00:00
bool,
2024-04-16 00:46:26 +01:00
name="merge_other",
description="Whether to merge authors with less than 5% of the total count into 'Other'.",
default=True,
),
],
2024-03-18 23:57:13 +00:00
):
"""Checks the quote quota for the quotes channel."""
now = discord.utils.utcnow()
2024-03-18 23:59:46 +00:00
oldest = now - timedelta(days=days)
2024-03-18 23:57:13 +00:00
await ctx.defer()
2024-03-18 23:59:46 +00:00
channel = self.quotes_channel or discord.utils.get(ctx.guild.text_channels, name="quotes")
2024-03-18 23:57:13 +00:00
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
2024-04-16 00:46:26 +01:00
async for message in channel.history(limit=None, after=oldest, oldest_first=False):
2024-03-18 23:57:13 +00:00
total += 1
if not message.content:
filtered_messages += 1
continue
if message.attachments:
2024-03-19 00:37:06 +00:00
regex = r".*\s*-\s*@?([\w\s]+)"
2024-03-18 23:57:13 +00:00
else:
2024-03-19 00:37:06 +00:00
regex = r".+\s+-\s*@?([\w\s]+)"
2024-03-18 23:57:13 +00:00
2024-03-19 00:06:12 +00:00
if not (m := re.match(regex, str(message.clean_content))):
2024-03-18 23:57:13 +00:00
filtered_messages += 1
continue
name = m.group(1)
2024-03-19 00:43:23 +00:00
name = name.strip().casefold()
if name == "me":
2024-03-19 00:25:45 +00:00
name = message.author.name.strip().casefold()
if name in self.names:
2024-03-19 00:42:04 +00:00
name = self.names[name].title()
2024-03-19 00:25:45 +00:00
else:
filtered_messages += 1
continue
2024-03-19 00:43:23 +00:00
elif name in self.names:
2024-03-19 00:42:04 +00:00
name = self.names[name].title()
2024-03-19 00:37:06 +00:00
elif name.isdigit():
filtered_messages += 1
continue
2024-03-19 00:25:45 +00:00
2024-03-19 00:43:23 +00:00
name = name.title()
2024-03-18 23:57:13 +00:00
authors.setdefault(name, 0)
authors[name] += 1
2024-03-19 00:46:10 +00:00
if not authors:
if total:
return await ctx.edit(
content="No valid messages found in the last {!s} days. "
2024-04-16 00:46:26 +01:00
"Make sure quotes are formatted properly ending with ` - AuthorName`"
' (e.g. `"This is my quote" - Jimmy`)'.format(days)
2024-03-19 00:46:10 +00:00
)
else:
2024-04-16 00:46:26 +01:00
return await ctx.edit(content="No messages found in the last {!s} days.".format(days))
2024-03-19 00:46:10 +00:00
2024-03-18 23:57:13 +00:00
file = await asyncio.to_thread(
2024-04-16 00:46:26 +01:00
self.generate_pie_chart, list(authors.keys()), list(authors.values()), merge_other
2024-03-18 23:57:13 +00:00
)
return await ctx.edit(
content="{:,} messages (out of {:,}) were filtered (didn't follow format?)".format(
2024-04-16 00:46:26 +01:00
filtered_messages, total
2024-03-18 23:57:13 +00:00
),
2024-04-16 00:46:26 +01:00
file=file,
2024-03-18 23:57:13 +00:00
)
2024-04-15 19:47:58 +01:00
def _metacounter(
2024-04-16 00:46:26 +01:00
self, messages: list[discord.Message], filter_func: Callable[[discord.Message], bool], *, now: datetime = None
2024-04-15 19:47:58 +01:00
) -> dict[str, float | int]:
2024-04-16 11:40:13 +01:00
def _is_today(date: datetime) -> bool:
return date.date() == now.date()
2024-04-15 19:47:58 +01:00
now = now or discord.utils.utcnow().replace(minute=0, second=0, microsecond=0)
counts = {
"hour": 0,
"day": 0,
2024-04-16 11:40:13 +01:00
"today": 0,
2024-04-15 19:47:58 +01:00
"week": 0,
"all_time": 0,
"per_minute": 0.0,
"per_hour": 0.0,
"per_day": 0.0,
}
for message in messages:
if filter_func(message):
2024-04-15 19:50:17 +01:00
age = now - message.created_at
2024-04-15 19:55:42 +01:00
self.log.debug("%r was a truth (%.2f seconds ago).", message.id, age.total_seconds())
2024-04-15 19:47:58 +01:00
counts["all_time"] += 1
2024-04-16 11:40:13 +01:00
if _is_today(message.created_at):
counts["today"] += 1
2024-04-15 19:47:58 +01:00
if message.created_at > now - timedelta(hours=1):
counts["hour"] += 1
if message.created_at > now - timedelta(days=1):
counts["day"] += 1
if message.created_at > now - timedelta(days=7):
counts["week"] += 1
counts["per_minute"] = counts["hour"] / 60
counts["per_hour"] = counts["day"] / 24
counts["per_day"] = counts["week"] / 7
2024-04-15 19:50:17 +01:00
self.log.info("Total truth counts: %r", counts)
2024-04-15 19:47:58 +01:00
return counts
2024-04-15 19:44:49 +01:00
async def _process_trump_truths(self, messages: list[discord.Message]) -> dict[str, int]:
"""
Processes the given messages to count the number of posts by Donald Trump.
2024-04-15 19:44:49 +01:00
:param messages: The messages to process
:returns: The stats
"""
2024-04-16 00:46:26 +01:00
def is_truth(msg: discord.Message) -> bool:
if msg.author.id == 1101439218334576742:
2024-04-15 21:15:12 +01:00
# if msg.created_at.timestamp() <= 1713202855.80234:
# # Pre 6:30pm 15th April, embeds were not tagged for detection.
# truth = any((_te.type == "rich" for _te in msg.embeds))
# if truth:
# self.log.debug("Found untagged rich trump truth embed: %r", msg.id)
# return True
for __t_e in msg.embeds:
2024-04-15 21:16:42 +01:00
if __t_e.type == "rich" and __t_e.colour is not None and __t_e.colour.value == 0x5448EE:
2024-04-15 19:55:42 +01:00
self.log.debug("Found tagged rich trump truth embed: %r", msg.id)
return True
return False
2024-04-15 19:47:58 +01:00
return self._metacounter(messages, is_truth)
2024-04-15 19:44:49 +01:00
async def _process_tate_truths(self, messages: list[discord.Message]) -> dict[str, int]:
"""
Processes the given messages to count the number of posts by Andrew Tate.
:param messages: The messages to process
:returns: The stats
"""
2024-04-16 00:46:26 +01:00
2024-04-15 19:44:49 +01:00
def is_truth(msg: discord.Message) -> bool:
if msg.author.id == 1229496078726860921:
# All the tate truths are already tagged.
for __t_e in msg.embeds:
if __t_e.type == "rich" and __t_e.colour.value == 0x5448EE:
2024-04-15 19:55:42 +01:00
self.log.debug("Found tagged rich tate truth embed %r", __t_e)
2024-04-15 19:44:49 +01:00
return True
return False
2024-04-15 19:47:58 +01:00
return self._metacounter(messages, is_truth)
2024-04-15 19:44:49 +01:00
async def _process_all_messages(self, channel: discord.TextChannel) -> discord.Embed:
"""
Processes all the messages in the given channel.
:param channel: The channel to process
:returns: The stats
"""
2024-04-16 00:46:26 +01:00
embed = discord.Embed(title="Truth Counts", color=discord.Color.blurple(), timestamp=discord.utils.utcnow())
messages: list[discord.Message] = await channel.history(
2024-04-16 00:46:26 +01:00
limit=None, after=discord.Object(1229487065117233203)
).flatten()
2024-04-15 19:44:49 +01:00
trump_stats = await self._process_trump_truths(messages)
tate_stats = await self._process_tate_truths(messages)
embed.add_field(
name="Donald Trump",
value=(
f"**All time:** {trump_stats['all_time']:,}\n"
f"**Last Week:** {trump_stats['week']:,} ({trump_stats['per_day']:.1f}/day)\n"
2024-04-16 11:40:13 +01:00
f"**Today:** {trump_stats['today']:,}\n"
f"**Last 24 Hours:** {trump_stats['day']:,} ({trump_stats['per_hour']:.1f}/hour)\n"
2024-04-15 19:44:49 +01:00
f"**Last Hour:** {trump_stats['hour']:,} ({trump_stats['per_minute']:.1f}/min)"
),
)
embed.add_field(
name="Andrew Tate",
value=(
f"**All time:** {tate_stats['all_time']:,}\n"
f"**Last Week:** {tate_stats['week']:,} ({tate_stats['per_day']:.1f}/day)\n"
2024-04-16 11:40:13 +01:00
f"**Today:** {tate_stats['today']:,}\n"
f"**Last 24 Hours:** {tate_stats['day']:,} ({tate_stats['per_hour']:.1f}/hour)\n"
2024-04-15 19:44:49 +01:00
f"**Last Hour:** {tate_stats['hour']:,} ({tate_stats['per_minute']:.1f}/min)"
2024-04-16 00:46:26 +01:00
),
2024-04-15 19:44:49 +01:00
)
return embed
@commands.slash_command(name="truths")
async def truths_counter(self, ctx: discord.ApplicationContext):
"""Counts the number of times the word 'truth' has been said in the quotes channel."""
2024-04-15 18:24:55 +01:00
channel = discord.utils.get(ctx.guild.text_channels, name="spam")
if not channel:
return await ctx.respond(":x: Cannot find spam channel.")
await ctx.defer()
now = discord.utils.utcnow().replace(minute=0, second=0, microsecond=0)
embed = discord.Embed(
title="Counting truths, please wait.",
2024-04-15 19:44:49 +01:00
description="This may take a minute depending on how insane Trump and Tate are feeling.",
2024-04-15 18:24:55 +01:00
color=discord.Color.blurple(),
2024-04-16 00:46:26 +01:00
timestamp=now,
2024-04-15 18:24:55 +01:00
)
await ctx.respond(embed=embed)
2024-04-15 19:44:49 +01:00
embed = await self._process_all_messages(channel)
2024-04-15 18:24:55 +01:00
await ctx.edit(embed=embed)
2024-03-18 23:57:13 +00:00
def setup(bot):
bot.add_cog(QuoteQuota(bot))