discord-havoc/cogs/karma.py

309 lines
12 KiB
Python
Raw Normal View History

2025-02-13 14:51:35 -05:00
#!/usr/bin/env python3.12
# pylint: disable=broad-exception-caught, bare-except, invalid-name
import sys
from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
import constants
import traceback
import time
import importlib
import logging
import discord
import regex
from typing import Pattern
from aiohttp import ClientSession, ClientTimeout
from discord.ext import bridge, commands, tasks
class Util:
"""Karma Utility"""
def __init__(self, bot):
self.bot = bot
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"
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
async def get_karma(self, keyword: str) -> int|str:
"""
Get Karma for Keyword
Args:
keyword (str)
Returns:
int|str
"""
try:
async with ClientSession() as session:
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:
resp = await request.json()
return resp.get('count')
except Exception as e:
traceback.print_exc()
return f"Failed-- {type(e).__name__}: {str(e)}"
async def get_top(self, n: int = 10) -> dict:
"""
Get top (n=10) Karma
Args:
n (int): Number of top results to return, default 10
Returns:
dict
"""
try:
async with ClientSession() as session:
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:
resp: dict = await request.json()
return resp
except:
traceback.print_exc()
async def get_top_embed(self, n:int = 10) -> discord.Embed:
"""
Get Top Karma Embed
Args:
n (int): Number of top results to return, default 10
Returns:
discord.Embed
"""
top: dict = await self.get_top(n)
top_formatted: str = ""
for x, item in enumerate(top):
top_formatted += f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n"
top_formatted: str = top_formatted.strip()
embed: discord.Embed = discord.Embed(title=f"Top {n} Karma",
description=top_formatted,
colour=0xff00ff)
return embed
async def update_karma(self, display: str, _id: int, keyword: str, flag: int) -> bool:
"""
Update Karma for Keyword
Args:
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)
"""
if not flag in [0, 1]:
return
reqObj: dict = {
'granter': f"Discord: {display} ({_id})",
'keyword': keyword,
'flag': flag,
}
try:
async with ClientSession() as session:
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:
result = await request.json()
return result.get('success', False)
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
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
class Karma(commands.Cog):
"""Karma Cog for Havoc"""
def __init__(self, bot):
importlib.reload(constants)
self.bot = bot
self.util = Util(self.bot)
# self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)')
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})>+)')
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)
async def update_karma_chan(self):
"""Update the Karma Chan Leaderboard"""
try:
top_embed = await self.util.get_top_embed(n=25)
channel = self.bot.get_channel(self.karma_chanid)
message_to_edit = await channel.fetch_message(self.karma_msgid)
await message_to_edit.edit(embed=top_embed,
content="## This message will automatically update periodically.")
except:
traceback.print_exc()
@commands.Cog.listener()
async def on_message(self, message: discord.Message):
"""
Message hook, to monitor for ++/--
Also monitors for messages to #karma to autodelete, only Havoc may post in #karma!
"""
if message.channel.id == self.karma_chanid and not message.author.id == self.bot.user.id:
"""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",
description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed."
)
return await message.author.send(embed=removal_embed)
if message.author.id == self.bot.user.id: # Bots own message
return
if not message.guild.id in [1145182936002482196, 1228740575235149855]: # Not a valid guild for cmd
return
message_content: str = message.content.strip()
mentions: list = regex.findall(self.mention_regex, message_content)
for mention in mentions:
try:
logging.debug("Mention: %s", mention)
mentioned_uid: int = int(mention[1])
friendly_flag: int = int(mention[2])
guild: discord.Guild = self.bot.get_guild(message.guild.id)
guild_member: discord.Member = guild.get_member(mentioned_uid)
display: str = guild_member.display_name
message_content: str = message_content.replace(mention[0], display)
logging.debug("New message: %s", message_content)
except:
traceback.print_exc()
message_content: str = discord.utils.escape_markdown(message_content)
karma_regex: list[str] = regex.findall(self.karma_regex, message_content.strip())
if not karma_regex: # Not a request to adjust karma
return
flooding: bool = not await self.util.check_cooldown(message.author.id)
exempt_uids: list[int] = [1172340700663255091, 992437729927376996]
if flooding and not message.author.id in exempt_uids:
return await message.add_reaction(emoji="")
processed_keywords_lc: list[str] = []
logging.debug("Matched: %s", karma_regex)
for matched_keyword in karma_regex:
if len(matched_keyword) == 4:
(keyword, friendly_flag, _, __) = matched_keyword
else:
(keyword, friendly_flag) = matched_keyword
now: int = int(time.time())
flag: int = None
match friendly_flag:
case "++":
flag: int = 0
case "--":
flag: int = 1
case _:
logging.info("Unknown flag %s", flag)
continue
if keyword.lower() in processed_keywords_lc:
continue
processed_keywords_lc.append(keyword.lower())
self.util.timers[message.author.id] = now
updated: bool = await self.util.update_karma(message.author.display_name,
message.author.id, keyword, flag)
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:
top_10_embed: discord.Embed = await self.util.get_top_embed()
return await ctx.respond(embed=top_10_embed)
keyword: str = discord.utils.escape_markdown(keyword)
mentions: list[str] = regex.findall(self.mention_regex_no_flag, keyword)
for mention in mentions:
try:
mentioned_uid = int(mention[1])
guild = self.bot.get_guild(ctx.guild.id)
guild_member = guild.get_member(mentioned_uid)
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}*"
if isinstance(score, dict) and score.get('err'):
description: str = f"*{score.get('errorText')}*"
embed: discord.Embed = discord.Embed(title=f"Karma for {keyword}",
description=description)
return await ctx.respond(embed=embed)
except Exception as e:
await ctx.respond(f"Error: {str(e)}")
traceback.print_exc()
def cog_unload(self) -> None:
try:
self.update_karma_chan.cancel()
except:
"""Safe to ignore"""
pass
def setup(bot) -> None:
"""Run on Cog Load"""
bot.add_cog(Karma(bot))