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

328 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
def generate_pie_chart(
2024-03-19 00:20:22 +00:00
usernames: list[str],
counts: list[int],
2024-03-19 00:25:45 +00:00
no_other: bool = False
2024-03-18 23:57:13 +00:00
) -> 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
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()
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(
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
)
2024-03-19 00:25:45 +00:00
],
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
)
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
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:
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. "
"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)
)
2024-03-18 23:57:13 +00:00
file = await asyncio.to_thread(
self.generate_pie_chart,
list(authors.keys()),
2024-03-19 00:25:45 +00:00
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(
filtered_messages,
total
),
file=file
)
2024-04-15 19:47:58 +01:00
def _metacounter(
self,
messages: list[discord.Message],
filter_func: Callable[[discord.Message], bool],
*,
now: datetime = None
) -> dict[str, float | int]:
now = now or discord.utils.utcnow().replace(minute=0, second=0, microsecond=0)
counts = {
"hour": 0,
"day": 0,
"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:52:32 +01:00
self.log.info("%r was a truth (%.2f seconds ago).", message.id, age.total_seconds())
2024-04-15 19:47:58 +01:00
counts["all_time"] += 1
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
"""
def is_truth(msg: discord.Message) -> bool:
if msg.author.id == 1101439218334576742:
2024-04-15 18:41:11 +01:00
if msg.created_at.timestamp() <= 1713202855.80234:
# Pre 6:30pm 15th April, embeds were not tagged for detection.
2024-04-15 19:51:39 +01:00
truth = any((_te.type == "rich" for _te in msg.embeds))
if truth:
2024-04-15 19:52:32 +01:00
self.log.info("Found untagged rich trump truth embed: %r", msg.id)
2024-04-15 19:51:39 +01:00
return True
for __t_e in msg.embeds:
if __t_e.type == "rich" and __t_e.colour.value == 0x5448EE:
2024-04-15 19:52:32 +01:00
self.log.info("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
"""
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:51:39 +01:00
self.log.info("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
"""
embed = discord.Embed(
title="Truth Counts",
color=discord.Color.blurple(),
timestamp=discord.utils.utcnow()
)
messages: list[discord.Message] = await channel.history(
limit=None,
after=discord.Object(1228516325681532928)
).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"
f"**Last Day:** {trump_stats['day']:,} ({trump_stats['per_hour']:.1f}/hour)\n"
f"**Last Hour:** {trump_stats['hour']:,} ({trump_stats['per_minute']:.1f}/min)"
),
inline=False
)
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"
f"**Last Day:** {tate_stats['day']:,} ({tate_stats['per_hour']:.1f}/hour)\n"
f"**Last Hour:** {tate_stats['hour']:,} ({tate_stats['per_minute']:.1f}/min)"
)
)
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(),
timestamp=now
)
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))