discord-havoc/cogs/karma.py

358 lines
12 KiB
Python
Raw Normal View History

2025-02-13 14:51:35 -05:00
import sys
from os import path
2025-04-17 14:35:56 -04:00
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
2025-02-13 14:51:35 -05:00
import constants
import traceback
import time
import importlib
import logging
2025-02-15 13:57:47 -05:00
from typing import Optional
2025-02-13 14:51:35 -05:00
import discord
import regex
2025-02-14 07:42:32 -05:00
from regex import Pattern
2025-02-13 14:51:35 -05:00
from aiohttp import ClientSession, ClientTimeout
from discord.ext import bridge, commands, tasks
2025-02-15 08:36:45 -05:00
from disc_havoc import Havoc
2025-02-13 14:51:35 -05:00
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
class Util:
"""Karma Utility"""
2025-04-17 14:35:56 -04:00
2025-02-15 08:36:45 -05:00
def __init__(self, bot: Havoc):
self.bot: Havoc = bot
2025-02-13 14:51:35 -05:00
self.api_key: str = constants.PRV_API_KEY
self.karma_endpoints_base_url: str = "https://api.codey.lol/karma/"
self.karma_retrieval_url: str = f"{self.karma_endpoints_base_url}get"
self.karma_update_url: str = f"{self.karma_endpoints_base_url}modify"
2025-04-17 14:35:56 -04:00
self.karma_top_10_url: str = f"{self.karma_endpoints_base_url}top"
self.timers: dict = {} # discord uid : timestamp, used for rate limiting
self.karma_cooldown: int = 15 # 15 seconds between karma updates
2025-02-13 14:51:35 -05:00
2025-02-15 13:57:47 -05:00
async def get_karma(self, keyword: str) -> int:
2025-02-13 14:51:35 -05:00
"""
Get Karma for Keyword
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
Args:
keyword (str)
Returns:
2025-02-16 20:07:02 -05:00
int
2025-02-13 14:51:35 -05:00
"""
try:
async with ClientSession() as session:
2025-04-17 14:35:56 -04:00
async with await session.post(
self.karma_retrieval_url,
json={"keyword": keyword},
headers={
"content-type": "application/json; charset=utf-8",
"X-Authd-With": f"Bearer {constants.KARMA_API_KEY}",
},
timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
2025-02-13 14:51:35 -05:00
resp = await request.json()
2025-04-17 14:35:56 -04:00
return resp.get("count")
2025-02-13 14:51:35 -05:00
except Exception as e:
traceback.print_exc()
2025-02-15 13:57:47 -05:00
return False
2025-04-17 14:35:56 -04:00
2025-02-15 13:57:47 -05:00
async def get_top(self, n: int = 10) -> Optional[dict]:
2025-02-13 14:51:35 -05:00
"""
Get top (n=10) Karma
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
Args:
n (int): Number of top results to return, default 10
Returns:
2025-04-17 14:35:56 -04:00
Optional[dict]
2025-02-13 14:51:35 -05:00
"""
try:
async with ClientSession() as session:
2025-04-17 14:35:56 -04:00
async with await session.post(
self.karma_top_10_url,
json={
"n": n,
},
headers={
"content-type": "application/json; charset=utf-8",
"X-Authd-With": f"Bearer {constants.KARMA_API_KEY}",
},
timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
2025-02-13 14:51:35 -05:00
resp: dict = await request.json()
return resp
except:
traceback.print_exc()
2025-02-15 13:57:47 -05:00
return None
2025-02-13 14:51:35 -05:00
2025-04-17 14:35:56 -04:00
async def get_top_embed(self, n: int = 10) -> Optional[discord.Embed]:
2025-02-13 14:51:35 -05:00
"""
Get Top Karma Embed
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
Args:
n (int): Number of top results to return, default 10
Returns:
2025-04-17 14:35:56 -04:00
Optional[discord.Embed]
2025-02-13 14:51:35 -05:00
"""
2025-02-15 13:57:47 -05:00
top: Optional[dict] = await self.get_top(n)
if not top:
return None
2025-02-13 14:51:35 -05:00
top_formatted: str = ""
for x, item in enumerate(top):
2025-04-17 14:35:56 -04:00
top_formatted += (
f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n"
)
2025-02-15 13:57:47 -05:00
top_formatted = top_formatted.strip()
2025-04-17 14:35:56 -04:00
embed: discord.Embed = discord.Embed(
title=f"Top {n} Karma", description=top_formatted, colour=0xFF00FF
)
2025-02-13 14:51:35 -05:00
return embed
2025-04-17 14:35:56 -04:00
async def update_karma(
self, display: str, _id: int, keyword: str, flag: int
) -> bool:
2025-02-13 14:51:35 -05:00
"""
Update Karma for Keyword
Args:
2025-02-16 20:07:02 -05:00
2025-02-13 14:51:35 -05:00
display (str): Display name of the user who requested the update
_id (int): Discord UID of the user who requested the update
keyword (str): Keyword to update
flag (int)
2025-02-16 20:07:02 -05:00
Returns:
bool
"""
2025-02-13 14:51:35 -05:00
if not flag in [0, 1]:
2025-02-15 13:57:47 -05:00
return False
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
reqObj: dict = {
2025-04-17 14:35:56 -04:00
"granter": f"Discord: {display} ({_id})",
"keyword": keyword,
"flag": flag,
2025-02-13 14:51:35 -05:00
}
try:
async with ClientSession() as session:
2025-04-17 14:35:56 -04:00
async with await session.post(
self.karma_update_url,
json=reqObj,
headers={
"content-type": "application/json; charset=utf-8",
"X-Authd-With": f"Bearer {self.api_key}",
},
timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
2025-02-13 14:51:35 -05:00
result = await request.json()
2025-04-17 14:35:56 -04:00
return result.get("success", False)
2025-02-13 14:51:35 -05:00
except:
traceback.print_exc()
return False
async def check_cooldown(self, user_id: int) -> bool:
"""
Check if member has met cooldown period prior to adjusting karma
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
Args:
user_id (int): The Discord UID to check
Returns:
bool
"""
if not user_id in self.timers:
return True
now = int(time.time())
if (now - self.timers[user_id]) < self.karma_cooldown:
return False
return True
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
class Karma(commands.Cog):
"""Karma Cog for Havoc"""
2025-04-17 14:35:56 -04:00
2025-02-15 08:36:45 -05:00
def __init__(self, bot: Havoc):
2025-02-13 14:51:35 -05:00
importlib.reload(constants)
2025-02-15 08:36:45 -05:00
self.bot: Havoc = bot
2025-02-13 14:51:35 -05:00
self.util = Util(self.bot)
# self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)')
2025-04-17 14:35:56 -04:00
self.karma_regex: Pattern = regex.compile(
r"(\b\w+(?:\s+\w+)*)(\+\+($|\s)|\-\-($|\s))"
)
self.mention_regex: Pattern = regex.compile(r"(<@([0-9]{17,20})>)(\+\+|\-\-)")
self.mention_regex_no_flag: Pattern = regex.compile(r"(<@([0-9]{17,20})>+)")
2025-02-13 14:51:35 -05:00
self.karma_chanid: int = 1307065684785893406
self.karma_msgid: int = 1325442184572567686
# asyncio.get_event_loop().create_task(self.bot.get_channel(self.karma_chanid).send("."))
try:
self.update_karma_chan.start()
except Exception as e:
pass
@tasks.loop(seconds=30, reconnect=True)
2025-02-14 07:42:32 -05:00
async def update_karma_chan(self) -> None:
2025-02-13 14:51:35 -05:00
"""Update the Karma Chan Leaderboard"""
try:
top_embed = await self.util.get_top_embed(n=25)
channel = self.bot.get_channel(self.karma_chanid)
2025-02-15 13:57:47 -05:00
if not isinstance(channel, discord.TextChannel):
return
2025-02-13 14:51:35 -05:00
message_to_edit = await channel.fetch_message(self.karma_msgid)
2025-04-17 14:35:56 -04:00
await message_to_edit.edit(
embed=top_embed,
content="## This message will automatically update periodically.",
)
2025-02-13 14:51:35 -05:00
except:
traceback.print_exc()
@commands.Cog.listener()
2025-02-14 07:42:32 -05:00
async def on_message(self, message: discord.Message) -> None:
2025-02-13 14:51:35 -05:00
"""
Message hook, to monitor for ++/--
Also monitors for messages to #karma to autodelete, only Havoc may post in #karma!
"""
2025-04-17 14:35:56 -04:00
if not self.bot.user: # No valid client instance
2025-02-15 13:57:47 -05:00
return
if not isinstance(message.channel, discord.TextChannel):
return
2025-04-17 14:35:56 -04:00
if (
message.channel.id == self.karma_chanid
and not message.author.id == self.bot.user.id
):
2025-02-13 14:51:35 -05:00
"""Message to #karma not by Havoc, delete it"""
await message.delete(reason="Messages to #karma are not allowed")
removal_embed: discord.Embed = discord.Embed(
title="Message Deleted",
2025-04-17 14:35:56 -04:00
description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed.",
2025-02-13 14:51:35 -05:00
)
2025-02-15 13:57:47 -05:00
await message.author.send(embed=removal_embed)
2025-02-13 14:51:35 -05:00
2025-04-17 14:35:56 -04:00
if message.author.id == self.bot.user.id: # Bots own message
2025-02-13 14:51:35 -05:00
return
2025-02-15 13:57:47 -05:00
if not message.guild:
return
2025-04-17 14:35:56 -04:00
if not message.guild.id in [
1145182936002482196,
1228740575235149855,
]: # Not a valid guild for cmd
2025-02-13 14:51:35 -05:00
return
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
message_content: str = message.content.strip()
mentions: list = regex.findall(self.mention_regex, message_content)
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
for mention in mentions:
try:
logging.debug("Mention: %s", mention)
mentioned_uid: int = int(mention[1])
friendly_flag: int = int(mention[2])
2025-02-15 13:57:47 -05:00
guild: Optional[discord.Guild] = self.bot.get_guild(message.guild.id)
if not guild:
return
guild_member: Optional[discord.Member] = guild.get_member(mentioned_uid)
if not guild_member:
return
2025-02-13 14:51:35 -05:00
display: str = guild_member.display_name
2025-02-15 13:57:47 -05:00
message_content = message_content.replace(mention[0], display)
2025-02-13 14:51:35 -05:00
logging.debug("New message: %s", message_content)
except:
traceback.print_exc()
2025-02-15 13:57:47 -05:00
message_content = discord.utils.escape_markdown(message_content)
2025-02-13 14:51:35 -05:00
2025-04-17 14:35:56 -04:00
karma_regex: list[str] = regex.findall(
self.karma_regex, message_content.strip()
)
2025-02-13 14:51:35 -05:00
if not karma_regex: # Not a request to adjust karma
return
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
flooding: bool = not await self.util.check_cooldown(message.author.id)
2025-04-17 14:35:56 -04:00
exempt_uids: list[int] = [1172340700663255091, 992437729927376996]
2025-02-13 14:51:35 -05:00
if flooding and not message.author.id in exempt_uids:
return await message.add_reaction(emoji="")
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
processed_keywords_lc: list[str] = []
logging.debug("Matched: %s", karma_regex)
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
for matched_keyword in karma_regex:
2025-02-15 13:57:47 -05:00
if not isinstance(matched_keyword, tuple):
continue
2025-02-13 14:51:35 -05:00
if len(matched_keyword) == 4:
(keyword, friendly_flag, _, __) = matched_keyword
else:
2025-04-17 14:35:56 -04:00
(keyword, friendly_flag) = matched_keyword
2025-02-13 14:51:35 -05:00
now: int = int(time.time())
flag: int = None
match friendly_flag:
case "++":
2025-02-15 13:57:47 -05:00
flag = 0
2025-02-13 14:51:35 -05:00
case "--":
2025-02-15 13:57:47 -05:00
flag = 1
2025-02-13 14:51:35 -05:00
case _:
logging.info("Unknown flag %s", flag)
continue
if keyword.lower() in processed_keywords_lc:
continue
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
processed_keywords_lc.append(keyword.lower())
self.util.timers[message.author.id] = now
2025-04-17 14:35:56 -04:00
updated: bool = await self.util.update_karma(
message.author.display_name, message.author.id, keyword, flag
)
2025-02-13 14:51:35 -05:00
if updated:
return await message.add_reaction(emoji="👍")
@bridge.bridge_command()
async def karma(self, ctx, *, keyword: str | None = None) -> None:
"""With no arguments, top 10 karma is provided; a keyword can also be provided to lookup."""
try:
if not keyword:
2025-02-15 13:57:47 -05:00
top_10_embed: Optional[discord.Embed] = await self.util.get_top_embed()
if not top_10_embed:
return
2025-02-13 14:51:35 -05:00
return await ctx.respond(embed=top_10_embed)
2025-04-17 14:35:56 -04:00
2025-02-15 13:57:47 -05:00
keyword = discord.utils.escape_markdown(keyword)
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
mentions: list[str] = regex.findall(self.mention_regex_no_flag, keyword)
for mention in mentions:
try:
mentioned_uid = int(mention[1])
2025-02-15 13:57:47 -05:00
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild:
return
2025-04-17 14:35:56 -04:00
guild_member: Optional[discord.Member] = guild.get_member(
mentioned_uid
)
2025-02-15 13:57:47 -05:00
if not guild_member:
return
2025-02-13 14:51:35 -05:00
display = guild_member.display_name
keyword = keyword.replace(mention[0], display)
except:
traceback.print_exc()
continue
score: int = await self.util.get_karma(keyword)
description: str = f"**{keyword}** has a karma of *{score}*"
2025-04-17 14:35:56 -04:00
embed: discord.Embed = discord.Embed(
title=f"Karma for {keyword}", description=description
)
2025-02-13 14:51:35 -05:00
return await ctx.respond(embed=embed)
except Exception as e:
await ctx.respond(f"Error: {str(e)}")
traceback.print_exc()
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
def cog_unload(self) -> None:
try:
self.update_karma_chan.cancel()
except:
"""Safe to ignore"""
pass
2025-04-17 14:35:56 -04:00
2025-02-13 14:51:35 -05:00
def setup(bot) -> None:
"""Run on Cog Load"""
bot.add_cog(Karma(bot))