309 lines
12 KiB
Python
309 lines
12 KiB
Python
|
#!/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))
|