Compare commits

..

21 Commits

Author SHA1 Message Date
863ebeb919 add quote.py to repo/not loaded 2025-06-27 16:12:39 -04:00
80cc3dc1e8 meme cog: Don't convert images if already in PNG format; sing cog: remove ephemeral=True from bridge s/sing command initial response; misc_util: remove 2 coffees 2025-06-22 07:52:19 -04:00
75addd629c misc/migration related 2025-06-08 08:53:27 -04:00
046ad4f94a migration related 2025-05-27 16:50:40 -04:00
b3f0e084ce misc/rm 2025-05-20 11:13:49 -04:00
b13c2eec2a whoops- spotify activity read error 2025-05-17 12:33:27 -04:00
bcc3bc02fa cleanup 2025-05-17 08:07:53 -04:00
3dac803305 meme dupe snitching/misc 2025-05-15 15:49:28 -04:00
6a1fd659e8 naas 2025-05-01 15:54:14 -04:00
a433dc2fb5 reformat/refactor 2025-04-26 21:59:46 -04:00
5bde6ac880 satan command 2025-04-26 20:05:04 -04:00
4d331b7ffd why hath thou returned, vile egg coffee? begone! 2025-04-20 05:33:24 -04:00
1bb482315e reformat (black) 2025-04-17 14:35:56 -04:00
d12b066c8e re-add listening activity 2025-04-10 19:52:29 -04:00
c7db52efde clean docstrings for commands, resolves #1 2025-04-10 11:17:32 -04:00
11ff1d6447 sing - minor, respond with actual API error on failure, rather than just always "Not found!" 2025-04-08 20:03:07 -04:00
6e74c1bb31 cleanup (moved misc_utils to utils), changed sing footer. 2025-03-29 08:09:28 -04:00
4f3c82f9c0 do away with sing embeds globally 2025-03-29 06:41:03 -04:00
436755f884 better shortened lyrics for non-botspam chans 2025-03-25 06:25:04 -04:00
6018de870d minor 2025-03-24 15:14:38 -04:00
e9d0065876 fix casing 2025-03-24 09:08:29 -04:00
23 changed files with 2757 additions and 2242 deletions

14
api.py
View File

@ -6,6 +6,7 @@ from fastapi import FastAPI, HTTPException
from pydantic import BaseModel from pydantic import BaseModel
import util import util
class ValidSendMsgRequest(BaseModel): class ValidSendMsgRequest(BaseModel):
""" """
- **guild**: optional, guild id in case multiple channels match (normally first result would be used) - **guild**: optional, guild id in case multiple channels match (normally first result would be used)
@ -17,18 +18,19 @@ class ValidSendMsgRequest(BaseModel):
channel: str channel: str
message: str message: str
class API: class API:
"""API [FastAPI Instance] for Havoc""" """API [FastAPI Instance] for Havoc"""
def __init__(self, discord_bot): def __init__(self, discord_bot):
api_app = FastAPI(title="Havoc API") api_app = FastAPI(title="Havoc API")
self.bot = discord_bot self.bot = discord_bot
self.api_app = api_app self.api_app = api_app
@api_app.get("/{any:path}") @api_app.get("/{any:path}")
def block_get(): def block_get():
raise HTTPException(status_code=403, detail="Invalid request") raise HTTPException(status_code=403, detail="Invalid request")
@api_app.post("/send_msg") @api_app.post("/send_msg")
async def send_msg_handler(data: ValidSendMsgRequest): async def send_msg_handler(data: ValidSendMsgRequest):
await util.discord_helpers.send_message( await util.discord_helpers.send_message(
@ -38,12 +40,14 @@ class API:
message=data.message, message=data.message,
) )
return { return {
'result': "presumed_success", "result": "presumed_success",
} }
def __init__(): def __init__():
import util import util
importlib.reload(util) importlib.reload(util)
__init__()
__init__()

View File

@ -1,330 +0,0 @@
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
from typing import Optional
import discord
import regex
from regex import Pattern
from aiohttp import ClientSession, ClientTimeout
from discord.ext import bridge, commands, tasks
from disc_havoc import Havoc
class Util:
"""Karma Utility"""
def __init__(self, bot: Havoc):
self.bot: Havoc = 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:
"""
Get Karma for Keyword
Args:
keyword (str)
Returns:
int
"""
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 False
async def get_top(self, n: int = 10) -> Optional[dict]:
"""
Get top (n=10) Karma
Args:
n (int): Number of top results to return, default 10
Returns:
Optional[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()
return None
async def get_top_embed(self, n:int = 10) -> Optional[discord.Embed]:
"""
Get Top Karma Embed
Args:
n (int): Number of top results to return, default 10
Returns:
Optional[discord.Embed]
"""
top: Optional[dict] = await self.get_top(n)
if not top:
return None
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 = 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)
Returns:
bool
"""
if not flag in [0, 1]:
return False
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: Havoc):
importlib.reload(constants)
self.bot: Havoc = 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) -> None:
"""Update the Karma Chan Leaderboard"""
try:
top_embed = await self.util.get_top_embed(n=25)
channel = self.bot.get_channel(self.karma_chanid)
if not isinstance(channel, discord.TextChannel):
return
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) -> None:
"""
Message hook, to monitor for ++/--
Also monitors for messages to #karma to autodelete, only Havoc may post in #karma!
"""
if not self.bot.user: # No valid client instance
return
if not isinstance(message.channel, discord.TextChannel):
return
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."
)
await message.author.send(embed=removal_embed)
if message.author.id == self.bot.user.id: # Bots own message
return
if not message.guild:
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: 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
display: str = guild_member.display_name
message_content = message_content.replace(mention[0], display)
logging.debug("New message: %s", message_content)
except:
traceback.print_exc()
message_content = 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 not isinstance(matched_keyword, tuple):
continue
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 = 0
case "--":
flag = 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: Optional[discord.Embed] = await self.util.get_top_embed()
if not top_10_embed:
return
return await ctx.respond(embed=top_10_embed)
keyword = 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: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild:
return
guild_member: Optional[discord.Member] = guild.get_member(mentioned_uid)
if not guild_member:
return
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}*"
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))

View File

@ -5,8 +5,10 @@ from discord.ext import bridge, commands
from util.lovehate_db import DB from util.lovehate_db import DB
from disc_havoc import Havoc from disc_havoc import Havoc
class LoveHate(commands.Cog): class LoveHate(commands.Cog):
"""LoveHate Cog for Havoc""" """LoveHate Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.db = DB(self.bot) self.db = DB(self.bot)
@ -14,66 +16,54 @@ class LoveHate(commands.Cog):
def join_with_and(self, items: list) -> str: def join_with_and(self, items: list) -> str:
""" """
Join list with and added before last item Join list with and added before last item
Args: Args:
items (list) items (list)
Returns: Returns:
str str
""" """
if len(items) > 1: if len(items) > 1:
return ', '.join(items[:-1]) + ' and ' + items[-1] return ", ".join(items[:-1]) + " and " + items[-1]
return items[0] if items else '' return items[0] if items else ""
@bridge.bridge_command() @bridge.bridge_command()
async def loves(self, ctx, user: Optional[str] = None) -> None: async def loves(self, ctx, user: Optional[str] = None) -> None:
""" """
If keyword isn't provided, returns the things YOU love; specify a user to find what THEY love. If keyword isn't provided, returns the things YOU love; specify a user to find what THEY love.
Args:
ctx (Any)
user (Optional[str])
Returns:
None
""" """
try: try:
if not user: if not user:
display_name = ctx.author.display_name display_name = ctx.author.display_name
loves: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name, loves: Union[list[tuple], bool] = await self.db.get_lovehates(
loves=True) user=display_name, loves=True
)
if not loves: if not loves:
return await ctx.respond("You don't seem to love anything...") return await ctx.respond("You don't seem to love anything...")
out_loves: list = [] out_loves: list = []
if not isinstance(loves, list): if not isinstance(loves, list):
return return
for love in loves: for love in loves:
(love,) = love (love,) = love
out_loves.append(love) out_loves.append(love)
out_loves_str: str = self.join_with_and(out_loves) out_loves_str: str = self.join_with_and(out_loves)
return await ctx.respond(f"{ctx.author.mention} loves {out_loves_str}") return await ctx.respond(f"{ctx.author.mention} loves {out_loves_str}")
loves = await self.db.get_lovehates(user=user.strip(), loves=True) loves = await self.db.get_lovehates(user=user.strip(), loves=True)
if not loves: if not loves:
return await ctx.respond(f"{user} doesn't seem to love anything...") return await ctx.respond(f"{user} doesn't seem to love anything...")
out_loves_str = self.join_with_and(out_loves) out_loves_str = self.join_with_and(out_loves)
return await ctx.respond(f"{user} loves {out_loves_str}") return await ctx.respond(f"{user} loves {out_loves_str}")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"Error: {str(e)}") return await ctx.respond(f"Error: {str(e)}")
@bridge.bridge_command() @bridge.bridge_command()
async def wholoves(self, ctx, *, thing: Optional[str] = None) -> None: async def wholoves(self, ctx, *, thing: Optional[str] = None) -> None:
""" """
Check who loves <thing> Check who loves <thing>
Args:
ctx (Any)
thing (Optional[str])
Returns:
None
""" """
try: try:
if not thing: if not thing:
@ -82,48 +72,43 @@ class LoveHate(commands.Cog):
_thing = thing _thing = thing
if discord.utils.raw_mentions(_thing): if discord.utils.raw_mentions(_thing):
# There are mentions # There are mentions
thing_id: int = discord.utils.raw_mentions(_thing)[0] # First mention thing_id: int = discord.utils.raw_mentions(_thing)[0] # First mention
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id) guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild: if not guild:
return return
thing_member: Optional[discord.Member] = guild.get_member(thing_id) thing_member: Optional[discord.Member] = guild.get_member(thing_id)
if not thing_member: if not thing_member:
return return
_thing = thing_member.display_name _thing = thing_member.display_name
if not _thing: if not _thing:
return return
who_loves: Union[list, bool] = await self.db.get_wholovehates(thing=_thing, who_loves: Union[list, bool] = await self.db.get_wholovehates(
loves=True) thing=_thing, loves=True
)
if not isinstance(who_loves, list): if not isinstance(who_loves, list):
return await ctx.respond(f"I couldn't find anyone who loves {thing}...") return await ctx.respond(f"I couldn't find anyone who loves {thing}...")
out_wholoves: list = [] out_wholoves: list = []
for lover in who_loves: for lover in who_loves:
(lover,) = lover (lover,) = lover
out_wholoves.append(str(lover)) out_wholoves.append(str(lover))
optional_s: str = "s" if len(out_wholoves) == 1 else "" optional_s: str = "s" if len(out_wholoves) == 1 else ""
out_wholoves_str: str = self.join_with_and(out_wholoves) out_wholoves_str: str = self.join_with_and(out_wholoves)
return await ctx.respond(f"{out_wholoves_str} love{optional_s} {thing}") return await ctx.respond(f"{out_wholoves_str} love{optional_s} {thing}")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"Error: {str(e)}") return await ctx.respond(f"Error: {str(e)}")
@bridge.bridge_command() @bridge.bridge_command()
async def whohates(self, ctx, *, thing: Optional[str] = None) -> None: async def whohates(self, ctx, *, thing: Optional[str] = None) -> None:
""" """
Check who hates <thing> Check who hates <thing>
Args:
ctx (Any)
thing (Optional[str])
Returns:
None
""" """
try: try:
if not thing: if not thing:
@ -135,17 +120,18 @@ class LoveHate(commands.Cog):
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id) guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild: if not guild:
return return
thing_id: int = discord.utils.raw_mentions(_thing)[0] # First mention thing_id: int = discord.utils.raw_mentions(_thing)[0] # First mention
thing_member: Optional[discord.Member] = guild.get_member(thing_id) thing_member: Optional[discord.Member] = guild.get_member(thing_id)
if not thing_member: if not thing_member:
return return
_thing = thing_member.display_name _thing = thing_member.display_name
who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(thing=_thing, who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(
hates=True) thing=_thing, hates=True
)
if not who_hates: if not who_hates:
return await ctx.respond(f"I couldn't find anyone who hates {thing}...") return await ctx.respond(f"I couldn't find anyone who hates {thing}...")
out_whohates: list = [] out_whohates: list = []
if not isinstance(who_hates, list): if not isinstance(who_hates, list):
return return
@ -154,7 +140,7 @@ class LoveHate(commands.Cog):
out_whohates.append(str(hater)) out_whohates.append(str(hater))
optional_s: str = "s" if len(out_whohates) == 1 else "" optional_s: str = "s" if len(out_whohates) == 1 else ""
out_whohates_str: str = self.join_with_and(out_whohates) out_whohates_str: str = self.join_with_and(out_whohates)
return await ctx.respond(f"{out_whohates_str} hate{optional_s} {thing}") return await ctx.respond(f"{out_whohates_str} hate{optional_s} {thing}")
@ -162,22 +148,14 @@ class LoveHate(commands.Cog):
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"Error: {str(e)}") return await ctx.respond(f"Error: {str(e)}")
@bridge.bridge_command() @bridge.bridge_command()
async def dontcare(self, ctx, thing: str) -> None: async def dontcare(self, ctx, thing: str) -> None:
""" """
Make me forget your opinion on <thing> Make me forget your opinion on <thing>
Args:
ctx (Any)
thing (str)
Returns:
None
""" """
try: try:
stop_caring: str = await self.db.update(ctx.author.display_name, stop_caring: str = await self.db.update(ctx.author.display_name, thing, 0)
thing, 0) return await ctx.respond(stop_caring)
return await ctx.respond(stop_caring)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
@ -186,53 +164,42 @@ class LoveHate(commands.Cog):
async def hates(self, ctx, user: Optional[str] = None) -> None: async def hates(self, ctx, user: Optional[str] = None) -> None:
""" """
If keyword isn't provided, returns the things YOU hate; specify a user to find what THEY hate. If keyword isn't provided, returns the things YOU hate; specify a user to find what THEY hate.
Args:
ctx (Any)
user (Optional[str])
Returns:
None
""" """
try: try:
if not user: if not user:
display_name = ctx.author.display_name display_name = ctx.author.display_name
hates: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name, hates: Union[list[tuple], bool] = await self.db.get_lovehates(
hates=True) user=display_name, hates=True
)
if not hates: if not hates:
return await ctx.respond("You don't seem to hate anything...") return await ctx.respond("You don't seem to hate anything...")
else: else:
hates = await self.db.get_lovehates(user=user.strip(), hates=True) hates = await self.db.get_lovehates(user=user.strip(), hates=True)
if not hates: if not hates:
return await ctx.respond(f"{user} doesn't seem to hate anything...") return await ctx.respond(f"{user} doesn't seem to hate anything...")
out_hates: list = [] out_hates: list = []
if not isinstance(hates, list): if not isinstance(hates, list):
return return
for hated_thing in hates: for hated_thing in hates:
(hated_thing,) = hated_thing (hated_thing,) = hated_thing
out_hates.append(str(hated_thing)) out_hates.append(str(hated_thing))
out_hates_str: str = self.join_with_and(out_hates) out_hates_str: str = self.join_with_and(out_hates)
return await ctx.respond(f"{user} hates {out_hates_str}") return await ctx.respond(f"{user} hates {out_hates_str}")
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
@bridge.bridge_command(aliases=['sarcastichate']) @bridge.bridge_command(aliases=["sarcastichate"])
async def love(self, ctx, *, thing: str) -> None: async def love(self, ctx, *, thing: str) -> None:
""" """
Love <thing> Love <thing>
Args:
ctx (Any)
thing (str)
Returns:
None
""" """
try: try:
if discord.utils.raw_mentions(thing): if discord.utils.raw_mentions(thing):
# There are mentions # There are mentions
thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild) guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild)
if not guild: if not guild:
return return
@ -241,28 +208,21 @@ class LoveHate(commands.Cog):
return return
thing = thing_member.display_name thing = thing_member.display_name
love: str = await self.db.update(ctx.author.display_name, love: str = await self.db.update(ctx.author.display_name, thing, 1)
thing, 1) return await ctx.respond(love)
return await ctx.respond(love)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
@bridge.bridge_command(aliases=['sarcasticlove']) @bridge.bridge_command(aliases=["sarcasticlove"])
async def hate(self, ctx, *, thing: str) -> None: async def hate(self, ctx, *, thing: str) -> None:
""" """
Hate <thing> Hate <thing>
Args:
ctx (Any)
thing (str)
Returns:
None
""" """
try: try:
if discord.utils.raw_mentions(thing): if discord.utils.raw_mentions(thing):
# There are mentions # There are mentions
thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id) guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild: if not guild:
return return
@ -270,18 +230,17 @@ class LoveHate(commands.Cog):
if not thing_member: if not thing_member:
return return
thing = thing_member.display_name thing = thing_member.display_name
hate: str = await self.db.update(ctx.author.display_name, hate: str = await self.db.update(ctx.author.display_name, thing, -1)
thing, -1) return await ctx.respond(hate)
return await ctx.respond(hate)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
def cog_unload(self) -> None: def cog_unload(self) -> None:
# not needed currently # not needed currently
pass pass
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(LoveHate(bot)) bot.add_cog(LoveHate(bot))

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,455 +0,0 @@
import os
import logging
import traceback
import random
import datetime
import pytz
from typing import Optional, LiteralString, Union
import regex
import aiosqlite as sqlite3
from aiohttp import ClientSession, ClientTimeout
from bohancompliment import ComplimentGenerator
from discord import Embed
class Util:
"""Misc Utility"""
def __init__(self) -> None:
self.URL_URBANDICTIONARY: str = "http://api.urbandictionary.com/v0/define"
self.URL_INSULTAPI: str = "https://insult.mattbas.org/api/insult"
self.COMPLIMENT_GENERATOR = ComplimentGenerator()
self.dbs: dict[str, str|LiteralString] = {
'whisky': os.path.join("/usr/local/share",
"sqlite_dbs", "whiskey.db"),
'drinks': os.path.join("/usr/local/share",
"sqlite_dbs", "cocktails.db"),
'strains': os.path.join("/usr/local/share",
"sqlite_dbs", "strains.db"),
'qajoke': os.path.join("/usr/local/share",
"sqlite_dbs", "qajoke.db"),
'rjokes': os.path.join("/usr/local/share",
"sqlite_dbs", "rjokes.db"),
'randmsg': os.path.join("/usr/local/share",
"sqlite_dbs", "randmsg.db"),
'stats': os.path.join("/usr/local/share",
"sqlite_dbs", "havoc_stats.db"),
'cookies': os.path.join("/usr/local/share",
"sqlite_dbs", "cookies.db"),
}
self.COFFEES: list = ['a cup of french-pressed coffee', 'a cup of cold brew', 'a cup of flash brew',
'a cup of Turkish coffee', 'a cup of Moka', 'an espresso',
'a cup of Nescafe coffee',
'an iced coffee', 'a Frappé', 'a freddo cappuccino',
'a cup of Chock full o\'Nuts', 'a cup of Folgers', 'a cup of Lavazza',
'a cup of Maxwell House', 'a cup of Moccona', 'a cup of Mr. Brown Coffee',
'a cup of affogato al caffè',
'a cup of Caffè Medici', 'a cup of Café Touba',
'a double-double', 'an indian filter coffee', 'a cup of pocillo',
'a cup of caffè americano', 'a cup of caffè lungo', 'a latte', 'a manilo',
'a flat white', 'a cup of café cubano', 'a cup of caffè crema',
'a cup of cafe zorro', 'an espresso roberto', 'an espresso romano',
'an espresso sara', 'a guillermo', 'a ristretto', 'a cup of melya',
'a cup of caffè marocchino', 'a cup of café miel', 'a cup of café de olla',
'a Mazagran', 'a Palazzo', 'an ice shot', 'a macchiato',
'a cortado', 'a red eye', 'a cappuccino',
'a mocha', 'a café au lait', 'a bicerin',
'a caffè corretto', 'a ca phe trung', 'a café bombón', 'a Vienna coffee',
'a flat black', 'a lungo', 'a doppio', 'a ristretto bianco', 'a piccolo latte',
'a gibraltar', 'a breve', 'a café con leche', 'a su café', 'a café del tiempo',
'a java chip frappuccino', 'a pumpkin spice latte', 'a caramel macchiato',
'a white chocolate mocha', 'a hazelnut coffee', 'a toffee nut latte',
'a peppermint mocha', 'a cinnamon dolce latte', 'a coconut milk latte',
'an almond milk cappuccino', 'an oat milk latte', 'a caramel frappuccino',
'a chocolate frappuccino', 'a butter pecan coffee', 'a maple pecan latte',
'a sea salt caramel mocha', 'a nitro cold brew', 'a pumpkin cold brew',
'a honey almond flat white', 'a sweet cream cold brew', 'a matcha latte',
'a golden latte', 'a turmeric latte', 'a beetroot latte', 'a Kopi luwak']
self.LAST_5_COFFEES: list = []
def tdTuple(self, td:datetime.timedelta) -> tuple:
"""
Create TimeDelta Tuple
Args:
td (datetime.timedelta)
Returns:
tuple
"""
def _t(t, n):
if t < n:
return (t, 0)
v = t//n
return (t - (v * n), v)
(s, h) = _t(td.seconds, 3600)
(s, m) = _t(s, 60)
(mics, mils) = _t(td.microseconds, 1000)
return (td.days, h, m, s, mics, mils)
async def get_counter(self, counter: Optional[str] = None) -> Optional[dict]:
"""
Get Counter
Args:
counter (Optional[str])
Returns:
Optional[dict]
"""
stats_db: str|LiteralString = self.dbs.get('stats', '')
if not stats_db:
return None
async with sqlite3.connect(stats_db,
timeout=3) as db_conn:
db_conn.row_factory = sqlite3.Row
query: str = "SELECT ? FROM stats LIMIT 1"
if not counter:
query = "SELECT * FROM stats LIMIT 1"
async with await db_conn.execute(query, (counter,) if counter else None) as db_cursor:
result = await db_cursor.fetchone()
return result
async def get_stats_embed(self) -> Optional[Embed]:
"""
Get Stats Embed
Returns:
Optional[Embed]
"""
counters: Optional[dict] = await self.get_counter()
if not counters:
return None
embed: Embed = Embed(title="Stats")
counter_message: str = ""
counters_sorted: dict = dict(sorted(counters.items(),
key=lambda item: item[1], reverse=True))
for counter, value in counters_sorted.items():
counter = regex.sub(r'_', ' ',
counter.strip()).title()
counter_message += f"- {value} {counter}\n"
embed.description = counter_message.strip()
return embed
async def increment_counter(self, counter: str) -> bool:
"""
Increment Counter
Args:
counter (str)
Returns:
bool
"""
stats_db: str|LiteralString = self.dbs.get('stats', '')
if not stats_db:
return False
async with sqlite3.connect(stats_db,
timeout=3) as db_conn:
async with await db_conn.execute(f"UPDATE stats SET {counter} = {counter} + 1") as db_cursor:
if db_cursor.rowcount < 0:
logging.critical("[karma::increment_counter] Fail! %s", db_cursor.rowcount)
return False
await db_conn.commit()
return True
async def get_ud_def(self, term: str) -> tuple[str, str]:
"""
Get Definition from UD
Args:
term (str)
Returns:
tuple[str, str]
"""
try:
async with ClientSession() as session:
async with await session.get(self.URL_URBANDICTIONARY,
params={
"term": term,
},
headers = {
'content-type': 'application/json; charset=utf-8',
}, timeout=ClientTimeout(connect=5, sock_read=5)) as request:
logging.debug("UD returned: %s",
await request.text())
data: dict = await request.json()
if "list" in data:
definitions: list[dict] = data["list"]
if definitions:
definition: dict = definitions[0]
definition_word: str = definition.get("word", "N/A")
definition_text: str = regex.sub(r'(\r|\n|\r\n)', ' ', definition["definition"].strip())
return (definition_word, definition_text) # Tuple: Returned word, returned definition
else:
return (term, "Not found!")
else:
return (term, "Error retrieving data from Urban Dictionary")
except Exception as e:
traceback.print_exc()
return (term, f"ERR: {str(e)}")
async def get_insult(self, recipient: str) -> str:
"""
Get Insult
Args:
recipient (str)
Returns:
str
"""
async with ClientSession() as session:
async with await session.get(f"{self.URL_INSULTAPI}?who={recipient}") as request:
request.raise_for_status()
return await request.text()
async def get_compliment(self, subject: str,
language: Optional[str] = None) -> str:
"""
Get Compliment
Args:
subject (str)
language (Optional[str])
Returns:
str
"""
if not language:
return self.COMPLIMENT_GENERATOR.compliment(subject)
return self.COMPLIMENT_GENERATOR.compliment_in_language(subject, language)
async def get_whisky(self) -> Optional[tuple]:
"""
Get Whisky
Returns:
Optional[tuple]
"""
whisky_db: str|LiteralString = self.dbs.get('whisky', '')
if not whisky_db:
return None
async with sqlite3.connect(database=whisky_db,
timeout=2) as db_conn:
db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
db_result: Optional[Union[sqlite3.Row, tuple]] = await db_cursor.fetchone()
if not db_result:
return None
(name, category, description) = db_result
name = regex.sub(r'(^\p{White_Space}|\r|\n)', '',
regex.sub(r'\p{White_Space}{2,}', ' ',
name.strip()))
category = regex.sub(r'(^\p{White_Space}|\r|\n)', '',
regex.sub(r'\p{White_Space}{2,}', ' ',
category.strip()))
description = regex.sub(r'(^\p{White_Space}|\r|\n)', '',
regex.sub(r'\p{White_Space}{2,}', ' ',
description.strip()))
return (name, category, description)
async def get_drink(self) -> Optional[tuple]:
"""
Get Drink
Returns:
Optional[tuple]
"""
drinks_db: str|LiteralString = self.dbs.get('drinks', '')
if not drinks_db:
return None
async with sqlite3.connect(database=drinks_db,
timeout=2) as db_conn:
db_query: str = "SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
db_result: tuple = await db_cursor.fetchone()
(name, ingredients) = db_result
name = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', name.strip()))
ingredients = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', ingredients.strip()))
ingredients = regex.sub(r'\*', '\u2731', ingredients.strip())
return (name, ingredients)
async def get_strain(self, strain: Optional[str] = None) -> Optional[tuple]:
"""
Get Strain
Args:
strain (Optional[str])
Returns:
Optional[tuple]
"""
strains_db: str|LiteralString = self.dbs.get('strains', '')
if not strains_db:
return None
async with sqlite3.connect(database=strains_db,
timeout=2) as db_conn:
db_params: Optional[tuple] = None
if not strain:
db_query: str = "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1"
else:
db_query = "SELECT name, description FROM strains_w_desc WHERE name LIKE ?"
db_params = (f"%{strain.strip()}%",)
async with await db_conn.execute(db_query, db_params) as db_cursor:
db_result: Optional[tuple] = await db_cursor.fetchone()
return db_result
async def get_qajoke(self) -> Optional[tuple]:
"""
Get QA Joke
Returns:
Optional[tuple]
"""
qajoke_db: str|LiteralString = self.dbs.get('qajoke', '')
if not qajoke_db:
return None
async with sqlite3.connect(database=qajoke_db,
timeout=2) as db_conn:
db_query: str = "SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1"
async with await db_conn.execute(db_query) as cursor:
(question, answer) = await cursor.fetchone()
return (question, answer)
return None
async def get_rjoke(self) -> Optional[tuple]:
"""
Get r/joke Joke
Returns:
Optional[tuple]
"""
rjokes_db: str|LiteralString = self.dbs.get('rjokes', '')
if not rjokes_db:
return None
async with sqlite3.connect(database=rjokes_db, timeout=2) as db_conn:
db_query: str = "SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'"
async with await db_conn.execute(db_query) as cursor:
(title, body, score) = await cursor.fetchone()
return (title, body, score)
return None
async def get_random_fact(self) -> str:
"""
Get Random Fact
Returns:
str
"""
try:
facts_api_url: str = "https://uselessfacts.jsph.pl/api/v2/facts/random"
facts_backup_url: str = "https://cnichols1734.pythonanywhere.com/facts/random"
async with ClientSession() as client:
try:
async with await client.get(facts_api_url,
timeout=ClientTimeout(connect=5, sock_read=5)) as request:
_json: dict = await request.json()
fact: str = _json.get('text', None)
if not fact:
raise BaseException("RandFact Src 1 Failed")
return fact
except:
async with await client.get(facts_backup_url,
timeout=ClientTimeout(connect=5, sock_read=5)) as request:
_json = await request.json()
fact = _json.get('fact', None)
return fact
except Exception as e:
traceback.print_exc()
return f"Failed to get a random fact :( [{str(e)}]"
async def get_cookie(self) -> Optional[dict]:
"""
Get Cookie
Returns:
Optional[dict]
"""
cookies_db = self.dbs.get('cookies', '')
if not cookies_db:
return None
async with sqlite3.connect(cookies_db,
timeout=2) as db_conn:
db_query: str = "SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
(name, origin, image_url) = await db_cursor.fetchone()
return {
'name': name,
'origin': origin,
'image_url': image_url
}
def get_coffee(self,
recipient_allergic: Optional[bool] = False) -> Optional[str]:
"""
Get Coffee
Args:
recipient_allergic (bool): Is the recipient allergic? (so we know when to keep our nuts out of it)
Returns:
str
"""
try:
randomCoffee: str = random.choice(self.COFFEES)
if self.LAST_5_COFFEES and randomCoffee in self.LAST_5_COFFEES\
or (recipient_allergic and "nut" in randomCoffee.lower()):
return self.get_coffee() # Recurse
if len(self.LAST_5_COFFEES) >= 5:
self.LAST_5_COFFEES.pop() # Store no more than 5 of the last served coffees
self.LAST_5_COFFEES.append(randomCoffee)
return randomCoffee
except:
traceback.print_exc()
return None
def get_days_to_xmas(self) -> Optional[tuple]:
"""
Get # of Days until Xmas
Returns:
Optional[tuple]
"""
today: datetime.datetime = datetime.datetime.now(tz=pytz.UTC)
xmas: datetime.datetime = datetime.datetime(
year=today.year,
month=12,
day=25,
tzinfo=pytz.UTC,
)
td: datetime.timedelta = (xmas - today)
days, hours, minutes, seconds, us, ms = self.tdTuple(td)
return (days, hours, minutes, seconds, ms, us)
async def get_randmsg(self) -> Optional[str]:
"""
Get Random Message from randmsg.db
Returns:
Optional[str]
"""
randmsg_db: str|LiteralString = self.dbs.get('randmsg', '')
if not randmsg_db:
return None
async with sqlite3.connect(database=randmsg_db,
timeout=2) as db_conn:
db_query: str = "SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
(result,) = await db_cursor.fetchone()
return result

View File

@ -1,6 +1,7 @@
import io import io
import random import random
import asyncio import asyncio
import logging
import traceback import traceback
from typing import Optional from typing import Optional
import discord import discord
@ -9,138 +10,115 @@ from discord.ext import bridge, commands
from disc_havoc import Havoc from disc_havoc import Havoc
import util import util
class Owner(commands.Cog): class Owner(commands.Cog):
"""Owner Cog for Havoc""" """Owner Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.former_roles_store: dict[int, list[discord.Role]] = {} self.former_roles_store: dict[int, list[discord.Role]] = {}
self._temperature: int = random.randrange(20, 30) self._temperature: int = random.randrange(20, 30)
@bridge.bridge_command(guild_ids=[1145182936002482196]) @bridge.bridge_command(guild_ids=[1145182936002482196])
async def temperature(self, ctx, temp: Optional[int|str] = None) -> None: async def temperature(self, ctx, temp: Optional[int | str] = None) -> None:
""" """
Set Temperature Set Temperature
Args:
ctx (Any): Discord context
temperature (Optional[int|str]): New temperature
Returns:
None
""" """
if not temp: if not temp:
return await ctx.respond(f"The current temperature is: {self._temperature} °C") return await ctx.respond(
f"The current temperature is: {self._temperature} °C"
)
if not self.bot.is_owner(ctx.author): if not self.bot.is_owner(ctx.author):
return await ctx.respond("I am afraid I can't let you do that.") return await ctx.respond("I am afraid I can't let you do that.")
try: try:
_temperature: int = int(temp) _temperature: int = int(temp)
except: except Exception as e:
return await ctx.respond("Invalid input") logging.debug("Exception: %s", str(e))
return await ctx.respond("Invalid input")
if _temperature < -15: if _temperature < -15:
return await ctx.respond("Too cold! (-15°C minimum)") return await ctx.respond("Too cold! (-15°C minimum)")
elif _temperature > 35: elif _temperature > 35:
return await ctx.respond("Too hot! (35°C maximum)") return await ctx.respond("Too hot! (35°C maximum)")
self._temperature = _temperature self._temperature = _temperature
return await ctx.respond(f"As per your request, I have adjusted the temperature to {_temperature} °C.") return await ctx.respond(
f"As per your request, I have adjusted the temperature to {_temperature} °C."
)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def editmsg(self, ctx, async def editmsg(self, ctx, msgid: str, *, newcontent: str) -> None:
msgid: str,
*,
newcontent: str
) -> None:
""" """
Edit a message previously sent by the bot Edit a message previously sent by the bot
Args:
ctx (Any): Discord context
msgid (str): Should be an int, the message id to edit
newcontent (str): Content to replace message with
""" """
try: try:
message: Optional[discord.Message] = self.bot.get_message(int(msgid)) message: Optional[discord.Message] = self.bot.get_message(int(msgid))
if not message: if not message:
await ctx.respond(f"**Failed:** Message {msgid} not found.", await ctx.respond(
ephemeral=True) f"**Failed:** Message {msgid} not found.", ephemeral=True
)
return None return None
await message.edit(content=newcontent) await message.edit(content=newcontent)
await ctx.respond("**Done!**", ephemeral=True) await ctx.respond("**Done!**", ephemeral=True)
except Exception as e: except Exception as e:
await ctx.respond(f"**Failed:** {str(e)}", await ctx.respond(f"**Failed:** {str(e)}", ephemeral=True)
ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def reload(self, ctx) -> None: async def reload(self, ctx) -> None:
""" """
Reload Cogs Reload Cogs
Args:
ctx (Any): Discord context
Returns:
None
""" """
self.bot.load_exts(False) self.bot.load_exts(False)
await ctx.respond("Reloaded!", ephemeral=True) await ctx.respond("Reloaded!", ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def say(self, ctx, *, async def say(self, ctx, *, parameters: str) -> None:
parameters: str) -> None:
""" """
Make me say something in a channel Make me say something in a channel
Args:
ctx (Any): Discord context
parameters (str): Channel <space> Message
Returns:
None
""" """
_parameters: list[str] = parameters.split(" ") _parameters: list[str] = parameters.split(" ")
if not len(_parameters) > 1: if not len(_parameters) > 1:
return await ctx.respond("**Error**: Incorrect command usage; required: <chan> <msg>", ephemeral=True) return await ctx.respond(
"**Error**: Incorrect command usage; required: <chan> <msg>",
ephemeral=True,
)
channel: str = _parameters[0] channel: str = _parameters[0]
channel_mentions: list[int] = discord.utils.raw_channel_mentions(channel) channel_mentions: list[int] = discord.utils.raw_channel_mentions(channel)
if channel_mentions: if channel_mentions:
channel = str(channel_mentions[0]) channel = str(channel_mentions[0])
msg: str = " ".join(_parameters[1:]) msg: str = " ".join(_parameters[1:])
await util.discord_helpers.send_message(self.bot, channel=channel, await util.discord_helpers.send_message(self.bot, channel=channel, message=msg)
message=msg)
return await ctx.respond("**Done.**", ephemeral=True) return await ctx.respond("**Done.**", ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def chgstatus(self, ctx, *, async def chgstatus(self, ctx, *, status: Optional[str] = None) -> None:
status: Optional[str] = None) -> None:
""" """
Change bots status Change bots status
Args:
ctx (Any): Discord context
status (Optional[str]): The new status to set
Returns:
None
""" """
if not status: if not status:
return await ctx.respond("ERR: No status provided to change to!", return await ctx.respond(
ephemeral=True) "ERR: No status provided to change to!", ephemeral=True
)
await self.bot.change_presence(status=discord.Status.online,
activity=discord.CustomActivity(name=status.strip())) await self.bot.change_presence(
status=discord.Status.online,
activity=discord.CustomActivity(name=status.strip()),
)
await ctx.respond("Done!", ephemeral=True) await ctx.respond("Done!", ephemeral=True)
@commands.message_command(name="Remove Messages Starting Here") @commands.message_command(name="Remove Messages Starting Here")
@commands.is_owner() @commands.is_owner()
async def purge(self, ctx, message: discord.Message) -> None: async def purge(self, ctx, message: discord.Message) -> None:
""" """
Purge Messages Purge Messages
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
message (discord.Message): Discord message message (discord.Message): Discord message
@ -148,10 +126,12 @@ class Owner(commands.Cog):
None None
""" """
try: try:
await ctx.channel.purge(after=message, await ctx.channel.purge(
bulk=True, after=message,
limit=900000, bulk=True,
reason=f"Purge initiated by {ctx.author.display_name}") limit=900000,
reason=f"Purge initiated by {ctx.author.display_name}",
)
await message.delete(reason=f"Purge initiated by {ctx.author.display_name}") await message.delete(reason=f"Purge initiated by {ctx.author.display_name}")
await ctx.respond("**Done!**") await ctx.respond("**Done!**")
# Wait 3 seconds, then delete interaction # Wait 3 seconds, then delete interaction
@ -161,13 +141,13 @@ class Owner(commands.Cog):
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"**ERR: {str(e)}**") return await ctx.respond(f"**ERR: {str(e)}**")
@commands.message_command(name="Move to Memes") @commands.message_command(name="Move to Memes")
@commands.is_owner() @commands.is_owner()
async def movememe(self, ctx, message: discord.Message) -> None: async def movememe(self, ctx, message: discord.Message) -> None:
""" """
Move to Memes Move to Memes
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
message (discord.Message): Discord message message (discord.Message): Discord message
@ -177,34 +157,40 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
memes_channel: discord.TextChannel = ctx.guild.get_channel(1147229098544988261) memes_channel: discord.TextChannel = ctx.guild.get_channel(
message_content: str = message.content 1147229098544988261
)
message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
_file: Optional[discord.File] = None _file: Optional[discord.File] = None
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
if not _file: if not _file:
return # No file to move return # No file to move
await memes_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...*\n**{message_author}:** {message_content}", file=_file) await memes_channel.send(
f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...*\n**{message_author}:** {message_content}",
file=_file,
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return await ctx.respond("Failed! :(", ephemeral=True) return await ctx.respond("Failed! :(", ephemeral=True)
@commands.message_command(name="Move to Drugs") @commands.message_command(name="Move to Drugs")
@commands.is_owner() @commands.is_owner()
async def movedrugs(self, ctx, message: discord.Message) -> None: async def movedrugs(self, ctx, message: discord.Message) -> None:
""" """
Move to Drugs Move to Drugs
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
message (discord.Message): Discord message message (discord.Message): Discord message
@ -214,7 +200,9 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
drugs_channel: discord.TextChannel = ctx.guild.get_channel(1172247451047034910) drugs_channel: discord.TextChannel = ctx.guild.get_channel(
1172247451047034910
)
message_content: str = message.content message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
@ -222,27 +210,31 @@ class Owner(commands.Cog):
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
if not _file: if not _file:
return # No file to move return # No file to move
await drugs_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\ await drugs_channel.send(
*\n**{message_author}:** {message_content}", file=_file) f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\
*\n**{message_author}:** {message_content}",
file=_file,
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return await ctx.respond("Failed! :(", ephemeral=True) return await ctx.respond("Failed! :(", ephemeral=True)
@commands.message_command(name="Move to fun-house") @commands.message_command(name="Move to fun-house")
@commands.is_owner() @commands.is_owner()
async def movefunhouse(self, ctx, message: discord.Message) -> None: async def movefunhouse(self, ctx, message: discord.Message) -> None:
""" """
Move to fun-house Move to fun-house
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
message (discord.Message): Discord message message (discord.Message): Discord message
@ -252,7 +244,9 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
funhouse_channel: discord.TextChannel = ctx.guild.get_channel(1213160512364478607) funhouse_channel: discord.TextChannel = ctx.guild.get_channel(
1213160512364478607
)
message_content: str = message.content message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
@ -260,25 +254,28 @@ class Owner(commands.Cog):
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
await funhouse_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\ await funhouse_channel.send(
...*\n**{message_author}:** {message_content}") f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\
...*\n**{message_author}:** {message_content}"
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return await ctx.respond("Failed! :(", ephemeral=True) return await ctx.respond("Failed! :(", ephemeral=True)
@commands.user_command(name="Einsperren!", guild_ids=[145182936002482196]) @commands.user_command(name="Einsperren!", guild_ids=[145182936002482196])
@commands.is_owner() @commands.is_owner()
async def einsperren(self, ctx, member: discord.Member) -> None: async def einsperren(self, ctx, member: discord.Member) -> None:
""" """
Einsperren! Einsperren!
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
member (discord.Member): Discord member member (discord.Member): Discord member
@ -287,45 +284,63 @@ class Owner(commands.Cog):
""" """
try: try:
if not ctx.guild.id == 1145182936002482196: if not ctx.guild.id == 1145182936002482196:
return # Not home server! return # Not home server!
if not member.roles: if not member.roles:
return # No roles return # No roles
audit_reason: str = f"Einsperren von {ctx.user.display_name}" audit_reason: str = f"Einsperren von {ctx.user.display_name}"
member = ctx.guild.get_member(member.id) member = ctx.guild.get_member(member.id)
member_display: str = member.display_name member_display: str = member.display_name
einsperren_role: discord.Role = ctx.guild.get_role(1235415059300093973) if ctx.guild.id != 1145182936002482196\ einsperren_role: discord.Role = (
ctx.guild.get_role(1235415059300093973)
if ctx.guild.id != 1145182936002482196
else ctx.guild.get_role(1235406301614309386) else ctx.guild.get_role(1235406301614309386)
member_roles: list = [role for role in member.roles if not role.name == "@everyone"] )
member_role_names: list[str] = [str(role.name).lower() for role in member_roles] member_roles: list = [
role for role in member.roles if not role.name == "@everyone"
]
member_role_names: list[str] = [
str(role.name).lower() for role in member_roles
]
opers_chan: discord.TextChannel = ctx.guild.get_channel(1181416083287187546) opers_chan: discord.TextChannel = ctx.guild.get_channel(1181416083287187546)
if not "einsperren" in member_role_names: if "einsperren" not in member_role_names:
try: try:
if member.id in self.former_roles_store: if member.id in self.former_roles_store:
self.former_roles_store.pop(member.id) self.former_roles_store.pop(member.id)
self.former_roles_store[member.id] = member.roles self.former_roles_store[member.id] = member.roles
except: except: # noqa
pass # Safe to ignore """Safe to ignore"""
pass
try: try:
await member.edit(roles=[einsperren_role], reason=audit_reason) await member.edit(roles=[einsperren_role], reason=audit_reason)
await ctx.respond(f"Gesendet {member_display} an einsperren.", ephemeral=True) await ctx.respond(
await opers_chan.send(f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren.") f"Gesendet {member_display} an einsperren.", ephemeral=True
except: )
await opers_chan.send(
f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren."
)
except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True) return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True)
self.former_roles_store[member.id] = member.roles self.former_roles_store[member.id] = member.roles
if not member.id in self.former_roles_store: if member.id not in self.former_roles_store:
await member.edit(roles=[]) # No roles await member.edit(roles=[]) # No roles
else: else:
former_roles: list = self.former_roles_store.get(member.id, [0]) former_roles: list = self.former_roles_store.get(member.id, [0])
await member.edit(roles=former_roles, reason=f"De-{audit_reason}") await member.edit(roles=former_roles, reason=f"De-{audit_reason}")
await ctx.respond(f"{member_display} wurde von der Einsperre befreit.", ephemeral=True) await ctx.respond(
await opers_chan.send(f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit.") f"{member_display} wurde von der Einsperre befreit.", ephemeral=True
)
await opers_chan.send(
f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit."
)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"ERR: {str(e)}", ephemeral=True) return await ctx.respond(f"ERR: {str(e)}", ephemeral=True)
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Owner(bot)) bot.add_cog(Owner(bot))

329
cogs/quote.py Normal file
View File

@ -0,0 +1,329 @@
#!/usr/bin/env python3.12
# pylint: disable=bare-except, broad-exception-caught
"""
Quote cog for Havoc
"""
import traceback
import time
import os
import datetime
from typing import Optional
import asyncio
import discord
import aiosqlite as sqlite3
from discord.ext import bridge, commands
from disc_havoc import Havoc
class DB:
"""DB Utility for Quote Cog"""
def __init__(self, bot: Havoc):
self.bot: Havoc = bot
self.db_path = os.path.join("/", "usr", "local", "share",
"sqlite_dbs", "quotes.db")
self.hp_chanid = 1157529874936909934
async def get_quote_count(self):
"""Get Quote Count"""
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute("SELECT COUNT (*) FROM quotes") as db_cursor:
result = await db_cursor.fetchone()
return result[-1]
async def remove_quote(self, quote_id: int):
"""Remove Quote from DB"""
try:
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute("DELETE FROM quotes WHERE id = ?", (quote_id,)) as _:
await db_conn.commit()
return True
except Exception as e: # noqa
_channel = self.bot.get_channel(self.hp_chanid)
if isinstance(_channel, discord.TextChannel):
await _channel.send(traceback.format_exc())
return False
async def add_quote(self, message_id: int, channel_id: int,
quoted_member_id: int,
message_time: int,
quoter_friendly: str,
quoted_friendly: str,
channel_friendly: str,
message_content: str,
):
"""Add Quote to DB"""
params = (
quoter_friendly,
int(time.time()),
quoted_friendly,
quoted_member_id,
channel_friendly,
channel_id,
message_id,
message_time,
quoter_friendly,
message_content,
)
try:
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
# pylint: disable=line-too-long
db_conn.row_factory = sqlite3.Row
async with await db_conn.execute("INSERT INTO quotes (added_by, added_at, quoted_user_display, quoted_user_memberid, quoted_channel_display, quoted_channel_id, quoted_message_id, quoted_message_time, added_by_friendly, quoted_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
params) as _: # pylint: enable=line-too-long
await db_conn.commit()
return True
except Exception as e: # noqa
return traceback.format_exc()
async def fetch_quote(self, random: bool = False, quoteid: Optional[int] = None,
added_by: Optional[str] = None, quoted_user: Optional[str] = None,
content: Optional[str] = None):
"""Fetch Quote from DB"""
try:
query_head = "SELECT id, added_by_friendly, added_at, quoted_user_display, quoted_channel_display, quoted_message_time, quoted_message FROM quotes"
query = ""
params: Optional[tuple] = None
if random:
query = f"{query_head} ORDER BY RANDOM() LIMIT 1"
elif quoteid:
query = f"{query_head} WHERE id = ? LIMIT 1"
params = (quoteid,)
elif added_by:
query = f"{query_head} WHERE added_by_friendly LIKE ? ORDER BY RANDOM() LIMIT 5"
params = (f"%{added_by}%",)
elif quoted_user:
query = f"{query_head} WHERE quoted_user_display LIKE ? ORDER BY RANDOM() LIMIT 5"
params = (f"%{quoted_user}%",)
elif content:
query = f"{query_head} WHERE quoted_message LIKE ? ORDER BY RANDOM() LIMIT 5"
params = (f"%{content}%",)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row
async with await db_conn.execute(query, params) as db_cursor:
results = await db_cursor.fetchall()
if not results:
return {
'err': 'No results for query',
}
if random or quoteid:
chosen = results[-1]
return {
str(k): v for k,v in chosen.items()
}
else:
return [
{ str(k): v for k,v in _.items() }
for _ in results
]
except Exception as e: # noqa
return traceback.format_exc()
class Quote(commands.Cog):
"""Quote Cog for Havoc"""
def __init__(self, bot: Havoc):
self.bot: Havoc = bot
self.db = DB(self.bot)
def is_homeserver(): # type: ignore
"""Check if channel/interaction is within homeserver"""
def predicate(ctx):
try:
return ctx.guild.id == 1145182936002482196
except Exception as e: # noqa
traceback.print_exc()
return False
return commands.check(predicate)
@commands.message_command(name="Add Quote")
async def add_quote(self, ctx, message: discord.Message):
"""Add A Quote"""
hp_chanid = 1157529874936909934
try:
if message.author.bot:
return await ctx.respond("Quotes are for real users, not bots.", ephemeral=True)
quoter_friendly = ctx.author.display_name
quoted_message_id = message.id
quoted_channel_friendly = f'#{ctx.channel.name}'
quoted_channel_id = ctx.channel.id
message_content = message.content
message_author_friendly = message.author.display_name
message_author_id = message.author.id
message_time = int(message.created_at.timestamp())
message_escaped = discord.utils.escape_mentions(discord.utils.escape_markdown(message_content)).strip()
if len(message_escaped) < 3:
return await ctx.respond("**Error**: Message (text content) is not long enough to quote.", ephemeral=True)
if len(message_escaped) > 512:
return await ctx.respond("**Error**: Message (text content) is too long to quote.", ephemeral=True)
result = await self.db.add_quote(message_id=quoted_message_id,
channel_id=quoted_channel_id,
quoted_member_id=message_author_id,
message_time=message_time,
quoter_friendly=quoter_friendly,
quoted_friendly=message_author_friendly,
channel_friendly=quoted_channel_friendly,
message_content=message_content)
if not result:
return await ctx.respond("Failed!", ephemeral=True)
else:
return await ctx.respond("OK!", ephemeral=True)
except Exception as e: # noqa
_channel = self.bot.get_channel(hp_chanid)
if not isinstance(_channel, discord.TextChannel):
return
await _channel.send(traceback.format_exc())
@bridge.bridge_command(aliases=['rand'])
@is_homeserver() # pylint: disable=too-many-function-args
async def randquote(self, ctx):
"""Get a random quote"""
try:
random_quote = await self.db.fetch_quote(random=True)
if random_quote.get('err'):
return await ctx.respond("Failed to get a quote")
quote_id = random_quote.get('id')
quoted_friendly = random_quote.get('quoted_user_display', 'Unknown')
adder_friendly = random_quote.get('added_by_friendly', 'Unknown')
message_time = datetime.datetime.fromtimestamp(random_quote.get('quoted_message_time'))
message_channel = random_quote.get('quoted_channel_display')
quote_added_at = datetime.datetime.fromtimestamp(random_quote.get('added_at'))
quote_content = random_quote.get('quoted_message')
embed = discord.Embed(
colour=discord.Colour.orange(),
title=f"Quote #{quote_id}",
)
embed.description = f"**{quoted_friendly}:** {quote_content}"
embed.add_field(name="Original Message Time", value=message_time)
embed.add_field(name="Channel", value=message_channel)
embed.add_field(name="Quote ID", value=quote_id)
embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}")
return await ctx.respond(embed=embed)
except Exception as e: # noqa
error = await ctx.respond(traceback.format_exc())
await asyncio.sleep(10)
await error.delete()
@bridge.bridge_command(aliases=['qg'])
@is_homeserver() # pylint: disable=too-many-function-args
async def quoteget(self, ctx, quoteid):
"""Get a specific quote by ID"""
try:
if not str(quoteid).strip().isnumeric():
return await ctx.respond("**Error**: Quote ID must be numeric.")
fetched_quote = await self.db.fetch_quote(quoteid=quoteid)
if fetched_quote.get('err'):
return await ctx.respond("**Error**: Quote not found")
quote_id = fetched_quote.get('id')
quoted_friendly = fetched_quote.get('quoted_user_display', 'Unknown')
adder_friendly = fetched_quote.get('added_by_friendly', 'Unknown')
message_time = datetime.datetime.fromtimestamp(fetched_quote.get('quoted_message_time'))
message_channel = fetched_quote.get('quoted_channel_display')
quote_added_at = datetime.datetime.fromtimestamp(fetched_quote.get('added_at'))
quote_content = fetched_quote.get('quoted_message')
embed = discord.Embed(
colour=discord.Colour.orange(),
title=f"Quote #{quote_id}",
)
embed.description = f"**{quoted_friendly}:** {quote_content}"
embed.add_field(name="Original Message Time", value=message_time)
embed.add_field(name="Channel", value=message_channel)
embed.add_field(name="Quote ID", value=quote_id)
embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}")
return await ctx.respond(embed=embed)
except Exception as e: # noqa
error = await ctx.respond(traceback.format_exc())
await asyncio.sleep(10)
await error.delete()
@bridge.bridge_command(aliases=['qs'])
@is_homeserver() # pylint: disable=too-many-function-args
async def quotesearch(self, ctx, *, content: str):
"""Search for a quote (by content)"""
try:
found_quotes = await self.db.fetch_quote(content=content)
if isinstance(found_quotes, dict) and found_quotes.get('err'):
return await ctx.respond(f"Quote search failed: {found_quotes.get('err')}")
embeds = []
for quote in found_quotes:
quote_id = quote.get('id')
quoted_friendly = quote.get('quoted_user_display', 'Unknown')
adder_friendly = quote.get('added_by_friendly', 'Unknown')
message_time = datetime.datetime.fromtimestamp(quote.get('quoted_message_time'))
message_channel = quote.get('quoted_channel_display')
quote_added_at = datetime.datetime.fromtimestamp(quote.get('added_at'))
quote_content = quote.get('quoted_message')
# await ctx.respond(f"**{quoted_friendly}**: {quote}")ed_friendly = quote.get('quoted_user_display', 'Unknown')
adder_friendly = quote.get('added_by_friendly', 'Unknown')
message_time = datetime.datetime.fromtimestamp(quote.get('quoted_message_time'))
message_channel = quote.get('quoted_channel_display')
quote_added_at = datetime.datetime.fromtimestamp(quote.get('added_at'))
quote = quote.get('quoted_message')
# await ctx.respond(f"**{quoted_friendly}**: {quote}")
embed = discord.Embed(
colour=discord.Colour.orange(),
title=f"Quote #{quote_id}",
)
embed.description = f"**{quoted_friendly}:** {quote_content}"
embed.add_field(name="Original Message Time", value=str(message_time))
embed.add_field(name="Channel", value=message_channel)
embed.add_field(name="Quote ID", value=quote_id)
embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}")
embeds.append(embed)
return await ctx.respond(embeds=embeds)
except Exception as e:
await ctx.respond(f"Error: {type(e).__name__} - {str(e)}")
@bridge.bridge_command(aliases=['nq'])
@is_homeserver() # pylint: disable=too-many-function-args
async def nquotes(self, ctx):
"""Get # of quotes stored"""
try:
quote_count = await self.db.get_quote_count()
if not quote_count:
return await ctx.respond("**Error**: No quotes found!")
return await ctx.respond(f"I currently have **{quote_count}** quotes stored.")
except Exception as e:
await ctx.respond(f"Error: {type(e).__name__} - {str(e)}")
@bridge.bridge_command(aliases=['qr'])
@commands.is_owner()
@is_homeserver() # pylint: disable=too-many-function-args
async def quoteremove(self, ctx, quoteid):
"""Remove a quote (by id)
Owner only"""
try:
if not str(quoteid).strip().isnumeric():
return await ctx.respond("**Error**: Quote ID must be numeric.")
quoteid = int(quoteid)
remove_quote = await self.db.remove_quote(quoteid)
if not remove_quote:
return await ctx.respond("**Error**: Failed!", ephemeral=True)
return await ctx.respond("Removed!", ephemeral=True)
except Exception as e:
await ctx.respond(f"Error: {type(e).__name__} - {str(e)}")
def setup(bot):
"""Run on Cog Load"""
bot.add_cog(Quote(bot))

View File

@ -6,59 +6,61 @@ from util.radio_util import get_now_playing, skip
import discord import discord
from disc_havoc import Havoc from disc_havoc import Havoc
class Radio(commands.Cog): class Radio(commands.Cog):
"""Radio Cog for Havoc""" """Radio Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.channels: dict[str, tuple] = { self.channels: dict[str, tuple] = {
'sfm': (1145182936002482196, 1221615558492029050), # Tuple: Guild Id, Chan Id "sfm": (
} 1145182936002482196,
1221615558492029050,
), # Tuple: Guild Id, Chan Id
}
self.STREAM_URL: str = "https://stream.codey.lol/sfm.ogg" self.STREAM_URL: str = "https://stream.codey.lol/sfm.ogg"
self.LAST_NP_TRACK: Optional[str] = None self.LAST_NP_TRACK: Optional[str] = None
try: try:
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
except Exception as e: except Exception as e:
logging.debug("Failed to cancel radio_state_loop: %s", logging.debug("Failed to cancel radio_state_loop: %s", str(e))
str(e))
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self) -> None: async def on_ready(self) -> None:
"""Run on Bot Ready""" """Run on Bot Ready"""
await self.radio_init() await self.radio_init()
def is_radio_chan(): # type: ignore def is_radio_chan(): # type: ignore
"""Check if channel is radio chan""" """Check if channel is radio chan"""
def predicate(ctx): def predicate(ctx):
try: try:
return ctx.channel.id == 1221615558492029050 return ctx.channel.id == 1221615558492029050
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return False return False
return commands.check(predicate)
return commands.check(predicate)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def reinitradio(self, ctx) -> None: async def reinitradio(self, ctx) -> None:
""" """
Reinitialize serious.FM Reinitialize serious.FM
Args:
ctx (Any): Discord context
Returns:
None
""" """
loop: discord.asyncio.AbstractEventLoop = self.bot.loop loop: discord.asyncio.AbstractEventLoop = self.bot.loop
loop.create_task(self.radio_init()) loop.create_task(self.radio_init())
await ctx.respond("Done!", ephemeral=True) await ctx.respond("Done!", ephemeral=True)
async def radio_init(self) -> None: async def radio_init(self) -> None:
"""Init Radio""" """Init Radio"""
try: try:
(radio_guild, radio_chan) = self.channels['sfm'] (radio_guild, radio_chan) = self.channels["sfm"]
guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild) guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild)
if not guild: if not guild:
return return
channel = guild.get_channel(radio_chan) channel = guild.get_channel(radio_chan)
if not isinstance(channel, discord.VoiceChannel): if not isinstance(channel, discord.VoiceChannel):
return return
if not self.bot.voice_clients: if not self.bot.voice_clients:
@ -67,26 +69,29 @@ class Radio(commands.Cog):
try: try:
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
except Exception as e: except Exception as e:
logging.debug("Failed to cancel radio_state_loop: %s", logging.debug("Failed to cancel radio_state_loop: %s", str(e))
str(e))
self.radio_state_loop.start() self.radio_state_loop.start()
logging.info("radio_state_loop task started!") logging.info("radio_state_loop task started!")
except: except Exception as e:
logging.critical("Could not start task...") logging.critical("Could not start task... Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return return
@tasks.loop(seconds=5.0) @tasks.loop(seconds=5.0)
async def radio_state_loop(self) -> None: async def radio_state_loop(self) -> None:
"""Radio State Loop""" """Radio State Loop"""
try: try:
(radio_guild, radio_chan) = self.channels['sfm'] (radio_guild, radio_chan) = self.channels["sfm"]
try: try:
vc: discord.VoiceProtocol = self.bot.voice_clients[-1] vc: discord.VoiceProtocol = self.bot.voice_clients[-1]
except: except Exception as e:
logging.debug("No voice client, establishing new VC connection...") logging.debug(
"No voice client, establishing new VC connection... (Exception: %s)",
str(e),
)
guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild) guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild)
if not guild: if not guild:
return return
@ -95,46 +100,46 @@ class Radio(commands.Cog):
return return
await channel.connect() await channel.connect()
vc = self.bot.voice_clients[-1] vc = self.bot.voice_clients[-1]
if not vc.is_playing() or vc.is_paused(): # type: ignore if not vc.is_playing() or vc.is_paused(): # type: ignore
""" """
Mypy does not seem aware of the is_playing, play, and is_paused methods, Mypy does not seem aware of the is_playing, play, and is_paused methods,
but they exist. but they exist.
""" """
logging.info("Detected VC not playing... playing!") logging.info("Detected VC not playing... playing!")
source: discord.FFmpegAudio = discord.FFmpegOpusAudio(self.STREAM_URL, source: discord.FFmpegAudio = discord.FFmpegOpusAudio(self.STREAM_URL)
before_options="-timeout 3000000") vc.play( # type: ignore
vc.play(source, # type: ignore source,
after=lambda e: logging.info("Error: %s", e) if e\ after=lambda e: logging.info("Error: %s", e) if e else None,
else None) )
# Get Now Playing (disabled) # Get Now Playing
# np_track: Optional[str] = await get_now_playing() np_track: Optional[str] = await get_now_playing()
# if np_track and not self.LAST_NP_TRACK == np_track: if np_track and not self.LAST_NP_TRACK == np_track:
# self.LAST_NP_TRACK = np_track self.LAST_NP_TRACK = np_track
# if isinstance(vc.channel, discord.VoiceChannel): await self.bot.change_presence(
# await vc.channel.set_status(f"Now playing: {np_track}") activity=discord.Activity(
except: type=discord.ActivityType.listening, name=np_track
)
)
except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def skip(self, ctx) -> None: async def skip(self, ctx) -> None:
""" """
Skip - Convenience Command Skip - Convenience Command
Args:
ctx (Any)
Returns:
None
""" """
await skip() await skip()
return await ctx.respond("OK", ephemeral=True) return await ctx.respond("OK", ephemeral=True)
def cog_unload(self) -> None: def cog_unload(self) -> None:
"""Run on Cog Unload""" """Run on Cog Unload"""
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Radio(bot)) bot.add_cog(Radio(bot))

View File

@ -2,7 +2,6 @@ import traceback
import logging import logging
from typing import Optional, Union from typing import Optional, Union
from regex import Pattern from regex import Pattern
import urllib
import discord import discord
import regex import regex
from util.sing_util import Utility from util.sing_util import Utility
@ -11,44 +10,42 @@ from disc_havoc import Havoc
BOT_CHANIDS = [] BOT_CHANIDS = []
class Sing(commands.Cog): class Sing(commands.Cog):
"""Sing Cog for Havoc""" """Sing Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.utility = Utility() self.utility = Utility()
global BOT_CHANIDS global BOT_CHANIDS
BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit
self.control_strip_regex: Pattern = regex.compile(r"\x0f|\x1f|\035|\002|\u2064|\x02|(\x03([0-9]{1,2}))|(\x03|\003)(?:\d{1,2}(?:,\d{1,2})?)?", self.control_strip_regex: Pattern = regex.compile(
regex.UNICODE) r"\x0f|\x1f|\035|\002|\u2064|\x02|(\x03([0-9]{1,2}))|(\x03|\003)(?:\d{1,2}(?:,\d{1,2})?)?",
regex.UNICODE,
def is_spamchan(): # type: ignore )
def is_spamchan(): # type: ignore
"""Check if channel is spam chan""" """Check if channel is spam chan"""
def predicate(ctx): def predicate(ctx):
try: try:
if not ctx.channel.id in BOT_CHANIDS: if ctx.channel.id not in BOT_CHANIDS:
logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS) logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS)
return ctx.channel.id in BOT_CHANIDS return ctx.channel.id in BOT_CHANIDS
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return False return False
return commands.check(predicate)
return commands.check(predicate)
@bridge.bridge_command(aliases=['sing'])
async def s(self, ctx, *, @bridge.bridge_command(aliases=["sing"])
song: Optional[str] = None) -> None: async def s(self, ctx, *, song: Optional[str] = None) -> None:
""" """
Search for lyrics, format is artist : song. Also reads activity. Search for lyrics, format is artist : song. Also reads activity.
Args:
ctx (Any): Discord context
song (Optional[str]): Song to search
Returns:
None
""" """
try: try:
with ctx.channel.typing(): with ctx.channel.typing():
interaction: bool = isinstance(ctx,
discord.ext.bridge.BridgeApplicationContext)
activity: Optional[discord.Activity] = None activity: Optional[discord.Activity] = None
if not song: if not song:
if not ctx.author.activities: if not ctx.author.activities:
@ -57,73 +54,81 @@ class Sing(commands.Cog):
for _activity in ctx.author.activities: for _activity in ctx.author.activities:
if _activity.type == discord.ActivityType.listening: if _activity.type == discord.ActivityType.listening:
activity = _activity activity = _activity
if not activity:
return await ctx.respond("**Error**: No song specified, no activity found to read.")
if interaction: if not activity:
await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord return await ctx.respond(
"**Error**: No song specified, no activity found to read."
)
await ctx.respond(
"*Searching...*"
) # Must respond to interactions within 3 seconds, per Discord
parsed = self.utility.parse_song_input(song, activity) parsed = self.utility.parse_song_input(song, activity)
if isinstance(parsed, tuple): if isinstance(parsed, tuple):
(search_artist, search_song, search_subsearch) = parsed (search_artist, search_song, search_subsearch) = parsed
# await ctx.respond(f"So, {search_song} by {search_artist}? Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging # await ctx.respond(f"So, {search_song} by {search_artist}? Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song, search_result: Optional[list] = await self.utility.lyric_search(
search_subsearch) search_artist, search_song, search_subsearch
)
if not search_result: if not search_result:
await ctx.respond("ERR: No search result.") await ctx.respond("ERR: No search result.")
return return
if len(search_result) == 1: if len(search_result) == 1:
return await ctx.respond("ERR: Not found!") # Error response from API
error, *_ = search_result[0]
return await ctx.respond(error)
if not isinstance(search_result[0], tuple): if not isinstance(search_result[0], tuple):
return # Invalid data type return # Invalid data type
( (
search_result_artist, search_result_song, search_result_src, search_result_artist,
search_result_confidence, search_result_time_taken search_result_song,
) = search_result[0] # First index is a tuple search_result_src,
search_result_wrapped: list[str] = search_result[1] # Second index is the wrapped lyrics search_result_confidence,
search_result_wrapped_short: list[str] = search_result[2] # Third is short wrapped lyrics search_result_time_taken,
if not ctx.channel.id in BOT_CHANIDS: ) = search_result[0] # First index is a tuple
search_result_wrapped = search_result_wrapped_short # Replace with shortened lyrics for non spamchans search_result_wrapped: list[str] = search_result[
embeds: list[Optional[discord.Embed]] = [] 1
embed_url: str = f"[on codey.lol](https://codey.lol/#{urllib.parse.quote(search_artist)}/{urllib.parse.quote(search_song)})" ] # Second index is the wrapped lyrics
c: int = 0 search_result_wrapped_short: list[str] = search_result[
footer: str = "To be continued..." #Placeholder 2
for section in search_result_wrapped: ] # Third is short wrapped lyrics
c+=1 if ctx.channel.id not in BOT_CHANIDS:
if c == len(search_result_wrapped): short_lyrics = " ".join(
footer = f"Found on: {search_result_src}" search_result_wrapped_short
section = self.control_strip_regex.sub('', section) ) # Replace with shortened lyrics for non spamchans
# if ctx.guild.id == 1145182936002482196: short_lyrics = regex.sub(
# section = section.upper() r"\p{Vert_Space}", " / ", short_lyrics.strip()
embed: discord.Embed = discord.Embed( )
title=f"{search_result_song} by {search_result_artist}", return await ctx.respond(
description=discord.utils.escape_markdown(section) f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}"
) )
embed.add_field(name="Confidence", value=search_result_confidence,
inline=True) out_messages: list = []
embed.add_field(name="Time Taken", value=search_result_time_taken, footer: str = "" # Placeholder
inline=True) for c, section in enumerate(search_result_wrapped):
embed.add_field(name="Link", value=embed_url) if c == len(search_result_wrapped):
embed.set_footer(text=footer) footer = f"`Found on: {search_result_src}`"
embeds.append(embed) section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
await ctx.respond(embed=embeds[0]) msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
for _embed in embeds[1:]: if c > 1:
if isinstance(_embed, discord.Embed): msg = "\n".join(msg.split("\n")[1:])
await ctx.send(embed=_embed) out_messages.append(msg.strip())
for msg in out_messages:
await ctx.send(msg)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
await ctx.respond(f"ERR: {str(e)}") await ctx.respond(f"ERR: {str(e)}")
@commands.user_command(name="Sing") @commands.user_command(name="Sing")
async def sing_context_menu(self, ctx, member: discord.Member) -> None: async def sing_context_menu(self, ctx, member: discord.Member) -> None:
""" """
Sing Context Menu Command Sing Context Menu Command
Args: Args:
ctx (Any): Discord context ctx (Any): Discord context
member (discord.Member): Discord member member (discord.Member): Discord member
@ -133,70 +138,93 @@ class Sing(commands.Cog):
try: try:
PODY_ID: int = 1172340700663255091 PODY_ID: int = 1172340700663255091
IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS
member_display = ctx.interaction.guild.get_member(member.id)\ member_display = ctx.interaction.guild.get_member(member.id).display_name
.display_name if (
if not(ctx.interaction.guild.get_member(member.id).activities)\ not (ctx.interaction.guild.get_member(member.id).activities)
and not member.id == PODY_ID: and not member.id == PODY_ID
return await ctx.respond(f"No activity detected to read for {member_display}.", ephemeral=True) ):
member_id: int = member.id #if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody! return await ctx.respond(
f"No activity detected to read for {member_display}.",
ephemeral=True,
)
member_id: int = member.id # if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody!
activity: Optional[discord.Activity] = None activity: Optional[discord.Activity] = None
if IS_SPAMCHAN: if IS_SPAMCHAN:
await ctx.respond(f"***Reading activity of {member_display}...***") await ctx.respond(f"***Reading activity of {member_display}...***")
for _activity in ctx.interaction.guild.get_member(member_id).activities: for _activity in ctx.interaction.guild.get_member(member_id).activities:
if _activity.type == discord.ActivityType.listening: if _activity.type == discord.ActivityType.listening:
activity = _activity activity = _activity
parsed: Union[tuple, bool] = self.utility.parse_song_input(song=None, parsed: Union[tuple, bool] = self.utility.parse_song_input(
activity=activity) song=None, activity=activity
)
if not parsed: if not parsed:
return await ctx.respond(f"Could not parse activity of {member_display}.", ephemeral=True) return await ctx.respond(
f"Could not parse activity of {member_display}.", ephemeral=True
)
if isinstance(parsed, tuple): if isinstance(parsed, tuple):
(search_artist, search_song, search_subsearch) = parsed (search_artist, search_song, search_subsearch) = parsed
await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord await ctx.respond(
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song, "*Searching...*"
search_subsearch) ) # Must respond to interactions within 3 seconds, per Discord
search_result: Optional[list] = await self.utility.lyric_search(
search_artist, search_song, search_subsearch
)
if not search_result: if not search_result:
await ctx.respond("ERR: No search result") await ctx.respond("ERR: No search result")
return return
if len(search_result) == 1 and\
isinstance(search_result[0][0], str):
return await ctx.send("ERR: No search result") # Error message from API
(search_result_artist, search_result_song, search_result_src,
search_result_confidence, search_result_time_taken) = search_result[0] # First index is a tuple
search_result_wrapped: list = search_result[1] # Second index is the wrapped lyrics
search_result_wrapped_short: list[str] = search_result[2] # Third index is shortened lyrics
if not IS_SPAMCHAN:
search_result_wrapped = search_result_wrapped_short # Swap for shortened lyrics if not spam chan
embeds: list[Optional[discord.Embed]] = [] if len(search_result) == 1 and isinstance(search_result[0][0], str):
return await ctx.send(
"ERR: No search result"
) # Error message from API
(
search_result_artist,
search_result_song,
search_result_src,
search_result_confidence,
search_result_time_taken,
) = search_result[0] # First index is a tuple
search_result_wrapped: list = search_result[
1
] # Second index is the wrapped lyrics
search_result_wrapped_short: list[str] = search_result[
2
] # Third index is shortened lyrics
if not IS_SPAMCHAN:
short_lyrics = " ".join(
search_result_wrapped_short
) # Replace with shortened lyrics for non spamchans
short_lyrics = regex.sub(
r"\p{Vert_Space}", " / ", short_lyrics.strip()
)
return await ctx.respond(
f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}"
)
out_messages: list = []
footer: str = ""
c: int = 0 c: int = 0
footer: str = "To be continued..." #Placeholder
for section in search_result_wrapped: for section in search_result_wrapped:
c+=1 c += 1
if c == len(search_result_wrapped): if c == len(search_result_wrapped):
footer = f"Found on: {search_result_src}" footer = f"`Found on: {search_result_src}`"
# if ctx.guild.id == 1145182936002482196: # if ctx.guild.id == 1145182936002482196:
# section = section.upper() # section = section.upper()
embed: discord.Embed = discord.Embed( section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
title=f"{search_result_song} by {search_result_artist}", msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
description=discord.utils.escape_markdown(section) if c > 1:
) msg = "\n".join(msg.split("\n")[1:])
embed.add_field(name="Confidence", value=search_result_confidence, inline=True) out_messages.append(msg.strip())
embed.add_field(name="Time Taken", value=search_result_time_taken, inline=True) for msg in out_messages:
embed.add_field(name="Link", value=f"[on codey.lol](https://codey.lol/#{urllib.parse.quote(search_result_artist)}/{urllib.parse.quote(search_result_song)})") await ctx.send(msg)
embed.set_footer(text=footer)
embeds.append(embed)
for _embed in embeds:
await ctx.send(embed=_embed)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"ERR: {str(e)}") return await ctx.respond(f"ERR: {str(e)}")
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Sing(bot)) bot.add_cog(Sing(bot))

View File

@ -1,22 +1,25 @@
""" """
AI AI
""" """
class AIException(Exception): class AIException(Exception):
"""AI Exception (generic)""" """AI Exception (generic)"""
pass
""" """
LoveHate LoveHate
""" """
class LoveHateException(Exception): class LoveHateException(Exception):
"""Love Hate Exception (generic)""" """Love Hate Exception (generic)"""
pass
""" """
Misc Misc
""" """
class MiscException(Exception): class MiscException(Exception):
"""Misc Exception (generic)""" """Misc Exception (generic)"""
pass

View File

@ -13,20 +13,19 @@ from termcolor import colored
from constants import OWNERS, BOT_CHANIDS from constants import OWNERS, BOT_CHANIDS
import api import api
logging.basicConfig(level=logging.INFO, logging.basicConfig(
format='%(asctime)s %(message)s', level=logging.INFO, format="%(asctime)s %(message)s", encoding="utf-8"
encoding='utf-8') )
setproctitle.setproctitle('disc-havoc') setproctitle.setproctitle("disc-havoc")
"""Auto Load Cogs""" """Auto Load Cogs"""
cogs_list: list[str] = [ cogs_list: list[str] = [
'misc', "misc",
'owner', "owner",
'sing', "sing",
'meme', "meme",
'karma', "lovehate",
'lovehate', "radio",
'radio',
] ]
bot_activity = discord.CustomActivity(name="I made cookies!") bot_activity = discord.CustomActivity(name="I made cookies!")
@ -36,60 +35,68 @@ load_dotenv()
intents = discord.Intents.all() intents = discord.Intents.all()
intents.message_content = True intents.message_content = True
class Havoc(bridge.Bot): class Havoc(bridge.Bot):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__(command_prefix=".", intents=intents, super().__init__(
owner_ids=OWNERS, activity=bot_activity, command_prefix=".",
help_command=commands.MinimalHelpCommand()) intents=intents,
owner_ids=OWNERS,
activity=bot_activity,
help_command=commands.MinimalHelpCommand(),
)
self.BOT_CHANIDS = BOT_CHANIDS self.BOT_CHANIDS = BOT_CHANIDS
self.load_exts() self.load_exts()
def load_exts(self, initialRun: Optional[bool] = True) -> None: def load_exts(self, initialRun: Optional[bool] = True) -> None:
""" """
Load Cogs/Extensions Load Cogs/Extensions
Args: Args:
initialRun (Optional[bool]) default: True initialRun (Optional[bool]) default: True
Returns: Returns:
None None
""" """
load_method = self.load_extension if initialRun\ load_method = self.load_extension if initialRun else self.reload_extension
else self.reload_extension
for cog in cogs_list: for cog in cogs_list:
logging.info("Loading: %s", cog) logging.info("Loading: %s", cog)
load_method(f'cogs.{cog}') load_method(f"cogs.{cog}")
importlib.reload(api) importlib.reload(api)
from api import API from api import API # noqa (voodoo)
api_config = hypercorn.config.Config() api_config = hypercorn.config.Config()
api_config.bind = ["127.0.0.1:5992"] api_config.bind = ["127.0.0.1:5992"]
api_instance = api.API(self) api_instance = api.API(self)
try: try:
self.fapi_task.cancel() self.fapi_task.cancel()
except Exception as e: except Exception as e:
logging.debug("Failed to cancel fapi_task: %s", logging.debug("Failed to cancel fapi_task: %s", str(e))
str(e))
logging.info("Starting FAPI Task") logging.info("Starting FAPI Task")
self.fapi_task: Task = self.loop.create_task(hypercorn.asyncio.serve(api_instance.api_app, self.fapi_task: Task = self.loop.create_task(
api_config)) hypercorn.asyncio.serve(api_instance.api_app, api_config)
)
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self) -> None: async def on_ready(self) -> None:
"""Run on Bot Ready""" """Run on Bot Ready"""
logging.info("%s online!", self.user) logging.info("%s online!", self.user)
def __init__() -> None: def __init__() -> None:
logging.info(colored(f"Log level: {logging.getLevelName(logging.root.level)}", logging.info(
"red", attrs=['reverse'])) colored(
f"Log level: {logging.getLevelName(logging.root.level)}",
"red",
attrs=["reverse"],
)
)
bot = Havoc() bot = Havoc()
bot.run(os.getenv('TOKEN')) bot.run(os.getenv("TOKEN"))
if __name__ == "__main__": if __name__ == "__main__":
__init__() __init__()

View File

@ -4,7 +4,7 @@ edge_tts==6.1.12
feedparser==6.0.11 feedparser==6.0.11
Flask==3.0.3 Flask==3.0.3
nvdlib==0.7.7 nvdlib==0.7.7
openai==1.54.3 openai==1.54.3d
requests_async==0.6.2 requests_async==0.6.2
shazamio==0.7.0 shazamio==0.7.0
streamrip==2.0.5 streamrip==2.0.5

View File

@ -12,14 +12,20 @@ from util.catbox import CatboxAsync
logger = logging.getLogger() logger = logging.getLogger()
logger.setLevel(logging.INFO) logger.setLevel(logging.INFO)
async def test() -> None: async def test() -> None:
f = os.path.join(os.path.expanduser("~"), "qu.png") f = os.path.join(os.path.expanduser("~"), "qu.png")
box1: LitterboxAsync = LitterboxAsync() box1: LitterboxAsync = LitterboxAsync()
box2: CatboxAsync = CatboxAsync() box2: CatboxAsync = CatboxAsync()
url1: Optional[str] = await box1.upload(f) url1: Optional[str] = await box1.upload(f)
url2: Optional[str] = await box2.upload(f) url2: Optional[str] = await box2.upload(f)
logging.info("""Uploaded URLs: logging.info(
"""Uploaded URLs:
Litter - %s\n Litter - %s\n
Cat - %s""", url1, url2) Cat - %s""",
url1,
asyncio.run(test()) url2,
)
asyncio.run(test())

View File

@ -1,4 +1,4 @@
import importlib import importlib
from . import discord_helpers from . import discord_helpers
importlib.reload(discord_helpers) importlib.reload(discord_helpers)

View File

@ -12,35 +12,36 @@ Catbox Uploader (Async)
catbox_api_url: str = "https://catbox.moe/user/api.php" catbox_api_url: str = "https://catbox.moe/user/api.php"
http_headers: dict[str, str] = { http_headers: dict[str, str] = {
'accept': '*/*', "accept": "*/*",
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53', "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53",
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8,es;q=0.7', "Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7",
'referer': 'https://www.google.com/', "referer": "https://www.google.com/",
'cookie': 'DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0' "cookie": "DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0",
} }
class CatboxAsync: class CatboxAsync:
def __init__(self) -> None: def __init__(self) -> None:
self.catbox_api_url = catbox_api_url self.catbox_api_url = catbox_api_url
self.headers = http_headers self.headers = http_headers
def generateRandomFileName(self, fileExt: Optional[str] = None) -> str: def generateRandomFileName(self, fileExt: Optional[str] = None) -> str:
""" """
Generate random file name Generate random file name
Args: Args:
fileExt (Optional[str]): File extension to use for naming fileExt (Optional[str]): File extension to use for naming
Returns: Returns:
str str
""" """
if not fileExt: if not fileExt:
fileExt = 'png' fileExt = "png"
return f"{random.getrandbits(32)}.{fileExt}" return f"{random.getrandbits(32)}.{fileExt}"
async def upload(self, file: str) -> Optional[str]: async def upload(self, file: str) -> Optional[str]:
""" """
Upload file to catbox Upload file to catbox
Args: Args:
file (str): Path of file to be uploaded file (str): Path of file to be uploaded
Returns: Returns:
@ -49,40 +50,41 @@ class CatboxAsync:
try: try:
if not file: if not file:
return None return None
if not(os.path.exists(file)): if not (os.path.exists(file)):
logging.critical("Could not find %s", logging.critical("Could not find %s", file)
file)
return None return None
fileExt: Optional[str] = None fileExt: Optional[str] = None
if file.find(".") > 0: if file.find(".") > 0:
fileExt = "".join(file.split(".")[-1:]) fileExt = "".join(file.split(".")[-1:])
with open(file, 'rb') as fileContents: with open(file, "rb") as fileContents:
post_data: FormData = FormData() post_data: FormData = FormData()
post_data.add_field(name="reqtype", post_data.add_field(name="reqtype", value="fileupload")
value="fileupload") post_data.add_field(name="userhash", value="")
post_data.add_field(name="userhash",
value="")
with magic.Magic(flags=magic.MAGIC_MIME) as m: with magic.Magic(flags=magic.MAGIC_MIME) as m:
content_type = m.id_filename(file) content_type = m.id_filename(file)
post_data.add_field(name="fileToUpload", post_data.add_field(
value=fileContents, name="fileToUpload",
filename=self.generateRandomFileName(fileExt), value=fileContents,
content_type=content_type filename=self.generateRandomFileName(fileExt),
) content_type=content_type,
)
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.catbox_api_url, async with await session.post(
headers=self.headers, self.catbox_api_url,
data=post_data, headers=self.headers,
timeout=ClientTimeout(connect=10, sock_read=10)) as request: data=post_data,
timeout=ClientTimeout(connect=10, sock_read=10),
) as request:
request.raise_for_status() request.raise_for_status()
return await request.text() return await request.text()
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return None return None
finally: finally:
try: try:
fileContents.close() fileContents.close()
except: except: # noqa
return None return None

View File

@ -6,11 +6,13 @@ from typing import Optional, Any
Discord Helper Methods Discord Helper Methods
""" """
async def get_channel_by_name(bot: discord.Bot, channel: str,
guild: int | None = None) -> Optional[Any]: # Optional[Any] used as pycord channel types can be ambigious async def get_channel_by_name(
bot: discord.Bot, channel: str, guild: int | None = None
) -> Optional[Any]: # Optional[Any] used as pycord channel types can be ambigious
""" """
Get Channel by Name Get Channel by Name
Args: Args:
bot (discord.Bot) bot (discord.Bot)
channel (str) channel (str)
@ -18,10 +20,9 @@ async def get_channel_by_name(bot: discord.Bot, channel: str,
Returns: Returns:
Optional[Any] Optional[Any]
""" """
channel = re.sub(r'^#', '', channel.strip()) channel = re.sub(r"^#", "", channel.strip())
if not guild: if not guild:
return discord.utils.get(bot.get_all_channels(), return discord.utils.get(bot.get_all_channels(), name=channel)
name=channel)
else: else:
_guild: Optional[discord.Guild] = bot.get_guild(guild) _guild: Optional[discord.Guild] = bot.get_guild(guild)
if not _guild: if not _guild:
@ -32,12 +33,14 @@ async def get_channel_by_name(bot: discord.Bot, channel: str,
return _channel return _channel
return None return None
async def send_message(bot: discord.Bot, channel: str,
message: str, guild: int | None = None) -> None: async def send_message(
bot: discord.Bot, channel: str, message: str, guild: int | None = None
) -> None:
""" """
Send Message to the provided channel. If guild is provided, will limit to channels within that guild to ensure the correct Send Message to the provided channel. If guild is provided, will limit to channels within that guild to ensure the correct
channel is selected. Useful in the event a channel exists in more than one guild that the bot resides in. channel is selected. Useful in the event a channel exists in more than one guild that the bot resides in.
Args: Args:
bot (discord.Bot) bot (discord.Bot)
channel (str) channel (str)
@ -50,9 +53,20 @@ async def send_message(bot: discord.Bot, channel: str,
channel_int: int = int(channel) channel_int: int = int(channel)
_channel = bot.get_channel(channel_int) _channel = bot.get_channel(channel_int)
else: else:
channel = re.sub(r'^#', '', channel.strip()) channel = re.sub(r"^#", "", channel.strip())
_channel = await get_channel_by_name(bot=bot, _channel = await get_channel_by_name(bot=bot, channel=channel, guild=guild)
channel=channel, guild=guild)
if not isinstance(_channel, discord.TextChannel): if not isinstance(_channel, discord.TextChannel):
return None return None
await _channel.send(message) await _channel.send(message)
async def log_to_playground(bot: discord.Bot, message: str) -> None:
"""
Send {message} to playground/log chan
Args:
bot (discord.Bot): Bot instance
message (str): The message to send
Returns:
None
"""
return await send_message(bot=bot, channel="havoc-playground", message=message)

View File

@ -1,9 +1,9 @@
import aiohttp import aiohttp
from typing import Optional from typing import Optional
import regex import regex
from regex import Pattern from regex import Pattern
import os import os
import random import random
import logging import logging
import traceback import traceback
from util.catbox import CatboxAsync from util.catbox import CatboxAsync
@ -13,66 +13,67 @@ Jesus Meme Generator
(requires Catbox uploader) (requires Catbox uploader)
""" """
class JesusMemeGenerator():
class JesusMemeGenerator:
def __init__(self) -> None: def __init__(self) -> None:
self.MEMEAPIURL = "https://apimeme.com/meme?meme=" self.MEMEAPIURL = "https://apimeme.com/meme?meme="
self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"), self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"), "memes")
"memes")
self.top_line_regex: Pattern = regex.compile( self.top_line_regex: Pattern = regex.compile(
r'[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]' r"[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]"
) )
self.bottom_line_regex: Pattern = regex.compile( self.bottom_line_regex: Pattern = regex.compile(
r'[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]' r"[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]"
) )
self.url_regex_1: Pattern = regex.compile( self.url_regex_1: Pattern = regex.compile(r"\p{Horiz_Space}")
r'\p{Horiz_Space}') self.url_regex_2: Pattern = regex.compile(r"#")
self.url_regex_2: Pattern = regex.compile(
r'#') async def create_meme(
self, top_line: str, bottom_line: str, meme="Jesus-Talking-To-Cool-Dude"
async def create_meme(self, top_line: str, bottom_line: str, ) -> Optional[str]:
meme="Jesus-Talking-To-Cool-Dude") -> Optional[str]:
""" """
Create Meme Create Meme
Args: Args:
top_line (str): Top line of meme top_line (str): Top line of meme
bottom_line (str): Bottom line of meme bottom_line (str): Bottom line of meme
meme (str): The meme to use, defaults to Jesus-Talking-To-Cool-Dude meme (str): The meme to use, defaults to Jesus-Talking-To-Cool-Dude
Returns: Returns:
Optional[str] Optional[str]
""" """
try: try:
if not top_line or not bottom_line: if not top_line or not bottom_line:
return None return None
top_line = self.top_line_regex.sub('', top_line = self.top_line_regex.sub("", top_line.strip())
top_line.strip()) bottom_line = self.bottom_line_regex.sub("", bottom_line.strip())
bottom_line = self.bottom_line_regex.sub('',
bottom_line.strip())
out_fname: Optional[str] = None out_fname: Optional[str] = None
if len(top_line) < 1 or len(bottom_line) < 1: if len(top_line) < 1 or len(bottom_line) < 1:
return None return None
formed_url: str = f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}" formed_url: str = f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}"
formed_url = self.url_regex_1.sub('+', self.url_regex_2.sub('%23', formed_url.strip())) formed_url = self.url_regex_1.sub(
"+", self.url_regex_2.sub("%23", formed_url.strip())
)
timeout = aiohttp.ClientTimeout(total=15) timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(formed_url) as response: async with session.get(formed_url) as response:
UUID = f"{random.getrandbits(8)}-{random.getrandbits(8)}" UUID = f"{random.getrandbits(8)}-{random.getrandbits(8)}"
out_fname = f"{UUID}.jpg" out_fname = f"{UUID}.jpg"
with open(f"{self.MEMESTORAGEDIR}/{out_fname}", 'wb') as f: with open(f"{self.MEMESTORAGEDIR}/{out_fname}", "wb") as f:
f.write(await response.read()) f.write(await response.read())
if not out_fname: if not out_fname:
uploader = CatboxAsync() uploader = CatboxAsync()
meme_link: Optional[str] = await uploader.upload(f"{self.MEMESTORAGEDIR}/{out_fname}") meme_link: Optional[str] = await uploader.upload(
f"{self.MEMESTORAGEDIR}/{out_fname}"
)
if not meme_link: if not meme_link:
logging.info("Meme upload failed!") logging.info("Meme upload failed!")
return None return None
return meme_link return meme_link
except: except Exception as e:
print(traceback.format_exc()) logging.debug("Exception: %s", str(e))
pass traceback.print_exc()
return None return None

View File

@ -13,37 +13,38 @@ import os
litterbox_api_url: str = "https://litterbox.catbox.moe/resources/internals/api.php" litterbox_api_url: str = "https://litterbox.catbox.moe/resources/internals/api.php"
http_headers: dict[str, str] = { http_headers: dict[str, str] = {
'accept': '*/*', "accept": "*/*",
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53', "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53",
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8,es;q=0.7', "Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7",
'referer': 'https://www.google.com/', "referer": "https://www.google.com/",
'cookie': 'DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0' "cookie": "DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0",
} }
class LitterboxAsync: class LitterboxAsync:
def __init__(self) -> None: def __init__(self) -> None:
self.headers: dict[str, str] = http_headers self.headers: dict[str, str] = http_headers
self.api_path = litterbox_api_url self.api_path = litterbox_api_url
def generateRandomFileName(self, fileExt: Optional[str] = None) -> str: def generateRandomFileName(self, fileExt: Optional[str] = None) -> str:
""" """
Generate Random Filename Generate Random Filename
Args: Args:
fileExt (Optional[str]): File extension to use for naming fileExt (Optional[str]): File extension to use for naming
Returns: Returns:
str str
""" """
if not fileExt: if not fileExt:
fileExt = 'png' fileExt = "png"
return f"{random.getrandbits(32)}.{fileExt}" return f"{random.getrandbits(32)}.{fileExt}"
async def upload(self, async def upload(
file: Union[str, bytes, BufferedReader], self, file: Union[str, bytes, BufferedReader], time="1h"
time='1h') -> Optional[str]: ) -> Optional[str]:
""" """
Upload File to Litterbox Upload File to Litterbox
Args: Args:
file (Union[str, bytes, BufferedReader]): File to upload (accepts either filepath or io.BufferedReader) file (Union[str, bytes, BufferedReader]): File to upload (accepts either filepath or io.BufferedReader)
time (str): Expiration time, default: 1h time (str): Expiration time, default: 1h
@ -59,46 +60,47 @@ class LitterboxAsync:
return None return None
if isinstance(file, BufferedReader): if isinstance(file, BufferedReader):
file = file.read() file = file.read()
fileExt: str = 'png' fileExt: str = "png"
if isinstance(file, str): if isinstance(file, str):
if file.find(".") > 0: if file.find(".") > 0:
fileExt = "".join(file.split(".")[-1:]) fileExt = "".join(file.split(".")[-1:])
file = open(file, 'rb').read() file = open(file, "rb").read()
with magic.Magic(flags=magic.MAGIC_MIME) as m: with magic.Magic(flags=magic.MAGIC_MIME) as m:
if isinstance(file, BufferedReader): if isinstance(file, BufferedReader):
content_type = str(m.id_buffer(file)) content_type = str(m.id_buffer(file))
else: else:
content_type = str(m.id_filename(file)) content_type = str(m.id_filename(file))
post_data: FormData = FormData() post_data: FormData = FormData()
post_data.add_field(name="reqtype", post_data.add_field(name="reqtype", value="fileupload")
value="fileupload") post_data.add_field(name="userhash", value="")
post_data.add_field(name="userhash", post_data.add_field(name="time", value=time)
value="")
post_data.add_field(name="time", post_data.add_field(
value=time) name="fileToUpload",
value=file,
post_data.add_field(name="fileToUpload", filename=self.generateRandomFileName(fileExt),
value=file, content_type=content_type,
filename=self.generateRandomFileName(fileExt),
content_type=content_type
) )
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.api_path, async with await session.post(
headers=self.headers, self.api_path,
data=post_data, headers=self.headers,
timeout=ClientTimeout(connect=5, sock_read=70)) as request: data=post_data,
timeout=ClientTimeout(connect=5, sock_read=70),
) as request:
request.raise_for_status() request.raise_for_status()
return await request.text() return await request.text()
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return None return None
finally: finally:
if isinstance(file, BufferedReader): if isinstance(file, BufferedReader):
try: try:
file.close() file.close()
except: except: # noqa
return None return None

View File

@ -4,24 +4,28 @@ from typing import LiteralString, Optional, Union
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from constructors import LoveHateException from constructors import LoveHateException
class DB: class DB:
"""LoveHate DB Utility Class""" """LoveHate DB Utility Class"""
def __init__(self, bot) -> None: def __init__(self, bot) -> None:
self.db_path: str|LiteralString = os.path.join("/usr/local/share", self.db_path: str | LiteralString = os.path.join(
"sqlite_dbs", "lovehate.db") "/usr/local/share", "sqlite_dbs", "lovehate.db"
)
async def get_wholovehates(self, thing: str, loves: bool = False,
hates: bool = False) -> Union[list[tuple], bool]: async def get_wholovehates(
self, thing: str, loves: bool = False, hates: bool = False
) -> Union[list[tuple], bool]:
""" """
Get a list of users who have professed their love OR hatred for <thing> Get a list of users who have professed their love OR hatred for <thing>
Args: Args:
thing (str): The <thing> to check thing (str): The <thing> to check
loves (bool): Are we looking for loves? loves (bool): Are we looking for loves?
hates (bool): ...or are we looking for hates? hates (bool): ...or are we looking for hates?
Returns: Returns:
Union[list[tuple], bool] Union[list[tuple], bool]
""" """
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?" query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?"
params: tuple = tuple() params: tuple = tuple()
@ -35,8 +39,11 @@ class DB:
flag = 1 flag = 1
elif not hates and not loves: elif not hates and not loves:
raise LoveHateException("Neither loves nor hates were requested") raise LoveHateException("Neither loves nor hates were requested")
params = (thing, flag,) params = (
thing,
flag,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(query, params) as db_cursor: async with await db_conn.execute(query, params) as db_cursor:
result: list[tuple] = await db_cursor.fetchall() result: list[tuple] = await db_cursor.fetchall()
@ -44,28 +51,35 @@ class DB:
if not result: if not result:
return False return False
return result return result
async def get_lovehates(self, loves: bool = False, hates: bool = False, async def get_lovehates(
user: Optional[str] = None, thing: Optional[str] = None) -> Union[list[tuple], bool]: self,
loves: bool = False,
hates: bool = False,
user: Optional[str] = None,
thing: Optional[str] = None,
) -> Union[list[tuple], bool]:
""" """
Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates
Args: Args:
loves (bool): Are we looking for loves? loves (bool): Are we looking for loves?
hates (bool): ...OR are we looking for hates? hates (bool): ...OR are we looking for hates?
user (Optional[str]): the user to query against user (Optional[str]): the user to query against
thing (Optional[str]): ... OR the thing to query against thing (Optional[str]): ... OR the thing to query against
Returns: Returns:
Union[list[tuple], bool] Union[list[tuple], bool]
""" """
query: str = "" query: str = ""
params: tuple = tuple() params: tuple = tuple()
if not user and not thing: if not user and not thing:
raise LoveHateException("Neither a <user> or <thing> was specified to query against") raise LoveHateException(
"Neither a <user> or <thing> was specified to query against"
)
flag: Optional[int] = None flag: Optional[int] = None
if hates and loves: if hates and loves:
@ -79,10 +93,16 @@ class DB:
if user: if user:
query = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?" query = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?"
params = (user, flag,) params = (
user,
flag,
)
elif thing: elif thing:
query = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?" query = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?"
params = (thing, flag,) params = (
thing,
flag,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(query, params) as db_cursor: async with await db_conn.execute(query, params) as db_cursor:
@ -91,22 +111,27 @@ class DB:
return False return False
return result return result
async def check_existence(self, user: str, thing: str) -> Optional[int]: async def check_existence(self, user: str, thing: str) -> Optional[int]:
""" """
Determine whether a user is opinionated on a <thing> Determine whether a user is opinionated on a <thing>
Args: Args:
user (str): The user to check user (str): The user to check
thing (str): The thing to check if the user has an opinion on thing (str): The thing to check if the user has an opinion on
Returns: Returns:
Optional[int] Optional[int]
""" """
params = (user, thing,) params = (
user,
thing,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute("SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?", params) as db_cursor: async with await db_conn.execute(
"SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?",
params,
) as db_cursor:
result = await db_cursor.fetchone() result = await db_cursor.fetchone()
if not result: if not result:
return None return None
@ -116,29 +141,38 @@ class DB:
async def update(self, user: str, thing: str, flag: int) -> str: async def update(self, user: str, thing: str, flag: int) -> str:
""" """
Updates the lovehate database, and returns an appropriate response Updates the lovehate database, and returns an appropriate response
Args: Args:
user (str): The user to update user (str): The user to update
thing (str): The thing the user loves/hates/doesn't care about anymore thing (str): The thing the user loves/hates/doesn't care about anymore
flag (int): int representation of love (1), hate (-1), and dontcare (0) flag (int): int representation of love (1), hate (-1), and dontcare (0)
Returns: Returns:
str str
""" """
if not flag in range(-1, 2): if flag not in range(-1, 2):
raise LoveHateException(f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)") raise LoveHateException(
f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)"
)
db_query: str = "" db_query: str = ""
params: tuple = (user, thing,) params: tuple = (
user,
thing,
)
already_opinionated: Optional[int] = await self.check_existence(user, thing) already_opinionated: Optional[int] = await self.check_existence(user, thing)
if already_opinionated: if already_opinionated:
if flag == 0: if flag == 0:
db_query = "DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?" db_query = (
"DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?"
)
else: else:
loves_or_hates: str = "loves" loves_or_hates: str = "loves"
if already_opinionated == -1: if already_opinionated == -1:
loves_or_hates = "hates" loves_or_hates = "hates"
raise LoveHateException(f"But {user} already {loves_or_hates} {thing}...") raise LoveHateException(
f"But {user} already {loves_or_hates} {thing}..."
)
else: else:
match flag: match flag:
case -1: case -1:
@ -147,19 +181,22 @@ class DB:
db_query = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, 1, ?)" db_query = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, 1, ?)"
case _: case _:
raise LoveHateException("Unknown error, default case matched") raise LoveHateException("Unknown error, default case matched")
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(db_query, params) as db_cursor: async with await db_conn.execute(db_query, params) as db_cursor:
await db_conn.commit() await db_conn.commit()
if db_cursor.rowcount != 1: if db_cursor.rowcount != 1:
raise LoveHateException(f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query") raise LoveHateException(
f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query"
)
match flag: match flag:
case -1: case -1:
return f"We're done here, {user} hates {thing}." return f"We're done here, {user} hates {thing}."
case 0: case 0:
return f"We're done here, {user} no longer cares one way or the other about {thing}." return f"We're done here, {user} no longer cares one way or the other about {thing}."
case 1: case 1:
return f"We're done here, {user} loves {thing}." return f"We're done here, {user} loves {thing}."
case _: case _:
raise LoveHateException("Unknown error, default case matched [2]") raise LoveHateException(
"Unknown error, default case matched [2]"
)

581
util/misc_util.py Normal file
View File

@ -0,0 +1,581 @@
import os
import logging
import traceback
import random
import datetime
import pytz
from typing import Optional, LiteralString, Union
import regex
import aiosqlite as sqlite3
from aiohttp import ClientSession, ClientTimeout
from bohancompliment import ComplimentGenerator
from discord import Embed
class Util:
"""Misc Utility"""
def __init__(self) -> None:
self.URL_URBANDICTIONARY: str = "http://api.urbandictionary.com/v0/define"
self.URL_NO: str = "https://api.codey.lol/misc/no"
self.URL_INSULTAPI: str = "https://insult.mattbas.org/api/insult"
self.COMPLIMENT_GENERATOR = ComplimentGenerator()
self.dbs: dict[str, str | LiteralString] = {
"whisky": os.path.join("/usr/local/share", "sqlite_dbs", "whiskey.db"),
"drinks": os.path.join("/usr/local/share", "sqlite_dbs", "cocktails.db"),
"strains": os.path.join("/usr/local/share", "sqlite_dbs", "strains.db"),
"qajoke": os.path.join("/usr/local/share", "sqlite_dbs", "qajoke.db"),
"rjokes": os.path.join("/usr/local/share", "sqlite_dbs", "rjokes.db"),
"randmsg": os.path.join("/usr/local/share", "sqlite_dbs", "randmsg.db"),
"stats": os.path.join("/usr/local/share", "sqlite_dbs", "havoc_stats.db"),
"cookies": os.path.join("/usr/local/share", "sqlite_dbs", "cookies.db"),
}
self.COFFEES: list = [
"a cup of french-pressed coffee",
"a cup of cold brew",
"a cup of flash brew",
"a cup of Turkish coffee",
"a cup of Moka",
"an espresso",
"a cup of Nescafe coffee",
"an iced coffee",
"a Frappé",
"a freddo cappuccino",
"a cup of Chock full o'Nuts",
"a cup of Folgers",
"a cup of Lavazza",
"a cup of Maxwell House",
"a cup of Moccona",
"a cup of Mr. Brown Coffee",
"a cup of affogato al caffè",
"a cup of Caffè Medici",
"a cup of Café Touba",
"a double-double",
"an indian filter coffee",
"a cup of pocillo",
"a cup of caffè americano",
"a cup of caffè lungo",
"a latte",
"a manilo",
"a flat white",
"a cup of café cubano",
"a cup of caffè crema",
"a cup of cafe zorro",
"an espresso roberto",
"an espresso romano",
"an espresso sara",
"a guillermo",
"a ristretto",
"a cup of melya",
"a cup of caffè marocchino",
"a cup of café miel",
"a cup of café de olla",
"a Mazagran",
"a Palazzo",
"an ice shot",
"a macchiato",
"a cortado",
"a red eye",
"a cappuccino",
"a mocha",
"a café au lait",
"a bicerin",
"a caffè corretto",
"a café bombón",
"a Vienna coffee",
"a flat black",
"a lungo",
"a doppio",
"a ristretto bianco",
"a piccolo latte",
"a gibraltar",
"a breve",
"a café con leche",
"a su café",
"a café del tiempo",
"a java chip frappuccino",
"a pumpkin spice latte",
"a caramel macchiato",
"a white chocolate mocha",
"a hazelnut coffee",
"a toffee nut latte",
"a peppermint mocha",
"a cinnamon dolce latte",
"a coconut milk latte",
"an almond milk cappuccino",
"an oat milk latte",
"a caramel frappuccino",
"a chocolate frappuccino",
"a butter pecan coffee",
"a maple pecan latte",
"a sea salt caramel mocha",
"a nitro cold brew",
"a pumpkin cold brew",
"a honey almond flat white",
"a sweet cream cold brew",
"a matcha latte",
"a golden latte",
"a kopi luwak",
]
self.LAST_5_COFFEES: list = []
def tdTuple(self, td: datetime.timedelta) -> tuple:
"""
Create TimeDelta Tuple
Args:
td (datetime.timedelta)
Returns:
tuple
"""
def _t(t, n):
if t < n:
return (t, 0)
v = t // n
return (t - (v * n), v)
(s, h) = _t(td.seconds, 3600)
(s, m) = _t(s, 60)
(mics, mils) = _t(td.microseconds, 1000)
return (td.days, h, m, s, mics, mils)
async def get_counter(self, counter: Optional[str] = None) -> Optional[dict]:
"""
Get Counter
Args:
counter (Optional[str])
Returns:
Optional[dict]
"""
stats_db: str | LiteralString = self.dbs.get("stats", "")
if not stats_db:
return None
async with sqlite3.connect(stats_db, timeout=3) as db_conn:
db_conn.row_factory = sqlite3.Row
query: str = "SELECT ? FROM stats LIMIT 1"
if not counter:
query = "SELECT * FROM stats LIMIT 1"
async with await db_conn.execute(
query, (counter,) if counter else None
) as db_cursor:
result = await db_cursor.fetchone()
return dict(result)
async def get_stats_embed(self) -> Optional[Embed]:
"""
Get Stats Embed
Returns:
Optional[Embed]
"""
counters: Optional[dict] = await self.get_counter()
if not counters:
return None
embed: Embed = Embed(title="Stats")
counter_message: str = ""
counters_sorted: dict = dict(
sorted(counters.items(), key=lambda item: item[1], reverse=True)
)
for counter, value in counters_sorted.items():
counter = regex.sub(r"_", " ", counter.strip()).title()
counter_message += f"- {value} {counter}\n"
embed.description = counter_message.strip()
return embed
async def increment_counter(self, counter: str) -> bool:
"""
Increment Counter
Args:
counter (str)
Returns:
bool
"""
stats_db: str | LiteralString = self.dbs.get("stats", "")
if not stats_db:
return False
async with sqlite3.connect(stats_db, timeout=3) as db_conn:
async with await db_conn.execute(
f"UPDATE stats SET {counter} = {counter} + 1"
) as db_cursor:
if db_cursor.rowcount < 0:
logging.critical("[increment_counter] Fail! %s", db_cursor.rowcount)
return False
await db_conn.commit()
return True
async def get_ud_def(self, term: str) -> tuple[str, str]:
"""
Get Definition from UD
Args:
term (str)
Returns:
tuple[str, str]
"""
try:
async with ClientSession() as session:
async with await session.get(
self.URL_URBANDICTIONARY,
params={
"term": term,
},
headers={
"content-type": "application/json; charset=utf-8",
},
timeout=ClientTimeout(connect=5, sock_read=5),
) as request:
logging.debug("UD returned: %s", await request.text())
data: dict = await request.json()
if "list" in data:
definitions: list[dict] = data["list"]
if definitions:
definition: dict = definitions[0]
definition_word: str = definition.get("word", "N/A")
definition_text: str = regex.sub(
r"(\r|\n|\r\n)", " ", definition["definition"].strip()
)
return (
definition_word,
definition_text,
) # Tuple: Returned word, returned definition
else:
return (term, "Not found!")
else:
return (term, "Error retrieving data from Urban Dictionary")
except Exception as e:
traceback.print_exc()
return (term, f"ERR: {str(e)}")
async def get_no(self) -> str:
try:
async with ClientSession() as session:
async with await session.get(
self.URL_NO,
headers={
"content-type": "application/json; charset=utf-8",
},
timeout=ClientTimeout(connect=5, sock_read=5),
) as request:
request.raise_for_status()
response = await request.json(encoding="utf-8")
no: str = response.get("no", None)
if not no:
logging.debug(
"Incorrect response received, JSON keys: %s",
response.keys(),
)
return "No."
return no
except Exception as e:
logging.debug("Exception: %s", str(e))
return "No."
async def get_insult(self, recipient: str) -> str:
"""
Get Insult
Args:
recipient (str)
Returns:
str
"""
async with ClientSession() as session:
async with await session.get(
f"{self.URL_INSULTAPI}?who={recipient}"
) as request:
request.raise_for_status()
return await request.text()
async def get_compliment(self, subject: str, language: Optional[str] = "en") -> str:
"""
Get Compliment
Args:
subject (str)
language (Optional[str]) (default: 'en')
Returns:
str
"""
return self.COMPLIMENT_GENERATOR.compliment_in_language(subject, language)
async def get_whisky(self) -> Optional[tuple]:
"""
Get Whisky
Returns:
Optional[tuple]
"""
whisky_db: str | LiteralString = self.dbs.get("whisky", "")
if not whisky_db:
return None
async with sqlite3.connect(database=whisky_db, timeout=2) as db_conn:
db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
db_result: Optional[
Union[sqlite3.Row, tuple]
] = await db_cursor.fetchone()
if not db_result:
return None
(name, category, description) = db_result
name = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", name.strip()),
)
category = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", category.strip()),
)
description = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", description.strip()),
)
return (name, category, description)
async def get_drink(self) -> Optional[tuple]:
"""
Get Drink
Returns:
Optional[tuple]
"""
drinks_db: str | LiteralString = self.dbs.get("drinks", "")
if not drinks_db:
return None
async with sqlite3.connect(database=drinks_db, timeout=2) as db_conn:
db_query: str = (
"SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1"
)
async with await db_conn.execute(db_query) as db_cursor:
db_result: tuple = await db_cursor.fetchone()
(name, ingredients) = db_result
name = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", name.strip()),
)
ingredients = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", ingredients.strip()),
)
ingredients = regex.sub(r"\*", "\u2731", ingredients.strip())
return (name, ingredients)
async def get_strain(self, strain: Optional[str] = None) -> Optional[tuple]:
"""
Get Strain
Args:
strain (Optional[str])
Returns:
Optional[tuple]
"""
strains_db: str | LiteralString = self.dbs.get("strains", "")
if not strains_db:
return None
async with sqlite3.connect(database=strains_db, timeout=2) as db_conn:
db_params: Optional[tuple] = None
if not strain:
db_query: str = "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1"
else:
db_query = (
"SELECT name, description FROM strains_w_desc WHERE name LIKE ?"
)
db_params = (f"%{strain.strip()}%",)
async with await db_conn.execute(db_query, db_params) as db_cursor:
db_result: Optional[tuple] = await db_cursor.fetchone()
return db_result
async def get_qajoke(self) -> Optional[tuple]:
"""
Get QA Joke
Returns:
Optional[tuple]
"""
qajoke_db: str | LiteralString = self.dbs.get("qajoke", "")
if not qajoke_db:
return None
async with sqlite3.connect(database=qajoke_db, timeout=2) as db_conn:
db_query: str = (
"SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1"
)
async with await db_conn.execute(db_query) as cursor:
(question, answer) = await cursor.fetchone()
return (question, answer)
return None
async def get_rjoke(self) -> Optional[tuple]:
"""
Get r/joke Joke
Returns:
Optional[tuple]
"""
rjokes_db: str | LiteralString = self.dbs.get("rjokes", "")
if not rjokes_db:
return None
async with sqlite3.connect(database=rjokes_db, timeout=2) as db_conn:
db_query: str = "SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'"
async with await db_conn.execute(db_query) as cursor:
(title, body, score) = await cursor.fetchone()
return (title, body, score)
return None
async def get_random_fact(self) -> str:
"""
Get Random Fact
Returns:
str
"""
try:
facts_api_url: str = "https://uselessfacts.jsph.pl/api/v2/facts/random"
facts_backup_url: str = (
"https://cnichols1734.pythonanywhere.com/facts/random"
)
async with ClientSession() as client:
try:
async with await client.get(
facts_api_url, timeout=ClientTimeout(connect=5, sock_read=5)
) as request:
_json: dict = await request.json()
fact: str = _json.get("text", None)
if not fact:
raise BaseException("RandFact Src 1 Failed")
return fact
except Exception as e:
logging.debug("Exception: %s", str(e))
async with await client.get(
facts_backup_url, timeout=ClientTimeout(connect=5, sock_read=5)
) as request:
_json = await request.json()
fact = _json.get("fact", None)
return fact
except Exception as e:
traceback.print_exc()
return f"Failed to get a random fact :( [{str(e)}]"
async def get_cookie(self) -> Optional[dict]:
"""
Get Cookie
Returns:
Optional[dict]
"""
cookies_db = self.dbs.get("cookies", "")
if not cookies_db:
return None
async with sqlite3.connect(cookies_db, timeout=2) as db_conn:
db_query: str = (
"SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1"
)
async with await db_conn.execute(db_query) as db_cursor:
(name, origin, image_url) = await db_cursor.fetchone()
return {"name": name, "origin": origin, "image_url": image_url}
def get_coffee(self, recipient_allergic: Optional[bool] = False) -> Optional[str]:
"""
Get Coffee
Args:
recipient_allergic (bool): Is the recipient allergic? (so we know when to keep our nuts out of it)
Returns:
str
"""
try:
randomCoffee: str = random.choice(self.COFFEES)
if (
self.LAST_5_COFFEES
and randomCoffee in self.LAST_5_COFFEES
or (recipient_allergic and "nut" in randomCoffee.lower())
):
return self.get_coffee() # Recurse
if len(self.LAST_5_COFFEES) >= 5:
self.LAST_5_COFFEES.pop() # Store no more than 5 of the last served coffees
self.LAST_5_COFFEES.append(randomCoffee)
return randomCoffee
except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc()
return None
def get_days_to_xmas(self) -> Optional[tuple]:
"""
Get # of Days until Xmas
Returns:
Optional[tuple]
"""
today: datetime.datetime = datetime.datetime.now(tz=pytz.UTC)
xmas: datetime.datetime = datetime.datetime(
year=today.year,
month=12,
day=25,
tzinfo=pytz.UTC,
)
td: datetime.timedelta = xmas - today
days, hours, minutes, seconds, us, ms = self.tdTuple(td)
return (days, hours, minutes, seconds, ms, us)
def get_days_to_halloween(self) -> Optional[tuple]:
"""
Get # of Days until Halloween
Returns:
Optional[tuple]
"""
today: datetime.datetime = datetime.datetime.now(tz=pytz.UTC)
halloween: datetime.datetime = datetime.datetime(
year=today.year,
month=10,
day=31,
tzinfo=pytz.UTC,
)
td: datetime.timedelta = halloween - today
days, hours, minutes, seconds, us, ms = self.tdTuple(td)
return (days, hours, minutes, seconds, ms, us)
async def get_randmsg(self) -> Optional[str]:
"""
Get Random Message from randmsg.db
Returns:
Optional[str]
"""
randmsg_db: str | LiteralString = self.dbs.get("randmsg", "")
if not randmsg_db:
return None
async with sqlite3.connect(database=randmsg_db, timeout=2) as db_conn:
db_query: str = "SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor:
(result,) = await db_cursor.fetchone()
return result

View File

@ -4,6 +4,7 @@ from aiohttp import ClientSession, ClientTimeout
"""Radio Utils""" """Radio Utils"""
async def get_now_playing() -> Optional[str]: async def get_now_playing() -> Optional[str]:
""" """
Get radio now playing Get radio now playing
@ -15,17 +16,21 @@ async def get_now_playing() -> Optional[str]:
np_url: str = "https://api.codey.lol/radio/np" np_url: str = "https://api.codey.lol/radio/np"
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(np_url, headers={ async with await session.post(
'content-type': 'application/json; charset=utf-8', np_url,
}, timeout=ClientTimeout(connect=1.5, sock_read=1.5)) as request: headers={
"content-type": "application/json; charset=utf-8",
},
timeout=ClientTimeout(connect=1.5, sock_read=1.5),
) as request:
request.raise_for_status() request.raise_for_status()
response_json = await request.json() response_json = await request.json()
artistsong = response_json.get('artistsong') artistsong = response_json.get("artistsong")
return artistsong return artistsong
except Exception as e: except Exception as e:
logging.critical("Now playing retrieval failed: %s", logging.critical("Now playing retrieval failed: %s", str(e))
str(e)) return None
return None
async def skip() -> bool: async def skip() -> bool:
""" """
@ -38,13 +43,13 @@ async def skip() -> bool:
try: try:
ls_uri: str = "http://127.0.0.1:29000" ls_uri: str = "http://127.0.0.1:29000"
async with ClientSession() as session: async with ClientSession() as session:
async with session.get(f"{ls_uri}/next", async with session.get(
timeout=ClientTimeout(connect=2, sock_read=2)) as request: f"{ls_uri}/next", timeout=ClientTimeout(connect=2, sock_read=2)
request.raise_for_status() ) as request:
text: Optional[str] = await request.text() request.raise_for_status()
return text == "OK" text: Optional[str] = await request.text()
return text == "OK"
except Exception as e: except Exception as e:
logging.debug("Skip failed: %s", str(e)) logging.debug("Skip failed: %s", str(e))
return False # failsafe return False # failsafe

View File

@ -6,21 +6,24 @@ import traceback
from discord import Activity from discord import Activity
from typing import Optional, Union from typing import Optional, Union
class Utility: class Utility:
"""Sing Utility""" """Sing Utility"""
def __init__(self) -> None: def __init__(self) -> None:
self.api_url: str = "http://127.0.0.1:52111/lyric/search" self.api_url: str = "http://127.0.0.1:52111/lyric/search"
self.api_src: str = "DISC-HAVOC" self.api_src: str = "DISC-HAVOC"
def parse_song_input(self, song: Optional[str] = None, def parse_song_input(
activity: Optional[Activity] = None) -> Union[bool, tuple]: self, song: Optional[str] = None, activity: Optional[Activity] = None
) -> Union[bool, tuple]:
""" """
Parse Song (Sing Command) Input Parse Song (Sing Command) Input
Args: Args:
song (Optional[str]): Song to search song (Optional[str]): Song to search
activity (Optional[discord.Activity]): Discord activity, used to attempt lookup if no song is provided activity (Optional[discord.Activity]): Discord activity, used to attempt lookup if no song is provided
Returns: Returns:
Union[bool, tuple] Union[bool, tuple]
""" """
@ -29,13 +32,18 @@ class Utility:
return False return False
if not song and activity: if not song and activity:
if not activity.name: if not activity.name:
return False # No valid activity found return False # No valid activity found
match activity.name.lower(): match activity.name.lower():
case "codey toons" | "cider" | "sonixd": case "codey toons" | "cider" | "sonixd":
search_artist: str = " ".join(str(activity.state)\ search_artist: str = " ".join(
.strip().split(" ")[1:]) str(activity.state).strip().split(" ")[1:]
search_artist = regex.sub(r"(\s{0,})(\[(spotify|tidal|sonixd|browser|yt music)])$", "", )
search_artist.strip(), flags=regex.IGNORECASE) search_artist = regex.sub(
r"(\s{0,})(\[(spotify|tidal|sonixd|browser|yt music)])$",
"",
search_artist.strip(),
flags=regex.IGNORECASE,
)
search_song = str(activity.details) search_song = str(activity.details)
song = f"{search_artist} : {search_song}" song = f"{search_artist} : {search_song}"
case "tidal hi-fi": case "tidal hi-fi":
@ -43,110 +51,139 @@ class Utility:
search_song = str(activity.details) search_song = str(activity.details)
song = f"{search_artist} : {search_song}" song = f"{search_artist} : {search_song}"
case "spotify": case "spotify":
if not activity.title or not activity.artist: # type: ignore if not activity.title or not activity.artist: # type: ignore
""" """
Attributes exist, but mypy does not recognize them. Ignored. Attributes exist, but mypy does not recognize them. Ignored.
""" """
return False return False
search_artist = str(activity.title) # type: ignore search_artist = str(activity.artist) # type: ignore
search_song = str(activity.artist) # type: ignore search_song = str(activity.title) # type: ignore
song = f"{search_artist} : {search_song}" song = f"{search_artist} : {search_song}"
case "serious.fm" | "cocks.fm" | "something": case "serious.fm" | "cocks.fm" | "something":
if not activity.details: if not activity.details:
song = str(activity.state) song = str(activity.state)
else: else:
search_artist = str(activity.state).rsplit("[", maxsplit=1)[0] # Strip genre search_artist = str(activity.state).rsplit("[", maxsplit=1)[
0
] # Strip genre
search_song = str(activity.details) search_song = str(activity.details)
song = f"{search_artist} : {search_song}" song = f"{search_artist} : {search_song}"
case _: case _:
return False # Unsupported activity detected return False # Unsupported activity detected
search_split_by: str = ":" if not(song) or len(song.split(":")) > 1\ search_split_by: str = (
else "-" # Support either : or - to separate artist/track ":" if not (song) or len(song.split(":")) > 1 else "-"
) # Support either : or - to separate artist/track
if not song: if not song:
return False return False
search_artist = song.split(search_split_by)[0].strip() search_artist = song.split(search_split_by)[0].strip()
search_song = "".join(song.split(search_split_by)[1:]).strip() search_song = "".join(song.split(search_split_by)[1:]).strip()
search_subsearch: Optional[str] = None search_subsearch: Optional[str] = None
if search_split_by == ":" and len(song.split(":")) > 2: # Support sub-search if : is used (per instructions) if (
search_song = song.split(search_split_by)[1].strip() # Reduce search_song to only the 2nd split of : [the rest is meant to be lyric text] search_split_by == ":" and len(song.split(":")) > 2
search_subsearch = "".join(song.split(search_split_by)[2:]) # Lyric text from split index 2 and beyond ): # Support sub-search if : is used (per instructions)
search_song = song.split(
search_split_by
)[
1
].strip() # Reduce search_song to only the 2nd split of : [the rest is meant to be lyric text]
search_subsearch = "".join(
song.split(search_split_by)[2:]
) # Lyric text from split index 2 and beyond
return (search_artist, search_song, search_subsearch) return (search_artist, search_song, search_subsearch)
except: except Exception as e:
logging.debug("Exception: %s", str(e))
traceback.print_exc() traceback.print_exc()
return False return False
async def lyric_search(self, artist: str, song: str, async def lyric_search(
sub: Optional[str] = None) -> Optional[list]: self, artist: str, song: str, sub: Optional[str] = None
) -> Optional[list]:
""" """
Lyric Search Lyric Search
Args: Args:
artist (str): Artist to search artist (str): Artist to search
song (str): Song to search song (str): Song to search
sub (Optional[str]): Lyrics for subsearch sub (Optional[str]): Lyrics for subsearch
Returns: Returns:
Optional[list] Optional[list]
""" """
try: try:
if not artist or not song: if not artist or not song:
return [("FAIL! Artist/Song not provided",)] return [("FAIL! Artist/Song not provided",)]
search_obj: dict = { search_obj: dict = {
'a': artist.strip(), "a": artist.strip(),
's': song.strip(), "s": song.strip(),
'extra': True, "extra": True,
'src': self.api_src, "src": self.api_src,
} }
if len(song.strip()) < 1: if len(song.strip()) < 1:
search_obj.pop('a') search_obj.pop("a")
search_obj.pop('s') search_obj.pop("s")
search_obj['t'] = artist.strip() # Parse failed, try title without sep search_obj["t"] = artist.strip() # Parse failed, try title without sep
if sub and len(sub) >= 2: if sub and len(sub) >= 2:
search_obj['sub'] = sub.strip() search_obj["sub"] = sub.strip()
async with aiohttp.ClientSession() as session: async with aiohttp.ClientSession() as session:
async with await session.post(self.api_url, async with await session.post(
json=search_obj, self.api_url,
timeout=aiohttp.ClientTimeout(connect=5, sock_read=10)) as request: json=search_obj,
timeout=aiohttp.ClientTimeout(connect=5, sock_read=10),
) as request:
request.raise_for_status() request.raise_for_status()
response: dict = await request.json() response: dict = await request.json()
if response.get('err'): if response.get("err"):
return [(f"ERR: {response.get('errorText')}",)] return [(f"ERR: {response.get('errorText')}",)]
out_lyrics = regex.sub(r'<br>', '\u200B\n', response.get('lyrics', '')) out_lyrics = regex.sub(
r"<br>", "\u200b\n", response.get("lyrics", "")
)
response_obj: dict = { response_obj: dict = {
'artist': response.get('artist'), "artist": response.get("artist"),
'song': response.get('song'), "song": response.get("song"),
'lyrics': out_lyrics, "lyrics": out_lyrics,
'src': response.get('src'), "src": response.get("src"),
'confidence': float(response.get('confidence', 0.0)), "confidence": float(response.get("confidence", 0.0)),
'time': float(response.get('time', -1.0)), "time": float(response.get("time", -1.0)),
} }
lyrics = response_obj.get('lyrics') lyrics = response_obj.get("lyrics")
if not lyrics: if not lyrics:
return None return None
response_obj['lyrics'] = textwrap.wrap(text=lyrics.strip(), response_obj["lyrics"] = textwrap.wrap(
width=4000, drop_whitespace=False, text=lyrics.strip(),
replace_whitespace=False, break_long_words=True, width=1500,
break_on_hyphens=True, max_lines=8) drop_whitespace=False,
response_obj['lyrics_short'] = textwrap.wrap(text=lyrics.strip(), replace_whitespace=False,
width=750, drop_whitespace=False, break_long_words=True,
replace_whitespace=False, break_long_words=True, break_on_hyphens=True,
break_on_hyphens=True, max_lines=1) max_lines=8,
)
response_obj["lyrics_short"] = textwrap.wrap(
text=lyrics.strip(),
width=750,
drop_whitespace=False,
replace_whitespace=False,
break_long_words=True,
break_on_hyphens=True,
max_lines=1,
)
return [ return [
( (
response_obj.get('artist'), response_obj.get('song'), response_obj.get('src'), response_obj.get("artist"),
response_obj.get("song"),
response_obj.get("src"),
f"{int(response_obj.get('confidence', -1.0))}%", f"{int(response_obj.get('confidence', -1.0))}%",
f"{response_obj.get('time', -666.0):.4f}s", f"{response_obj.get('time', -666.0):.4f}s",
), ),
response_obj.get('lyrics'), response_obj.get("lyrics"),
response_obj.get('lyrics_short'), response_obj.get("lyrics_short"),
] ]
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return [f"Retrieval failed: {str(e)}"] return [f"Retrieval failed: {str(e)}"]