Compare commits
21 Commits
55f8f29a31
...
main
Author | SHA1 | Date | |
---|---|---|---|
863ebeb919 | |||
80cc3dc1e8 | |||
75addd629c | |||
046ad4f94a | |||
b3f0e084ce | |||
b13c2eec2a | |||
bcc3bc02fa | |||
3dac803305 | |||
6a1fd659e8 | |||
a433dc2fb5 | |||
5bde6ac880 | |||
4d331b7ffd | |||
1bb482315e | |||
d12b066c8e | |||
c7db52efde | |||
11ff1d6447 | |||
6e74c1bb31 | |||
4f3c82f9c0 | |||
436755f884 | |||
6018de870d | |||
e9d0065876 |
14
api.py
14
api.py
@ -6,6 +6,7 @@ from fastapi import FastAPI, HTTPException
|
||||
from pydantic import BaseModel
|
||||
import util
|
||||
|
||||
|
||||
class ValidSendMsgRequest(BaseModel):
|
||||
"""
|
||||
- **guild**: optional, guild id in case multiple channels match (normally first result would be used)
|
||||
@ -17,18 +18,19 @@ class ValidSendMsgRequest(BaseModel):
|
||||
channel: str
|
||||
message: str
|
||||
|
||||
|
||||
class API:
|
||||
"""API [FastAPI Instance] for Havoc"""
|
||||
|
||||
def __init__(self, discord_bot):
|
||||
api_app = FastAPI(title="Havoc API")
|
||||
self.bot = discord_bot
|
||||
self.api_app = api_app
|
||||
|
||||
|
||||
@api_app.get("/{any:path}")
|
||||
def block_get():
|
||||
raise HTTPException(status_code=403, detail="Invalid request")
|
||||
|
||||
|
||||
@api_app.post("/send_msg")
|
||||
async def send_msg_handler(data: ValidSendMsgRequest):
|
||||
await util.discord_helpers.send_message(
|
||||
@ -38,12 +40,14 @@ class API:
|
||||
message=data.message,
|
||||
)
|
||||
return {
|
||||
'result': "presumed_success",
|
||||
"result": "presumed_success",
|
||||
}
|
||||
|
||||
|
||||
|
||||
def __init__():
|
||||
import util
|
||||
|
||||
importlib.reload(util)
|
||||
|
||||
__init__()
|
||||
|
||||
__init__()
|
||||
|
330
cogs/karma.py
330
cogs/karma.py
@ -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))
|
145
cogs/lovehate.py
145
cogs/lovehate.py
@ -5,8 +5,10 @@ from discord.ext import bridge, commands
|
||||
from util.lovehate_db import DB
|
||||
from disc_havoc import Havoc
|
||||
|
||||
|
||||
class LoveHate(commands.Cog):
|
||||
"""LoveHate Cog for Havoc"""
|
||||
|
||||
def __init__(self, bot: Havoc) -> None:
|
||||
self.bot: Havoc = bot
|
||||
self.db = DB(self.bot)
|
||||
@ -14,66 +16,54 @@ class LoveHate(commands.Cog):
|
||||
def join_with_and(self, items: list) -> str:
|
||||
"""
|
||||
Join list with and added before last item
|
||||
|
||||
|
||||
Args:
|
||||
items (list)
|
||||
Returns:
|
||||
str
|
||||
str
|
||||
"""
|
||||
if len(items) > 1:
|
||||
return ', '.join(items[:-1]) + ' and ' + items[-1]
|
||||
return items[0] if items else ''
|
||||
|
||||
return ", ".join(items[:-1]) + " and " + items[-1]
|
||||
return items[0] if items else ""
|
||||
|
||||
@bridge.bridge_command()
|
||||
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.
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
user (Optional[str])
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not user:
|
||||
display_name = ctx.author.display_name
|
||||
loves: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name,
|
||||
loves=True)
|
||||
loves: Union[list[tuple], bool] = await self.db.get_lovehates(
|
||||
user=display_name, loves=True
|
||||
)
|
||||
if not loves:
|
||||
return await ctx.respond("You don't seem to love anything...")
|
||||
|
||||
|
||||
out_loves: list = []
|
||||
if not isinstance(loves, list):
|
||||
return
|
||||
for love in loves:
|
||||
(love,) = love
|
||||
out_loves.append(love)
|
||||
|
||||
|
||||
out_loves_str: str = self.join_with_and(out_loves)
|
||||
return await ctx.respond(f"{ctx.author.mention} loves {out_loves_str}")
|
||||
|
||||
loves = await self.db.get_lovehates(user=user.strip(), loves=True)
|
||||
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)
|
||||
return await ctx.respond(f"{user} loves {out_loves_str}")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def wholoves(self, ctx, *, thing: Optional[str] = None) -> None:
|
||||
"""
|
||||
Check who loves <thing>
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
thing (Optional[str])
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not thing:
|
||||
@ -82,48 +72,43 @@ class LoveHate(commands.Cog):
|
||||
_thing = thing
|
||||
if discord.utils.raw_mentions(_thing):
|
||||
# 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)
|
||||
if not guild:
|
||||
return
|
||||
thing_member: Optional[discord.Member] = guild.get_member(thing_id)
|
||||
if not thing_member:
|
||||
return
|
||||
|
||||
|
||||
_thing = thing_member.display_name
|
||||
|
||||
|
||||
if not _thing:
|
||||
return
|
||||
|
||||
who_loves: Union[list, bool] = await self.db.get_wholovehates(thing=_thing,
|
||||
loves=True)
|
||||
|
||||
who_loves: Union[list, bool] = await self.db.get_wholovehates(
|
||||
thing=_thing, loves=True
|
||||
)
|
||||
if not isinstance(who_loves, list):
|
||||
return await ctx.respond(f"I couldn't find anyone who loves {thing}...")
|
||||
|
||||
|
||||
out_wholoves: list = []
|
||||
for lover in who_loves:
|
||||
(lover,) = lover
|
||||
out_wholoves.append(str(lover))
|
||||
|
||||
optional_s: str = "s" if len(out_wholoves) == 1 else ""
|
||||
|
||||
|
||||
out_wholoves_str: str = self.join_with_and(out_wholoves)
|
||||
|
||||
return await ctx.respond(f"{out_wholoves_str} love{optional_s} {thing}")
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def whohates(self, ctx, *, thing: Optional[str] = None) -> None:
|
||||
"""
|
||||
Check who hates <thing>
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
thing (Optional[str])
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not thing:
|
||||
@ -135,17 +120,18 @@ class LoveHate(commands.Cog):
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
|
||||
if not guild:
|
||||
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)
|
||||
if not thing_member:
|
||||
return
|
||||
_thing = thing_member.display_name
|
||||
_thing = thing_member.display_name
|
||||
|
||||
who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(thing=_thing,
|
||||
hates=True)
|
||||
who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(
|
||||
thing=_thing, hates=True
|
||||
)
|
||||
if not who_hates:
|
||||
return await ctx.respond(f"I couldn't find anyone who hates {thing}...")
|
||||
|
||||
|
||||
out_whohates: list = []
|
||||
if not isinstance(who_hates, list):
|
||||
return
|
||||
@ -154,7 +140,7 @@ class LoveHate(commands.Cog):
|
||||
out_whohates.append(str(hater))
|
||||
|
||||
optional_s: str = "s" if len(out_whohates) == 1 else ""
|
||||
|
||||
|
||||
out_whohates_str: str = self.join_with_and(out_whohates)
|
||||
|
||||
return await ctx.respond(f"{out_whohates_str} hate{optional_s} {thing}")
|
||||
@ -162,22 +148,14 @@ class LoveHate(commands.Cog):
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"Error: {str(e)}")
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def dontcare(self, ctx, thing: str) -> None:
|
||||
"""
|
||||
Make me forget your opinion on <thing>
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
thing (str)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
stop_caring: str = await self.db.update(ctx.author.display_name,
|
||||
thing, 0)
|
||||
return await ctx.respond(stop_caring)
|
||||
stop_caring: str = await self.db.update(ctx.author.display_name, thing, 0)
|
||||
return await ctx.respond(stop_caring)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
@ -186,53 +164,42 @@ class LoveHate(commands.Cog):
|
||||
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.
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
user (Optional[str])
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not user:
|
||||
display_name = ctx.author.display_name
|
||||
hates: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name,
|
||||
hates=True)
|
||||
hates: Union[list[tuple], bool] = await self.db.get_lovehates(
|
||||
user=display_name, hates=True
|
||||
)
|
||||
if not hates:
|
||||
return await ctx.respond("You don't seem to hate anything...")
|
||||
else:
|
||||
hates = await self.db.get_lovehates(user=user.strip(), hates=True)
|
||||
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 = []
|
||||
if not isinstance(hates, list):
|
||||
return
|
||||
for hated_thing in hates:
|
||||
(hated_thing,) = 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}")
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
@bridge.bridge_command(aliases=['sarcastichate'])
|
||||
@bridge.bridge_command(aliases=["sarcastichate"])
|
||||
async def love(self, ctx, *, thing: str) -> None:
|
||||
"""
|
||||
Love <thing>
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
thing (str)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if discord.utils.raw_mentions(thing):
|
||||
# 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)
|
||||
if not guild:
|
||||
return
|
||||
@ -241,28 +208,21 @@ class LoveHate(commands.Cog):
|
||||
return
|
||||
thing = thing_member.display_name
|
||||
|
||||
love: str = await self.db.update(ctx.author.display_name,
|
||||
thing, 1)
|
||||
return await ctx.respond(love)
|
||||
love: str = await self.db.update(ctx.author.display_name, thing, 1)
|
||||
return await ctx.respond(love)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
@bridge.bridge_command(aliases=['sarcasticlove'])
|
||||
@bridge.bridge_command(aliases=["sarcasticlove"])
|
||||
async def hate(self, ctx, *, thing: str) -> None:
|
||||
"""
|
||||
Hate <thing>
|
||||
|
||||
Args:
|
||||
ctx (Any)
|
||||
thing (str)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if discord.utils.raw_mentions(thing):
|
||||
# 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)
|
||||
if not guild:
|
||||
return
|
||||
@ -270,18 +230,17 @@ class LoveHate(commands.Cog):
|
||||
if not thing_member:
|
||||
return
|
||||
thing = thing_member.display_name
|
||||
hate: str = await self.db.update(ctx.author.display_name,
|
||||
thing, -1)
|
||||
return await ctx.respond(hate)
|
||||
hate: str = await self.db.update(ctx.author.display_name, thing, -1)
|
||||
return await ctx.respond(hate)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
# not needed currently
|
||||
pass
|
||||
|
||||
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(LoveHate(bot))
|
||||
bot.add_cog(LoveHate(bot))
|
||||
|
690
cogs/meme.py
690
cogs/meme.py
File diff suppressed because it is too large
Load Diff
1157
cogs/misc.py
1157
cogs/misc.py
File diff suppressed because it is too large
Load Diff
@ -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
|
261
cogs/owner.py
261
cogs/owner.py
@ -1,6 +1,7 @@
|
||||
import io
|
||||
import random
|
||||
import asyncio
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Optional
|
||||
import discord
|
||||
@ -9,138 +10,115 @@ from discord.ext import bridge, commands
|
||||
from disc_havoc import Havoc
|
||||
import util
|
||||
|
||||
|
||||
class Owner(commands.Cog):
|
||||
"""Owner Cog for Havoc"""
|
||||
|
||||
def __init__(self, bot: Havoc) -> None:
|
||||
self.bot: Havoc = bot
|
||||
self.former_roles_store: dict[int, list[discord.Role]] = {}
|
||||
self._temperature: int = random.randrange(20, 30)
|
||||
|
||||
@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
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
temperature (Optional[int|str]): New temperature
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
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):
|
||||
return await ctx.respond("I am afraid I can't let you do that.")
|
||||
try:
|
||||
_temperature: int = int(temp)
|
||||
except:
|
||||
return await ctx.respond("Invalid input")
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
return await ctx.respond("Invalid input")
|
||||
if _temperature < -15:
|
||||
return await ctx.respond("Too cold! (-15°C minimum)")
|
||||
elif _temperature > 35:
|
||||
return await ctx.respond("Too hot! (35°C maximum)")
|
||||
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()
|
||||
@commands.is_owner()
|
||||
async def editmsg(self, ctx,
|
||||
msgid: str,
|
||||
*,
|
||||
newcontent: str
|
||||
) -> None:
|
||||
async def editmsg(self, ctx, msgid: str, *, newcontent: str) -> None:
|
||||
"""
|
||||
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:
|
||||
message: Optional[discord.Message] = self.bot.get_message(int(msgid))
|
||||
if not message:
|
||||
await ctx.respond(f"**Failed:** Message {msgid} not found.",
|
||||
ephemeral=True)
|
||||
await ctx.respond(
|
||||
f"**Failed:** Message {msgid} not found.", ephemeral=True
|
||||
)
|
||||
return None
|
||||
await message.edit(content=newcontent)
|
||||
await ctx.respond("**Done!**", ephemeral=True)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"**Failed:** {str(e)}",
|
||||
ephemeral=True)
|
||||
|
||||
await ctx.respond(f"**Failed:** {str(e)}", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def reload(self, ctx) -> None:
|
||||
"""
|
||||
Reload Cogs
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
self.bot.load_exts(False)
|
||||
await ctx.respond("Reloaded!", ephemeral=True)
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def say(self, ctx, *,
|
||||
parameters: str) -> None:
|
||||
async def say(self, ctx, *, parameters: str) -> None:
|
||||
"""
|
||||
Make me say something in a channel
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
parameters (str): Channel <space> Message
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
_parameters: list[str] = parameters.split(" ")
|
||||
|
||||
|
||||
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_mentions: list[int] = discord.utils.raw_channel_mentions(channel)
|
||||
if channel_mentions:
|
||||
channel = str(channel_mentions[0])
|
||||
msg: str = " ".join(_parameters[1:])
|
||||
await util.discord_helpers.send_message(self.bot, channel=channel,
|
||||
message=msg)
|
||||
await util.discord_helpers.send_message(self.bot, channel=channel, message=msg)
|
||||
return await ctx.respond("**Done.**", ephemeral=True)
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def chgstatus(self, ctx, *,
|
||||
status: Optional[str] = None) -> None:
|
||||
async def chgstatus(self, ctx, *, status: Optional[str] = None) -> None:
|
||||
"""
|
||||
Change bots status
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
status (Optional[str]): The new status to set
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if not status:
|
||||
return await ctx.respond("ERR: No status provided to change to!",
|
||||
ephemeral=True)
|
||||
|
||||
await self.bot.change_presence(status=discord.Status.online,
|
||||
activity=discord.CustomActivity(name=status.strip()))
|
||||
return await ctx.respond(
|
||||
"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 ctx.respond("Done!", ephemeral=True)
|
||||
|
||||
|
||||
@commands.message_command(name="Remove Messages Starting Here")
|
||||
@commands.is_owner()
|
||||
async def purge(self, ctx, message: discord.Message) -> None:
|
||||
"""
|
||||
Purge Messages
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
message (discord.Message): Discord message
|
||||
@ -148,10 +126,12 @@ class Owner(commands.Cog):
|
||||
None
|
||||
"""
|
||||
try:
|
||||
await ctx.channel.purge(after=message,
|
||||
bulk=True,
|
||||
limit=900000,
|
||||
reason=f"Purge initiated by {ctx.author.display_name}")
|
||||
await ctx.channel.purge(
|
||||
after=message,
|
||||
bulk=True,
|
||||
limit=900000,
|
||||
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!**")
|
||||
# Wait 3 seconds, then delete interaction
|
||||
@ -161,13 +141,13 @@ class Owner(commands.Cog):
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"**ERR: {str(e)}**")
|
||||
|
||||
|
||||
@commands.message_command(name="Move to Memes")
|
||||
@commands.is_owner()
|
||||
async def movememe(self, ctx, message: discord.Message) -> None:
|
||||
"""
|
||||
Move to Memes
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
message (discord.Message): Discord message
|
||||
@ -177,34 +157,40 @@ class Owner(commands.Cog):
|
||||
try:
|
||||
if not isinstance(message.channel, discord.TextChannel):
|
||||
return
|
||||
memes_channel: discord.TextChannel = ctx.guild.get_channel(1147229098544988261)
|
||||
message_content: str = message.content
|
||||
memes_channel: discord.TextChannel = ctx.guild.get_channel(
|
||||
1147229098544988261
|
||||
)
|
||||
message_content: str = message.content
|
||||
message_author: str = message.author.display_name
|
||||
message_channel: str = message.channel.name
|
||||
_file: Optional[discord.File] = None
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True,
|
||||
timeout=20).raw.read())
|
||||
ext: str = item.url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f'img.{ext}')
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
if not _file:
|
||||
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)
|
||||
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 message.delete()
|
||||
await ctx.respond("OK!", ephemeral=True)
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return await ctx.respond("Failed! :(", ephemeral=True)
|
||||
|
||||
|
||||
@commands.message_command(name="Move to Drugs")
|
||||
@commands.is_owner()
|
||||
async def movedrugs(self, ctx, message: discord.Message) -> None:
|
||||
"""
|
||||
Move to Drugs
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
message (discord.Message): Discord message
|
||||
@ -214,7 +200,9 @@ class Owner(commands.Cog):
|
||||
try:
|
||||
if not isinstance(message.channel, discord.TextChannel):
|
||||
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_author: str = message.author.display_name
|
||||
message_channel: str = message.channel.name
|
||||
@ -222,27 +210,31 @@ class Owner(commands.Cog):
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True,
|
||||
timeout=20).raw.read())
|
||||
ext: str = item.url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f'img.{ext}')
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
if not _file:
|
||||
return # No file to move
|
||||
await drugs_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\
|
||||
*\n**{message_author}:** {message_content}", file=_file)
|
||||
return # No file to move
|
||||
await drugs_channel.send(
|
||||
f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\
|
||||
*\n**{message_author}:** {message_content}",
|
||||
file=_file,
|
||||
)
|
||||
await message.delete()
|
||||
await ctx.respond("OK!", ephemeral=True)
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return await ctx.respond("Failed! :(", ephemeral=True)
|
||||
|
||||
|
||||
@commands.message_command(name="Move to fun-house")
|
||||
@commands.is_owner()
|
||||
async def movefunhouse(self, ctx, message: discord.Message) -> None:
|
||||
"""
|
||||
Move to fun-house
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
message (discord.Message): Discord message
|
||||
@ -252,7 +244,9 @@ class Owner(commands.Cog):
|
||||
try:
|
||||
if not isinstance(message.channel, discord.TextChannel):
|
||||
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_author: str = message.author.display_name
|
||||
message_channel: str = message.channel.name
|
||||
@ -260,25 +254,28 @@ class Owner(commands.Cog):
|
||||
if message.attachments:
|
||||
for item in message.attachments:
|
||||
if item.url and len(item.url) >= 20:
|
||||
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True,
|
||||
timeout=20).raw.read())
|
||||
ext: str = item.url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f'img.{ext}')
|
||||
await funhouse_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\
|
||||
...*\n**{message_author}:** {message_content}")
|
||||
image: io.BytesIO = io.BytesIO(
|
||||
requests.get(item.url, stream=True, timeout=20).raw.read()
|
||||
)
|
||||
ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
|
||||
_file = discord.File(image, filename=f"img.{ext}")
|
||||
await funhouse_channel.send(
|
||||
f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\
|
||||
...*\n**{message_author}:** {message_content}"
|
||||
)
|
||||
await message.delete()
|
||||
await ctx.respond("OK!", ephemeral=True)
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return await ctx.respond("Failed! :(", ephemeral=True)
|
||||
|
||||
|
||||
@commands.user_command(name="Einsperren!", guild_ids=[145182936002482196])
|
||||
@commands.is_owner()
|
||||
async def einsperren(self, ctx, member: discord.Member) -> None:
|
||||
"""
|
||||
Einsperren!
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
member (discord.Member): Discord member
|
||||
@ -287,45 +284,63 @@ class Owner(commands.Cog):
|
||||
"""
|
||||
try:
|
||||
if not ctx.guild.id == 1145182936002482196:
|
||||
return # Not home server!
|
||||
return # Not home server!
|
||||
if not member.roles:
|
||||
return # No roles
|
||||
return # No roles
|
||||
audit_reason: str = f"Einsperren von {ctx.user.display_name}"
|
||||
member = ctx.guild.get_member(member.id)
|
||||
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)
|
||||
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)
|
||||
if not "einsperren" in member_role_names:
|
||||
if "einsperren" not in member_role_names:
|
||||
try:
|
||||
if member.id in self.former_roles_store:
|
||||
self.former_roles_store.pop(member.id)
|
||||
self.former_roles_store[member.id] = member.roles
|
||||
except:
|
||||
pass # Safe to ignore
|
||||
except: # noqa
|
||||
"""Safe to ignore"""
|
||||
pass
|
||||
try:
|
||||
await member.edit(roles=[einsperren_role], reason=audit_reason)
|
||||
await ctx.respond(f"Gesendet {member_display} an einsperren.", ephemeral=True)
|
||||
await opers_chan.send(f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren.")
|
||||
except:
|
||||
await ctx.respond(
|
||||
f"Gesendet {member_display} an einsperren.", ephemeral=True
|
||||
)
|
||||
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()
|
||||
return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True)
|
||||
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
|
||||
else:
|
||||
former_roles: list = self.former_roles_store.get(member.id, [0])
|
||||
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 opers_chan.send(f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit.")
|
||||
|
||||
await ctx.respond(
|
||||
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:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"ERR: {str(e)}", ephemeral=True)
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Owner(bot))
|
||||
bot.add_cog(Owner(bot))
|
||||
|
329
cogs/quote.py
Normal file
329
cogs/quote.py
Normal 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))
|
109
cogs/radio.py
109
cogs/radio.py
@ -6,59 +6,61 @@ from util.radio_util import get_now_playing, skip
|
||||
import discord
|
||||
from disc_havoc import Havoc
|
||||
|
||||
|
||||
class Radio(commands.Cog):
|
||||
"""Radio Cog for Havoc"""
|
||||
|
||||
def __init__(self, bot: Havoc) -> None:
|
||||
self.bot: Havoc = bot
|
||||
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.LAST_NP_TRACK: Optional[str] = None
|
||||
try:
|
||||
self.radio_state_loop.cancel()
|
||||
except Exception as e:
|
||||
logging.debug("Failed to cancel radio_state_loop: %s",
|
||||
str(e))
|
||||
|
||||
logging.debug("Failed to cancel radio_state_loop: %s", str(e))
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Run on Bot Ready"""
|
||||
await self.radio_init()
|
||||
|
||||
def is_radio_chan(): # type: ignore
|
||||
|
||||
def is_radio_chan(): # type: ignore
|
||||
"""Check if channel is radio chan"""
|
||||
|
||||
def predicate(ctx):
|
||||
try:
|
||||
return ctx.channel.id == 1221615558492029050
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
@commands.is_owner()
|
||||
async def reinitradio(self, ctx) -> None:
|
||||
"""
|
||||
Reinitialize serious.FM
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
loop: discord.asyncio.AbstractEventLoop = self.bot.loop
|
||||
loop.create_task(self.radio_init())
|
||||
await ctx.respond("Done!", ephemeral=True)
|
||||
|
||||
|
||||
async def radio_init(self) -> None:
|
||||
"""Init Radio"""
|
||||
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)
|
||||
if not guild:
|
||||
return
|
||||
channel = guild.get_channel(radio_chan)
|
||||
channel = guild.get_channel(radio_chan)
|
||||
if not isinstance(channel, discord.VoiceChannel):
|
||||
return
|
||||
if not self.bot.voice_clients:
|
||||
@ -67,26 +69,29 @@ class Radio(commands.Cog):
|
||||
try:
|
||||
self.radio_state_loop.cancel()
|
||||
except Exception as e:
|
||||
logging.debug("Failed to cancel radio_state_loop: %s",
|
||||
str(e))
|
||||
logging.debug("Failed to cancel radio_state_loop: %s", str(e))
|
||||
self.radio_state_loop.start()
|
||||
logging.info("radio_state_loop task started!")
|
||||
except:
|
||||
logging.critical("Could not start task...")
|
||||
except Exception as e:
|
||||
logging.critical("Could not start task... Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
|
||||
@tasks.loop(seconds=5.0)
|
||||
async def radio_state_loop(self) -> None:
|
||||
"""Radio State Loop"""
|
||||
try:
|
||||
(radio_guild, radio_chan) = self.channels['sfm']
|
||||
(radio_guild, radio_chan) = self.channels["sfm"]
|
||||
try:
|
||||
vc: discord.VoiceProtocol = self.bot.voice_clients[-1]
|
||||
except:
|
||||
logging.debug("No voice client, establishing new VC connection...")
|
||||
except Exception as e:
|
||||
logging.debug(
|
||||
"No voice client, establishing new VC connection... (Exception: %s)",
|
||||
str(e),
|
||||
)
|
||||
guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild)
|
||||
if not guild:
|
||||
return
|
||||
@ -95,46 +100,46 @@ class Radio(commands.Cog):
|
||||
return
|
||||
await channel.connect()
|
||||
vc = self.bot.voice_clients[-1]
|
||||
|
||||
|
||||
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,
|
||||
but they exist.
|
||||
"""
|
||||
logging.info("Detected VC not playing... playing!")
|
||||
source: discord.FFmpegAudio = discord.FFmpegOpusAudio(self.STREAM_URL,
|
||||
before_options="-timeout 3000000")
|
||||
vc.play(source, # type: ignore
|
||||
after=lambda e: logging.info("Error: %s", e) if e\
|
||||
else None)
|
||||
# Get Now Playing (disabled)
|
||||
# np_track: Optional[str] = await get_now_playing()
|
||||
# if np_track and not self.LAST_NP_TRACK == np_track:
|
||||
# self.LAST_NP_TRACK = np_track
|
||||
# if isinstance(vc.channel, discord.VoiceChannel):
|
||||
# await vc.channel.set_status(f"Now playing: {np_track}")
|
||||
except:
|
||||
source: discord.FFmpegAudio = discord.FFmpegOpusAudio(self.STREAM_URL)
|
||||
vc.play( # type: ignore
|
||||
source,
|
||||
after=lambda e: logging.info("Error: %s", e) if e else None,
|
||||
)
|
||||
# Get Now Playing
|
||||
np_track: Optional[str] = await get_now_playing()
|
||||
if np_track and not self.LAST_NP_TRACK == np_track:
|
||||
self.LAST_NP_TRACK = np_track
|
||||
await self.bot.change_presence(
|
||||
activity=discord.Activity(
|
||||
type=discord.ActivityType.listening, name=np_track
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
async def skip(self, ctx) -> None:
|
||||
"""
|
||||
Skip - Convenience Command
|
||||
Args:
|
||||
ctx (Any)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
|
||||
|
||||
await skip()
|
||||
return await ctx.respond("OK", ephemeral=True)
|
||||
|
||||
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
"""Run on Cog Unload"""
|
||||
"""Run on Cog Unload"""
|
||||
self.radio_state_loop.cancel()
|
||||
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Radio(bot))
|
||||
bot.add_cog(Radio(bot))
|
||||
|
256
cogs/sing.py
256
cogs/sing.py
@ -2,7 +2,6 @@ import traceback
|
||||
import logging
|
||||
from typing import Optional, Union
|
||||
from regex import Pattern
|
||||
import urllib
|
||||
import discord
|
||||
import regex
|
||||
from util.sing_util import Utility
|
||||
@ -11,44 +10,42 @@ from disc_havoc import Havoc
|
||||
|
||||
BOT_CHANIDS = []
|
||||
|
||||
|
||||
class Sing(commands.Cog):
|
||||
"""Sing Cog for Havoc"""
|
||||
|
||||
def __init__(self, bot: Havoc) -> None:
|
||||
self.bot: Havoc = bot
|
||||
self.utility = Utility()
|
||||
global BOT_CHANIDS
|
||||
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})?)?",
|
||||
regex.UNICODE)
|
||||
|
||||
def is_spamchan(): # type: ignore
|
||||
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})?)?",
|
||||
regex.UNICODE,
|
||||
)
|
||||
|
||||
def is_spamchan(): # type: ignore
|
||||
"""Check if channel is spam chan"""
|
||||
|
||||
def predicate(ctx):
|
||||
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)
|
||||
return ctx.channel.id in BOT_CHANIDS
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return commands.check(predicate)
|
||||
|
||||
@bridge.bridge_command(aliases=['sing'])
|
||||
async def s(self, ctx, *,
|
||||
song: Optional[str] = None) -> None:
|
||||
|
||||
return commands.check(predicate)
|
||||
|
||||
@bridge.bridge_command(aliases=["sing"])
|
||||
async def s(self, ctx, *, song: Optional[str] = None) -> None:
|
||||
"""
|
||||
Search for lyrics, format is artist : song. Also reads activity.
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
song (Optional[str]): Song to search
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
with ctx.channel.typing():
|
||||
interaction: bool = isinstance(ctx,
|
||||
discord.ext.bridge.BridgeApplicationContext)
|
||||
activity: Optional[discord.Activity] = None
|
||||
if not song:
|
||||
if not ctx.author.activities:
|
||||
@ -57,73 +54,81 @@ class Sing(commands.Cog):
|
||||
for _activity in ctx.author.activities:
|
||||
if _activity.type == discord.ActivityType.listening:
|
||||
activity = _activity
|
||||
|
||||
if not activity:
|
||||
return await ctx.respond("**Error**: No song specified, no activity found to read.")
|
||||
|
||||
if interaction:
|
||||
await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord
|
||||
|
||||
if not activity:
|
||||
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)
|
||||
|
||||
if isinstance(parsed, tuple):
|
||||
(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
|
||||
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song,
|
||||
search_subsearch)
|
||||
|
||||
search_result: Optional[list] = await self.utility.lyric_search(
|
||||
search_artist, search_song, search_subsearch
|
||||
)
|
||||
|
||||
if not search_result:
|
||||
await ctx.respond("ERR: No search result.")
|
||||
return
|
||||
|
||||
|
||||
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):
|
||||
return # Invalid data type
|
||||
return # Invalid data type
|
||||
(
|
||||
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[str] = search_result[1] # Second index is the wrapped lyrics
|
||||
search_result_wrapped_short: list[str] = search_result[2] # Third is short wrapped lyrics
|
||||
if not ctx.channel.id in BOT_CHANIDS:
|
||||
search_result_wrapped = search_result_wrapped_short # Replace with shortened lyrics for non spamchans
|
||||
embeds: list[Optional[discord.Embed]] = []
|
||||
embed_url: str = f"[on codey.lol](https://codey.lol/#{urllib.parse.quote(search_artist)}/{urllib.parse.quote(search_song)})"
|
||||
c: int = 0
|
||||
footer: str = "To be continued..." #Placeholder
|
||||
for section in search_result_wrapped:
|
||||
c+=1
|
||||
if c == len(search_result_wrapped):
|
||||
footer = f"Found on: {search_result_src}"
|
||||
section = self.control_strip_regex.sub('', section)
|
||||
# if ctx.guild.id == 1145182936002482196:
|
||||
# section = section.upper()
|
||||
embed: discord.Embed = discord.Embed(
|
||||
title=f"{search_result_song} by {search_result_artist}",
|
||||
description=discord.utils.escape_markdown(section)
|
||||
)
|
||||
embed.add_field(name="Confidence", value=search_result_confidence,
|
||||
inline=True)
|
||||
embed.add_field(name="Time Taken", value=search_result_time_taken,
|
||||
inline=True)
|
||||
embed.add_field(name="Link", value=embed_url)
|
||||
embed.set_footer(text=footer)
|
||||
embeds.append(embed)
|
||||
await ctx.respond(embed=embeds[0])
|
||||
for _embed in embeds[1:]:
|
||||
if isinstance(_embed, discord.Embed):
|
||||
await ctx.send(embed=_embed)
|
||||
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[str] = search_result[
|
||||
1
|
||||
] # Second index is the wrapped lyrics
|
||||
search_result_wrapped_short: list[str] = search_result[
|
||||
2
|
||||
] # Third is short wrapped lyrics
|
||||
if ctx.channel.id not in BOT_CHANIDS:
|
||||
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 = "" # Placeholder
|
||||
for c, section in enumerate(search_result_wrapped):
|
||||
if c == len(search_result_wrapped):
|
||||
footer = f"`Found on: {search_result_src}`"
|
||||
section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
|
||||
msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
|
||||
if c > 1:
|
||||
msg = "\n".join(msg.split("\n")[1:])
|
||||
out_messages.append(msg.strip())
|
||||
for msg in out_messages:
|
||||
await ctx.send(msg)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
await ctx.respond(f"ERR: {str(e)}")
|
||||
|
||||
|
||||
@commands.user_command(name="Sing")
|
||||
async def sing_context_menu(self, ctx, member: discord.Member) -> None:
|
||||
"""
|
||||
Sing Context Menu Command
|
||||
|
||||
|
||||
Args:
|
||||
ctx (Any): Discord context
|
||||
member (discord.Member): Discord member
|
||||
@ -133,70 +138,93 @@ class Sing(commands.Cog):
|
||||
try:
|
||||
PODY_ID: int = 1172340700663255091
|
||||
IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS
|
||||
member_display = ctx.interaction.guild.get_member(member.id)\
|
||||
.display_name
|
||||
if not(ctx.interaction.guild.get_member(member.id).activities)\
|
||||
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!
|
||||
member_display = ctx.interaction.guild.get_member(member.id).display_name
|
||||
if (
|
||||
not (ctx.interaction.guild.get_member(member.id).activities)
|
||||
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!
|
||||
activity: Optional[discord.Activity] = None
|
||||
if IS_SPAMCHAN:
|
||||
await ctx.respond(f"***Reading activity of {member_display}...***")
|
||||
if IS_SPAMCHAN:
|
||||
await ctx.respond(f"***Reading activity of {member_display}...***")
|
||||
for _activity in ctx.interaction.guild.get_member(member_id).activities:
|
||||
if _activity.type == discord.ActivityType.listening:
|
||||
activity = _activity
|
||||
parsed: Union[tuple, bool] = self.utility.parse_song_input(song=None,
|
||||
activity=activity)
|
||||
parsed: Union[tuple, bool] = self.utility.parse_song_input(
|
||||
song=None, activity=activity
|
||||
)
|
||||
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):
|
||||
(search_artist, search_song, search_subsearch) = parsed
|
||||
await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord
|
||||
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song,
|
||||
search_subsearch)
|
||||
await ctx.respond(
|
||||
"*Searching...*"
|
||||
) # 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:
|
||||
await ctx.respond("ERR: No search result")
|
||||
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
|
||||
footer: str = "To be continued..." #Placeholder
|
||||
for section in search_result_wrapped:
|
||||
c+=1
|
||||
c += 1
|
||||
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:
|
||||
# section = section.upper()
|
||||
embed: discord.Embed = discord.Embed(
|
||||
title=f"{search_result_song} by {search_result_artist}",
|
||||
description=discord.utils.escape_markdown(section)
|
||||
)
|
||||
embed.add_field(name="Confidence", value=search_result_confidence, inline=True)
|
||||
embed.add_field(name="Time Taken", value=search_result_time_taken, inline=True)
|
||||
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)})")
|
||||
embed.set_footer(text=footer)
|
||||
embeds.append(embed)
|
||||
|
||||
for _embed in embeds:
|
||||
await ctx.send(embed=_embed)
|
||||
section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
|
||||
msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
|
||||
if c > 1:
|
||||
msg = "\n".join(msg.split("\n")[1:])
|
||||
out_messages.append(msg.strip())
|
||||
for msg in out_messages:
|
||||
await ctx.send(msg)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return await ctx.respond(f"ERR: {str(e)}")
|
||||
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Sing(bot))
|
||||
|
@ -1,22 +1,25 @@
|
||||
"""
|
||||
AI
|
||||
"""
|
||||
|
||||
|
||||
class AIException(Exception):
|
||||
"""AI Exception (generic)"""
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
LoveHate
|
||||
"""
|
||||
|
||||
|
||||
class LoveHateException(Exception):
|
||||
"""Love Hate Exception (generic)"""
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
Misc
|
||||
"""
|
||||
|
||||
|
||||
class MiscException(Exception):
|
||||
"""Misc Exception (generic)"""
|
||||
pass
|
@ -13,20 +13,19 @@ from termcolor import colored
|
||||
from constants import OWNERS, BOT_CHANIDS
|
||||
import api
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
encoding='utf-8')
|
||||
setproctitle.setproctitle('disc-havoc')
|
||||
logging.basicConfig(
|
||||
level=logging.INFO, format="%(asctime)s %(message)s", encoding="utf-8"
|
||||
)
|
||||
setproctitle.setproctitle("disc-havoc")
|
||||
|
||||
"""Auto Load Cogs"""
|
||||
cogs_list: list[str] = [
|
||||
'misc',
|
||||
'owner',
|
||||
'sing',
|
||||
'meme',
|
||||
'karma',
|
||||
'lovehate',
|
||||
'radio',
|
||||
"misc",
|
||||
"owner",
|
||||
"sing",
|
||||
"meme",
|
||||
"lovehate",
|
||||
"radio",
|
||||
]
|
||||
|
||||
bot_activity = discord.CustomActivity(name="I made cookies!")
|
||||
@ -36,60 +35,68 @@ load_dotenv()
|
||||
intents = discord.Intents.all()
|
||||
intents.message_content = True
|
||||
|
||||
|
||||
class Havoc(bridge.Bot):
|
||||
def __init__(self) -> None:
|
||||
super().__init__(command_prefix=".", intents=intents,
|
||||
owner_ids=OWNERS, activity=bot_activity,
|
||||
help_command=commands.MinimalHelpCommand())
|
||||
super().__init__(
|
||||
command_prefix=".",
|
||||
intents=intents,
|
||||
owner_ids=OWNERS,
|
||||
activity=bot_activity,
|
||||
help_command=commands.MinimalHelpCommand(),
|
||||
)
|
||||
self.BOT_CHANIDS = BOT_CHANIDS
|
||||
self.load_exts()
|
||||
|
||||
|
||||
def load_exts(self, initialRun: Optional[bool] = True) -> None:
|
||||
"""
|
||||
Load Cogs/Extensions
|
||||
|
||||
Args:
|
||||
|
||||
Args:
|
||||
initialRun (Optional[bool]) default: True
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
load_method = self.load_extension if initialRun\
|
||||
else self.reload_extension
|
||||
load_method = self.load_extension if initialRun else self.reload_extension
|
||||
|
||||
for cog in cogs_list:
|
||||
logging.info("Loading: %s", cog)
|
||||
load_method(f'cogs.{cog}')
|
||||
load_method(f"cogs.{cog}")
|
||||
|
||||
importlib.reload(api)
|
||||
from api import API
|
||||
from api import API # noqa (voodoo)
|
||||
|
||||
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)
|
||||
try:
|
||||
try:
|
||||
self.fapi_task.cancel()
|
||||
except Exception as e:
|
||||
logging.debug("Failed to cancel fapi_task: %s",
|
||||
str(e))
|
||||
|
||||
logging.debug("Failed to cancel fapi_task: %s", str(e))
|
||||
|
||||
logging.info("Starting FAPI Task")
|
||||
|
||||
self.fapi_task: Task = self.loop.create_task(hypercorn.asyncio.serve(api_instance.api_app,
|
||||
api_config))
|
||||
|
||||
self.fapi_task: Task = self.loop.create_task(
|
||||
hypercorn.asyncio.serve(api_instance.api_app, api_config)
|
||||
)
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_ready(self) -> None:
|
||||
"""Run on Bot Ready"""
|
||||
logging.info("%s online!", self.user)
|
||||
logging.info("%s online!", self.user)
|
||||
|
||||
|
||||
|
||||
def __init__() -> None:
|
||||
logging.info(colored(f"Log level: {logging.getLevelName(logging.root.level)}",
|
||||
"red", attrs=['reverse']))
|
||||
logging.info(
|
||||
colored(
|
||||
f"Log level: {logging.getLevelName(logging.root.level)}",
|
||||
"red",
|
||||
attrs=["reverse"],
|
||||
)
|
||||
)
|
||||
bot = Havoc()
|
||||
bot.run(os.getenv('TOKEN'))
|
||||
|
||||
bot.run(os.getenv("TOKEN"))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
__init__()
|
||||
__init__()
|
||||
|
@ -4,7 +4,7 @@ edge_tts==6.1.12
|
||||
feedparser==6.0.11
|
||||
Flask==3.0.3
|
||||
nvdlib==0.7.7
|
||||
openai==1.54.3
|
||||
openai==1.54.3d
|
||||
requests_async==0.6.2
|
||||
shazamio==0.7.0
|
||||
streamrip==2.0.5
|
||||
|
@ -12,14 +12,20 @@ from util.catbox import CatboxAsync
|
||||
logger = logging.getLogger()
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
async def test() -> None:
|
||||
f = os.path.join(os.path.expanduser("~"), "qu.png")
|
||||
box1: LitterboxAsync = LitterboxAsync()
|
||||
box2: CatboxAsync = CatboxAsync()
|
||||
url1: Optional[str] = await box1.upload(f)
|
||||
url2: Optional[str] = await box2.upload(f)
|
||||
logging.info("""Uploaded URLs:
|
||||
logging.info(
|
||||
"""Uploaded URLs:
|
||||
Litter - %s\n
|
||||
Cat - %s""", url1, url2)
|
||||
|
||||
asyncio.run(test())
|
||||
Cat - %s""",
|
||||
url1,
|
||||
url2,
|
||||
)
|
||||
|
||||
|
||||
asyncio.run(test())
|
||||
|
@ -1,4 +1,4 @@
|
||||
import importlib
|
||||
from . import discord_helpers
|
||||
|
||||
importlib.reload(discord_helpers)
|
||||
importlib.reload(discord_helpers)
|
||||
|
@ -12,35 +12,36 @@ Catbox Uploader (Async)
|
||||
|
||||
catbox_api_url: str = "https://catbox.moe/user/api.php"
|
||||
http_headers: dict[str, str] = {
|
||||
'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',
|
||||
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8,es;q=0.7',
|
||||
'referer': 'https://www.google.com/',
|
||||
'cookie': 'DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0'
|
||||
}
|
||||
"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",
|
||||
"Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7",
|
||||
"referer": "https://www.google.com/",
|
||||
"cookie": "DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0",
|
||||
}
|
||||
|
||||
|
||||
class CatboxAsync:
|
||||
def __init__(self) -> None:
|
||||
self.catbox_api_url = catbox_api_url
|
||||
self.headers = http_headers
|
||||
|
||||
|
||||
def generateRandomFileName(self, fileExt: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generate random file name
|
||||
|
||||
|
||||
Args:
|
||||
fileExt (Optional[str]): File extension to use for naming
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
if not fileExt:
|
||||
fileExt = 'png'
|
||||
fileExt = "png"
|
||||
return f"{random.getrandbits(32)}.{fileExt}"
|
||||
|
||||
|
||||
async def upload(self, file: str) -> Optional[str]:
|
||||
"""
|
||||
Upload file to catbox
|
||||
|
||||
|
||||
Args:
|
||||
file (str): Path of file to be uploaded
|
||||
Returns:
|
||||
@ -49,40 +50,41 @@ class CatboxAsync:
|
||||
try:
|
||||
if not file:
|
||||
return None
|
||||
if not(os.path.exists(file)):
|
||||
logging.critical("Could not find %s",
|
||||
file)
|
||||
if not (os.path.exists(file)):
|
||||
logging.critical("Could not find %s", file)
|
||||
return None
|
||||
|
||||
|
||||
fileExt: Optional[str] = None
|
||||
if file.find(".") > 0:
|
||||
fileExt = "".join(file.split(".")[-1:])
|
||||
|
||||
with open(file, 'rb') as fileContents:
|
||||
|
||||
with open(file, "rb") as fileContents:
|
||||
post_data: FormData = FormData()
|
||||
post_data.add_field(name="reqtype",
|
||||
value="fileupload")
|
||||
post_data.add_field(name="userhash",
|
||||
value="")
|
||||
post_data.add_field(name="reqtype", value="fileupload")
|
||||
post_data.add_field(name="userhash", value="")
|
||||
with magic.Magic(flags=magic.MAGIC_MIME) as m:
|
||||
content_type = m.id_filename(file)
|
||||
post_data.add_field(name="fileToUpload",
|
||||
value=fileContents,
|
||||
filename=self.generateRandomFileName(fileExt),
|
||||
content_type=content_type
|
||||
)
|
||||
post_data.add_field(
|
||||
name="fileToUpload",
|
||||
value=fileContents,
|
||||
filename=self.generateRandomFileName(fileExt),
|
||||
content_type=content_type,
|
||||
)
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(self.catbox_api_url,
|
||||
headers=self.headers,
|
||||
data=post_data,
|
||||
timeout=ClientTimeout(connect=10, sock_read=10)) as request:
|
||||
async with await session.post(
|
||||
self.catbox_api_url,
|
||||
headers=self.headers,
|
||||
data=post_data,
|
||||
timeout=ClientTimeout(connect=10, sock_read=10),
|
||||
) as request:
|
||||
request.raise_for_status()
|
||||
return await request.text()
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return None
|
||||
finally:
|
||||
try:
|
||||
fileContents.close()
|
||||
except:
|
||||
return None
|
||||
except: # noqa
|
||||
return None
|
||||
|
@ -6,11 +6,13 @@ from typing import Optional, Any
|
||||
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
|
||||
|
||||
|
||||
Args:
|
||||
bot (discord.Bot)
|
||||
channel (str)
|
||||
@ -18,10 +20,9 @@ async def get_channel_by_name(bot: discord.Bot, channel: str,
|
||||
Returns:
|
||||
Optional[Any]
|
||||
"""
|
||||
channel = re.sub(r'^#', '', channel.strip())
|
||||
channel = re.sub(r"^#", "", channel.strip())
|
||||
if not guild:
|
||||
return discord.utils.get(bot.get_all_channels(),
|
||||
name=channel)
|
||||
return discord.utils.get(bot.get_all_channels(), name=channel)
|
||||
else:
|
||||
_guild: Optional[discord.Guild] = bot.get_guild(guild)
|
||||
if not _guild:
|
||||
@ -32,12 +33,14 @@ async def get_channel_by_name(bot: discord.Bot, channel: str,
|
||||
return _channel
|
||||
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
|
||||
channel is selected. Useful in the event a channel exists in more than one guild that the bot resides in.
|
||||
|
||||
|
||||
Args:
|
||||
bot (discord.Bot)
|
||||
channel (str)
|
||||
@ -50,9 +53,20 @@ async def send_message(bot: discord.Bot, channel: str,
|
||||
channel_int: int = int(channel)
|
||||
_channel = bot.get_channel(channel_int)
|
||||
else:
|
||||
channel = re.sub(r'^#', '', channel.strip())
|
||||
_channel = await get_channel_by_name(bot=bot,
|
||||
channel=channel, guild=guild)
|
||||
channel = re.sub(r"^#", "", channel.strip())
|
||||
_channel = await get_channel_by_name(bot=bot, channel=channel, guild=guild)
|
||||
if not isinstance(_channel, discord.TextChannel):
|
||||
return None
|
||||
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)
|
||||
|
@ -1,9 +1,9 @@
|
||||
import aiohttp
|
||||
import aiohttp
|
||||
from typing import Optional
|
||||
import regex
|
||||
import regex
|
||||
from regex import Pattern
|
||||
import os
|
||||
import random
|
||||
import os
|
||||
import random
|
||||
import logging
|
||||
import traceback
|
||||
from util.catbox import CatboxAsync
|
||||
@ -13,66 +13,67 @@ Jesus Meme Generator
|
||||
(requires Catbox uploader)
|
||||
"""
|
||||
|
||||
class JesusMemeGenerator():
|
||||
|
||||
class JesusMemeGenerator:
|
||||
def __init__(self) -> None:
|
||||
self.MEMEAPIURL = "https://apimeme.com/meme?meme="
|
||||
self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"),
|
||||
"memes")
|
||||
self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"), "memes")
|
||||
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(
|
||||
r'[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]'
|
||||
)
|
||||
self.url_regex_1: Pattern = regex.compile(
|
||||
r'\p{Horiz_Space}')
|
||||
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") -> Optional[str]:
|
||||
r"[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]"
|
||||
)
|
||||
self.url_regex_1: Pattern = regex.compile(r"\p{Horiz_Space}")
|
||||
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"
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Create Meme
|
||||
|
||||
|
||||
Args:
|
||||
top_line (str): Top line of meme
|
||||
bottom_line (str): Bottom line of meme
|
||||
meme (str): The meme to use, defaults to Jesus-Talking-To-Cool-Dude
|
||||
|
||||
|
||||
Returns:
|
||||
Optional[str]
|
||||
|
||||
|
||||
"""
|
||||
try:
|
||||
if not top_line or not bottom_line:
|
||||
return None
|
||||
top_line = self.top_line_regex.sub('',
|
||||
top_line.strip())
|
||||
bottom_line = self.bottom_line_regex.sub('',
|
||||
bottom_line.strip())
|
||||
top_line = self.top_line_regex.sub("", top_line.strip())
|
||||
bottom_line = self.bottom_line_regex.sub("", bottom_line.strip())
|
||||
out_fname: Optional[str] = None
|
||||
|
||||
|
||||
if len(top_line) < 1 or len(bottom_line) < 1:
|
||||
return None
|
||||
|
||||
|
||||
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)
|
||||
async with aiohttp.ClientSession(timeout=timeout) as session:
|
||||
async with session.get(formed_url) as response:
|
||||
UUID = f"{random.getrandbits(8)}-{random.getrandbits(8)}"
|
||||
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())
|
||||
|
||||
|
||||
if not out_fname:
|
||||
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:
|
||||
logging.info("Meme upload failed!")
|
||||
return None
|
||||
return meme_link
|
||||
except:
|
||||
print(traceback.format_exc())
|
||||
pass
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
@ -13,37 +13,38 @@ import os
|
||||
|
||||
litterbox_api_url: str = "https://litterbox.catbox.moe/resources/internals/api.php"
|
||||
http_headers: dict[str, str] = {
|
||||
'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',
|
||||
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8,es;q=0.7',
|
||||
'referer': 'https://www.google.com/',
|
||||
'cookie': 'DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0'
|
||||
}
|
||||
"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",
|
||||
"Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7",
|
||||
"referer": "https://www.google.com/",
|
||||
"cookie": "DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0",
|
||||
}
|
||||
|
||||
|
||||
class LitterboxAsync:
|
||||
def __init__(self) -> None:
|
||||
self.headers: dict[str, str] = http_headers
|
||||
self.api_path = litterbox_api_url
|
||||
|
||||
|
||||
def generateRandomFileName(self, fileExt: Optional[str] = None) -> str:
|
||||
"""
|
||||
Generate Random Filename
|
||||
|
||||
|
||||
Args:
|
||||
fileExt (Optional[str]): File extension to use for naming
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
if not fileExt:
|
||||
fileExt = 'png'
|
||||
fileExt = "png"
|
||||
return f"{random.getrandbits(32)}.{fileExt}"
|
||||
|
||||
async def upload(self,
|
||||
file: Union[str, bytes, BufferedReader],
|
||||
time='1h') -> Optional[str]:
|
||||
async def upload(
|
||||
self, file: Union[str, bytes, BufferedReader], time="1h"
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Upload File to Litterbox
|
||||
|
||||
|
||||
Args:
|
||||
file (Union[str, bytes, BufferedReader]): File to upload (accepts either filepath or io.BufferedReader)
|
||||
time (str): Expiration time, default: 1h
|
||||
@ -59,46 +60,47 @@ class LitterboxAsync:
|
||||
return None
|
||||
if isinstance(file, BufferedReader):
|
||||
file = file.read()
|
||||
|
||||
fileExt: str = 'png'
|
||||
|
||||
fileExt: str = "png"
|
||||
if isinstance(file, str):
|
||||
if file.find(".") > 0:
|
||||
fileExt = "".join(file.split(".")[-1:])
|
||||
|
||||
file = open(file, 'rb').read()
|
||||
|
||||
file = open(file, "rb").read()
|
||||
|
||||
with magic.Magic(flags=magic.MAGIC_MIME) as m:
|
||||
if isinstance(file, BufferedReader):
|
||||
content_type = str(m.id_buffer(file))
|
||||
else:
|
||||
content_type = str(m.id_filename(file))
|
||||
|
||||
content_type = str(m.id_filename(file))
|
||||
|
||||
post_data: FormData = FormData()
|
||||
post_data.add_field(name="reqtype",
|
||||
value="fileupload")
|
||||
post_data.add_field(name="userhash",
|
||||
value="")
|
||||
post_data.add_field(name="time",
|
||||
value=time)
|
||||
|
||||
post_data.add_field(name="fileToUpload",
|
||||
value=file,
|
||||
filename=self.generateRandomFileName(fileExt),
|
||||
content_type=content_type
|
||||
post_data.add_field(name="reqtype", value="fileupload")
|
||||
post_data.add_field(name="userhash", value="")
|
||||
post_data.add_field(name="time", value=time)
|
||||
|
||||
post_data.add_field(
|
||||
name="fileToUpload",
|
||||
value=file,
|
||||
filename=self.generateRandomFileName(fileExt),
|
||||
content_type=content_type,
|
||||
)
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(self.api_path,
|
||||
headers=self.headers,
|
||||
data=post_data,
|
||||
timeout=ClientTimeout(connect=5, sock_read=70)) as request:
|
||||
async with await session.post(
|
||||
self.api_path,
|
||||
headers=self.headers,
|
||||
data=post_data,
|
||||
timeout=ClientTimeout(connect=5, sock_read=70),
|
||||
) as request:
|
||||
request.raise_for_status()
|
||||
return await request.text()
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return None
|
||||
finally:
|
||||
if isinstance(file, BufferedReader):
|
||||
try:
|
||||
file.close()
|
||||
except:
|
||||
except: # noqa
|
||||
return None
|
||||
|
@ -4,24 +4,28 @@ from typing import LiteralString, Optional, Union
|
||||
import aiosqlite as sqlite3
|
||||
from constructors import LoveHateException
|
||||
|
||||
|
||||
class DB:
|
||||
"""LoveHate DB Utility Class"""
|
||||
|
||||
def __init__(self, bot) -> None:
|
||||
self.db_path: str|LiteralString = os.path.join("/usr/local/share",
|
||||
"sqlite_dbs", "lovehate.db")
|
||||
|
||||
async def get_wholovehates(self, thing: str, loves: bool = False,
|
||||
hates: bool = False) -> Union[list[tuple], bool]:
|
||||
self.db_path: str | LiteralString = os.path.join(
|
||||
"/usr/local/share", "sqlite_dbs", "lovehate.db"
|
||||
)
|
||||
|
||||
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>
|
||||
|
||||
|
||||
Args:
|
||||
thing (str): The <thing> to check
|
||||
loves (bool): Are we looking for loves?
|
||||
hates (bool): ...or are we looking for hates?
|
||||
Returns:
|
||||
Union[list[tuple], bool]
|
||||
"""
|
||||
"""
|
||||
|
||||
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?"
|
||||
params: tuple = tuple()
|
||||
@ -35,8 +39,11 @@ class DB:
|
||||
flag = 1
|
||||
elif not hates and not loves:
|
||||
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 await db_conn.execute(query, params) as db_cursor:
|
||||
result: list[tuple] = await db_cursor.fetchall()
|
||||
@ -44,28 +51,35 @@ class DB:
|
||||
if not result:
|
||||
return False
|
||||
return result
|
||||
|
||||
async def get_lovehates(self, loves: bool = False, hates: bool = False,
|
||||
user: Optional[str] = None, thing: Optional[str] = None) -> Union[list[tuple], bool]:
|
||||
|
||||
async def get_lovehates(
|
||||
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
|
||||
|
||||
|
||||
Args:
|
||||
loves (bool): Are we looking for loves?
|
||||
hates (bool): ...OR are we looking for hates?
|
||||
user (Optional[str]): the user to query against
|
||||
thing (Optional[str]): ... OR the thing to query against
|
||||
|
||||
|
||||
Returns:
|
||||
Union[list[tuple], bool]
|
||||
"""
|
||||
|
||||
query: str = ""
|
||||
params: tuple = tuple()
|
||||
|
||||
|
||||
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
|
||||
|
||||
if hates and loves:
|
||||
@ -79,10 +93,16 @@ class DB:
|
||||
|
||||
if user:
|
||||
query = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?"
|
||||
params = (user, flag,)
|
||||
params = (
|
||||
user,
|
||||
flag,
|
||||
)
|
||||
elif thing:
|
||||
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 await db_conn.execute(query, params) as db_cursor:
|
||||
@ -91,22 +111,27 @@ class DB:
|
||||
return False
|
||||
return result
|
||||
|
||||
|
||||
async def check_existence(self, user: str, thing: str) -> Optional[int]:
|
||||
"""
|
||||
Determine whether a user is opinionated on a <thing>
|
||||
|
||||
|
||||
Args:
|
||||
user (str): The user to check
|
||||
thing (str): The thing to check if the user has an opinion on
|
||||
Returns:
|
||||
Optional[int]
|
||||
"""
|
||||
|
||||
params = (user, thing,)
|
||||
|
||||
params = (
|
||||
user,
|
||||
thing,
|
||||
)
|
||||
|
||||
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()
|
||||
if not result:
|
||||
return None
|
||||
@ -116,29 +141,38 @@ class DB:
|
||||
async def update(self, user: str, thing: str, flag: int) -> str:
|
||||
"""
|
||||
Updates the lovehate database, and returns an appropriate response
|
||||
|
||||
|
||||
Args:
|
||||
user (str): The user to update
|
||||
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)
|
||||
Returns:
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
if not flag in range(-1, 2):
|
||||
raise LoveHateException(f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)")
|
||||
if flag not in range(-1, 2):
|
||||
raise LoveHateException(
|
||||
f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)"
|
||||
)
|
||||
|
||||
db_query: str = ""
|
||||
params: tuple = (user, thing,)
|
||||
params: tuple = (
|
||||
user,
|
||||
thing,
|
||||
)
|
||||
|
||||
already_opinionated: Optional[int] = await self.check_existence(user, thing)
|
||||
if already_opinionated:
|
||||
if already_opinionated:
|
||||
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:
|
||||
loves_or_hates: str = "loves"
|
||||
if already_opinionated == -1:
|
||||
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:
|
||||
match flag:
|
||||
case -1:
|
||||
@ -147,19 +181,22 @@ class DB:
|
||||
db_query = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, 1, ?)"
|
||||
case _:
|
||||
raise LoveHateException("Unknown error, default case matched")
|
||||
|
||||
|
||||
async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
|
||||
async with await db_conn.execute(db_query, params) as db_cursor:
|
||||
await db_conn.commit()
|
||||
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:
|
||||
case -1:
|
||||
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}."
|
||||
case 1:
|
||||
return f"We're done here, {user} loves {thing}."
|
||||
case _:
|
||||
raise LoveHateException("Unknown error, default case matched [2]")
|
||||
raise LoveHateException(
|
||||
"Unknown error, default case matched [2]"
|
||||
)
|
||||
|
581
util/misc_util.py
Normal file
581
util/misc_util.py
Normal 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
|
@ -4,6 +4,7 @@ from aiohttp import ClientSession, ClientTimeout
|
||||
|
||||
"""Radio Utils"""
|
||||
|
||||
|
||||
async def get_now_playing() -> Optional[str]:
|
||||
"""
|
||||
Get radio now playing
|
||||
@ -15,17 +16,21 @@ async def get_now_playing() -> Optional[str]:
|
||||
np_url: str = "https://api.codey.lol/radio/np"
|
||||
try:
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(np_url, headers={
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
}, timeout=ClientTimeout(connect=1.5, sock_read=1.5)) as request:
|
||||
async with await session.post(
|
||||
np_url,
|
||||
headers={
|
||||
"content-type": "application/json; charset=utf-8",
|
||||
},
|
||||
timeout=ClientTimeout(connect=1.5, sock_read=1.5),
|
||||
) as request:
|
||||
request.raise_for_status()
|
||||
response_json = await request.json()
|
||||
artistsong = response_json.get('artistsong')
|
||||
artistsong = response_json.get("artistsong")
|
||||
return artistsong
|
||||
except Exception as e:
|
||||
logging.critical("Now playing retrieval failed: %s",
|
||||
str(e))
|
||||
return None
|
||||
logging.critical("Now playing retrieval failed: %s", str(e))
|
||||
return None
|
||||
|
||||
|
||||
async def skip() -> bool:
|
||||
"""
|
||||
@ -38,13 +43,13 @@ async def skip() -> bool:
|
||||
try:
|
||||
ls_uri: str = "http://127.0.0.1:29000"
|
||||
async with ClientSession() as session:
|
||||
async with session.get(f"{ls_uri}/next",
|
||||
timeout=ClientTimeout(connect=2, sock_read=2)) as request:
|
||||
request.raise_for_status()
|
||||
text: Optional[str] = await request.text()
|
||||
return text == "OK"
|
||||
async with session.get(
|
||||
f"{ls_uri}/next", timeout=ClientTimeout(connect=2, sock_read=2)
|
||||
) as request:
|
||||
request.raise_for_status()
|
||||
text: Optional[str] = await request.text()
|
||||
return text == "OK"
|
||||
except Exception as e:
|
||||
logging.debug("Skip failed: %s", str(e))
|
||||
|
||||
return False # failsafe
|
||||
|
||||
logging.debug("Skip failed: %s", str(e))
|
||||
|
||||
return False # failsafe
|
||||
|
@ -6,21 +6,24 @@ import traceback
|
||||
from discord import Activity
|
||||
from typing import Optional, Union
|
||||
|
||||
|
||||
class Utility:
|
||||
"""Sing Utility"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.api_url: str = "http://127.0.0.1:52111/lyric/search"
|
||||
self.api_src: str = "DISC-HAVOC"
|
||||
|
||||
def parse_song_input(self, song: Optional[str] = None,
|
||||
activity: Optional[Activity] = None) -> Union[bool, tuple]:
|
||||
|
||||
def parse_song_input(
|
||||
self, song: Optional[str] = None, activity: Optional[Activity] = None
|
||||
) -> Union[bool, tuple]:
|
||||
"""
|
||||
Parse Song (Sing Command) Input
|
||||
|
||||
|
||||
Args:
|
||||
song (Optional[str]): Song to search
|
||||
activity (Optional[discord.Activity]): Discord activity, used to attempt lookup if no song is provided
|
||||
|
||||
|
||||
Returns:
|
||||
Union[bool, tuple]
|
||||
"""
|
||||
@ -29,13 +32,18 @@ class Utility:
|
||||
return False
|
||||
if not song and activity:
|
||||
if not activity.name:
|
||||
return False # No valid activity found
|
||||
return False # No valid activity found
|
||||
match activity.name.lower():
|
||||
case "codey toons" | "cider" | "sonixd":
|
||||
search_artist: str = " ".join(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: str = " ".join(
|
||||
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_song = str(activity.details)
|
||||
song = f"{search_artist} : {search_song}"
|
||||
case "tidal hi-fi":
|
||||
@ -43,110 +51,139 @@ class Utility:
|
||||
search_song = str(activity.details)
|
||||
song = f"{search_artist} : {search_song}"
|
||||
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.
|
||||
"""
|
||||
return False
|
||||
search_artist = str(activity.title) # type: ignore
|
||||
search_song = str(activity.artist) # type: ignore
|
||||
search_artist = str(activity.artist) # type: ignore
|
||||
search_song = str(activity.title) # type: ignore
|
||||
song = f"{search_artist} : {search_song}"
|
||||
case "serious.fm" | "cocks.fm" | "something":
|
||||
if not activity.details:
|
||||
song = str(activity.state)
|
||||
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)
|
||||
song = f"{search_artist} : {search_song}"
|
||||
case _:
|
||||
return False # Unsupported activity detected
|
||||
|
||||
search_split_by: str = ":" if not(song) or len(song.split(":")) > 1\
|
||||
else "-" # Support either : or - to separate artist/track
|
||||
return False # Unsupported activity detected
|
||||
|
||||
search_split_by: str = (
|
||||
":" if not (song) or len(song.split(":")) > 1 else "-"
|
||||
) # Support either : or - to separate artist/track
|
||||
if not song:
|
||||
return False
|
||||
search_artist = song.split(search_split_by)[0].strip()
|
||||
search_song = "".join(song.split(search_split_by)[1:]).strip()
|
||||
search_subsearch: Optional[str] = None
|
||||
if search_split_by == ":" and len(song.split(":")) > 2: # 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
|
||||
if (
|
||||
search_split_by == ":" and len(song.split(":")) > 2
|
||||
): # 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)
|
||||
except:
|
||||
except Exception as e:
|
||||
logging.debug("Exception: %s", str(e))
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
async def lyric_search(self, artist: str, song: str,
|
||||
sub: Optional[str] = None) -> Optional[list]:
|
||||
|
||||
async def lyric_search(
|
||||
self, artist: str, song: str, sub: Optional[str] = None
|
||||
) -> Optional[list]:
|
||||
"""
|
||||
Lyric Search
|
||||
|
||||
|
||||
Args:
|
||||
artist (str): Artist to search
|
||||
song (str): Song to search
|
||||
sub (Optional[str]): Lyrics for subsearch
|
||||
sub (Optional[str]): Lyrics for subsearch
|
||||
Returns:
|
||||
Optional[list]
|
||||
"""
|
||||
try:
|
||||
if not artist or not song:
|
||||
return [("FAIL! Artist/Song not provided",)]
|
||||
|
||||
|
||||
search_obj: dict = {
|
||||
'a': artist.strip(),
|
||||
's': song.strip(),
|
||||
'extra': True,
|
||||
'src': self.api_src,
|
||||
"a": artist.strip(),
|
||||
"s": song.strip(),
|
||||
"extra": True,
|
||||
"src": self.api_src,
|
||||
}
|
||||
|
||||
|
||||
if len(song.strip()) < 1:
|
||||
search_obj.pop('a')
|
||||
search_obj.pop('s')
|
||||
search_obj['t'] = artist.strip() # Parse failed, try title without sep
|
||||
|
||||
search_obj.pop("a")
|
||||
search_obj.pop("s")
|
||||
search_obj["t"] = artist.strip() # Parse failed, try title without sep
|
||||
|
||||
if sub and len(sub) >= 2:
|
||||
search_obj['sub'] = sub.strip()
|
||||
|
||||
search_obj["sub"] = sub.strip()
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with await session.post(self.api_url,
|
||||
json=search_obj,
|
||||
timeout=aiohttp.ClientTimeout(connect=5, sock_read=10)) as request:
|
||||
async with await session.post(
|
||||
self.api_url,
|
||||
json=search_obj,
|
||||
timeout=aiohttp.ClientTimeout(connect=5, sock_read=10),
|
||||
) as request:
|
||||
request.raise_for_status()
|
||||
response: dict = await request.json()
|
||||
if response.get('err'):
|
||||
if response.get("err"):
|
||||
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 = {
|
||||
'artist': response.get('artist'),
|
||||
'song': response.get('song'),
|
||||
'lyrics': out_lyrics,
|
||||
'src': response.get('src'),
|
||||
'confidence': float(response.get('confidence', 0.0)),
|
||||
'time': float(response.get('time', -1.0)),
|
||||
"artist": response.get("artist"),
|
||||
"song": response.get("song"),
|
||||
"lyrics": out_lyrics,
|
||||
"src": response.get("src"),
|
||||
"confidence": float(response.get("confidence", 0.0)),
|
||||
"time": float(response.get("time", -1.0)),
|
||||
}
|
||||
|
||||
lyrics = response_obj.get('lyrics')
|
||||
|
||||
lyrics = response_obj.get("lyrics")
|
||||
if not lyrics:
|
||||
return None
|
||||
response_obj['lyrics'] = textwrap.wrap(text=lyrics.strip(),
|
||||
width=4000, drop_whitespace=False,
|
||||
replace_whitespace=False, break_long_words=True,
|
||||
break_on_hyphens=True, 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)
|
||||
|
||||
response_obj["lyrics"] = textwrap.wrap(
|
||||
text=lyrics.strip(),
|
||||
width=1500,
|
||||
drop_whitespace=False,
|
||||
replace_whitespace=False,
|
||||
break_long_words=True,
|
||||
break_on_hyphens=True,
|
||||
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 [
|
||||
(
|
||||
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"{response_obj.get('time', -666.0):.4f}s",
|
||||
),
|
||||
response_obj.get('lyrics'),
|
||||
response_obj.get('lyrics_short'),
|
||||
]
|
||||
response_obj.get("lyrics"),
|
||||
response_obj.get("lyrics_short"),
|
||||
]
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return [f"Retrieval failed: {str(e)}"]
|
||||
return [f"Retrieval failed: {str(e)}"]
|
||||
|
Reference in New Issue
Block a user