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 ) )