initial push
This commit is contained in:
commit
59caad4c74
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||

|
||||
|
||||
# Discord-Havoc Rewrite (Pycord)
|
49
api.py
Normal file
49
api.py
Normal file
@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
import importlib
|
||||
from typing import Optional
|
||||
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)
|
||||
- **channel**: channel to target
|
||||
- **message**: message to send
|
||||
"""
|
||||
|
||||
guild: Optional[int] = None
|
||||
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(
|
||||
bot=self.bot,
|
||||
guild=data.guild,
|
||||
channel=data.channel,
|
||||
message=data.message,
|
||||
)
|
||||
return {
|
||||
'result': "presumed_success",
|
||||
}
|
||||
|
||||
|
||||
def __init__():
|
||||
import util # pylint: disable=redefined-outer-name, reimported, import-outside-toplevel
|
||||
importlib.reload(util)
|
||||
|
||||
__init__()
|
308
cogs/karma.py
Normal file
308
cogs/karma.py
Normal file
@ -0,0 +1,308 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=broad-exception-caught, bare-except, invalid-name
|
||||
|
||||
import sys
|
||||
from os import path
|
||||
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
|
||||
import constants
|
||||
import traceback
|
||||
import time
|
||||
import importlib
|
||||
import logging
|
||||
import discord
|
||||
import regex
|
||||
from typing import Pattern
|
||||
from aiohttp import ClientSession, ClientTimeout
|
||||
from discord.ext import bridge, commands, tasks
|
||||
|
||||
|
||||
|
||||
|
||||
class Util:
|
||||
"""Karma Utility"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.api_key: str = constants.PRV_API_KEY
|
||||
self.karma_endpoints_base_url: str = "https://api.codey.lol/karma/"
|
||||
self.karma_retrieval_url: str = f"{self.karma_endpoints_base_url}get"
|
||||
self.karma_update_url: str = f"{self.karma_endpoints_base_url}modify"
|
||||
self.karma_top_10_url: str = f"{self.karma_endpoints_base_url}top"
|
||||
self.timers: dict = {} # discord uid : timestamp, used for rate limiting
|
||||
self.karma_cooldown: int = 15 # 15 seconds between karma updates
|
||||
|
||||
async def get_karma(self, keyword: str) -> int|str:
|
||||
"""
|
||||
Get Karma for Keyword
|
||||
Args:
|
||||
keyword (str)
|
||||
Returns:
|
||||
int|str
|
||||
"""
|
||||
try:
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(self.karma_retrieval_url,
|
||||
json={'keyword': keyword},
|
||||
headers={
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}',
|
||||
}, timeout=ClientTimeout(connect=3, sock_read=5)) as request:
|
||||
resp = await request.json()
|
||||
return resp.get('count')
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return f"Failed-- {type(e).__name__}: {str(e)}"
|
||||
|
||||
async def get_top(self, n: int = 10) -> dict:
|
||||
"""
|
||||
Get top (n=10) Karma
|
||||
Args:
|
||||
n (int): Number of top results to return, default 10
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
try:
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(self.karma_top_10_url,
|
||||
json = {
|
||||
'n': n,
|
||||
},
|
||||
headers={
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}'
|
||||
}, timeout=ClientTimeout(connect=3, sock_read=5)) as request:
|
||||
resp: dict = await request.json()
|
||||
return resp
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
async def get_top_embed(self, n:int = 10) -> discord.Embed:
|
||||
"""
|
||||
Get Top Karma Embed
|
||||
Args:
|
||||
n (int): Number of top results to return, default 10
|
||||
Returns:
|
||||
discord.Embed
|
||||
"""
|
||||
top: dict = await self.get_top(n)
|
||||
top_formatted: str = ""
|
||||
for x, item in enumerate(top):
|
||||
top_formatted += f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n"
|
||||
top_formatted: str = top_formatted.strip()
|
||||
embed: discord.Embed = discord.Embed(title=f"Top {n} Karma",
|
||||
description=top_formatted,
|
||||
colour=0xff00ff)
|
||||
return embed
|
||||
|
||||
async def update_karma(self, display: str, _id: int, keyword: str, flag: int) -> bool:
|
||||
"""
|
||||
Update Karma for Keyword
|
||||
Args:
|
||||
display (str): Display name of the user who requested the update
|
||||
_id (int): Discord UID of the user who requested the update
|
||||
keyword (str): Keyword to update
|
||||
flag (int)
|
||||
"""
|
||||
if not flag in [0, 1]:
|
||||
return
|
||||
|
||||
reqObj: dict = {
|
||||
'granter': f"Discord: {display} ({_id})",
|
||||
'keyword': keyword,
|
||||
'flag': flag,
|
||||
}
|
||||
|
||||
try:
|
||||
async with ClientSession() as session:
|
||||
async with await session.post(self.karma_update_url,
|
||||
json=reqObj,
|
||||
headers={
|
||||
'content-type': 'application/json; charset=utf-8',
|
||||
'X-Authd-With': f'Bearer {self.api_key}',
|
||||
},
|
||||
timeout=ClientTimeout(connect=3, sock_read=5)) as request:
|
||||
result = await request.json()
|
||||
return result.get('success', False)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
|
||||
async def check_cooldown(self, user_id: int) -> bool:
|
||||
"""
|
||||
Check if member has met cooldown period prior to adjusting karma
|
||||
Args:
|
||||
user_id (int): The Discord UID to check
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
if not user_id in self.timers:
|
||||
return True
|
||||
now = int(time.time())
|
||||
if (now - self.timers[user_id]) < self.karma_cooldown:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class Karma(commands.Cog):
|
||||
"""Karma Cog for Havoc"""
|
||||
def __init__(self, bot):
|
||||
importlib.reload(constants)
|
||||
self.bot = bot
|
||||
self.util = Util(self.bot)
|
||||
# self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)')
|
||||
self.karma_regex: Pattern = regex.compile(r'(\b\w+(?:\s+\w+)*)(\+\+($|\s)|\-\-($|\s))')
|
||||
self.mention_regex: Pattern = regex.compile(r'(<@([0-9]{17,20})>)(\+\+|\-\-)')
|
||||
self.mention_regex_no_flag: Pattern = regex.compile(r'(<@([0-9]{17,20})>+)')
|
||||
self.karma_chanid: int = 1307065684785893406
|
||||
self.karma_msgid: int = 1325442184572567686
|
||||
|
||||
# asyncio.get_event_loop().create_task(self.bot.get_channel(self.karma_chanid).send("."))
|
||||
|
||||
try:
|
||||
self.update_karma_chan.start()
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
|
||||
@tasks.loop(seconds=30, reconnect=True)
|
||||
async def update_karma_chan(self):
|
||||
"""Update the Karma Chan Leaderboard"""
|
||||
try:
|
||||
top_embed = await self.util.get_top_embed(n=25)
|
||||
channel = self.bot.get_channel(self.karma_chanid)
|
||||
message_to_edit = await channel.fetch_message(self.karma_msgid)
|
||||
await message_to_edit.edit(embed=top_embed,
|
||||
content="## This message will automatically update periodically.")
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
|
||||
@commands.Cog.listener()
|
||||
async def on_message(self, message: discord.Message):
|
||||
"""
|
||||
Message hook, to monitor for ++/--
|
||||
Also monitors for messages to #karma to autodelete, only Havoc may post in #karma!
|
||||
"""
|
||||
|
||||
if message.channel.id == self.karma_chanid and not message.author.id == self.bot.user.id:
|
||||
"""Message to #karma not by Havoc, delete it"""
|
||||
await message.delete(reason="Messages to #karma are not allowed")
|
||||
removal_embed: discord.Embed = discord.Embed(
|
||||
title="Message Deleted",
|
||||
description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed."
|
||||
)
|
||||
return await message.author.send(embed=removal_embed)
|
||||
|
||||
|
||||
if message.author.id == self.bot.user.id: # Bots own message
|
||||
return
|
||||
|
||||
if not message.guild.id in [1145182936002482196, 1228740575235149855]: # Not a valid guild for cmd
|
||||
return
|
||||
|
||||
message_content: str = message.content.strip()
|
||||
mentions: list = regex.findall(self.mention_regex, message_content)
|
||||
|
||||
for mention in mentions:
|
||||
try:
|
||||
logging.debug("Mention: %s", mention)
|
||||
mentioned_uid: int = int(mention[1])
|
||||
friendly_flag: int = int(mention[2])
|
||||
guild: discord.Guild = self.bot.get_guild(message.guild.id)
|
||||
guild_member: discord.Member = guild.get_member(mentioned_uid)
|
||||
display: str = guild_member.display_name
|
||||
message_content: str = message_content.replace(mention[0], display)
|
||||
logging.debug("New message: %s", message_content)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
message_content: str = discord.utils.escape_markdown(message_content)
|
||||
|
||||
karma_regex: list[str] = regex.findall(self.karma_regex, message_content.strip())
|
||||
if not karma_regex: # Not a request to adjust karma
|
||||
return
|
||||
|
||||
flooding: bool = not await self.util.check_cooldown(message.author.id)
|
||||
exempt_uids: list[int] = [1172340700663255091, 992437729927376996]
|
||||
if flooding and not message.author.id in exempt_uids:
|
||||
return await message.add_reaction(emoji="❗")
|
||||
|
||||
processed_keywords_lc: list[str] = []
|
||||
|
||||
logging.debug("Matched: %s", karma_regex)
|
||||
|
||||
for matched_keyword in karma_regex:
|
||||
if len(matched_keyword) == 4:
|
||||
(keyword, friendly_flag, _, __) = matched_keyword
|
||||
else:
|
||||
(keyword, friendly_flag) = matched_keyword
|
||||
now: int = int(time.time())
|
||||
|
||||
flag: int = None
|
||||
match friendly_flag:
|
||||
case "++":
|
||||
flag: int = 0
|
||||
case "--":
|
||||
flag: int = 1
|
||||
case _:
|
||||
logging.info("Unknown flag %s", flag)
|
||||
continue
|
||||
|
||||
if keyword.lower() in processed_keywords_lc:
|
||||
continue
|
||||
|
||||
processed_keywords_lc.append(keyword.lower())
|
||||
|
||||
self.util.timers[message.author.id] = now
|
||||
|
||||
updated: bool = await self.util.update_karma(message.author.display_name,
|
||||
message.author.id, keyword, flag)
|
||||
if updated:
|
||||
return await message.add_reaction(emoji="👍")
|
||||
|
||||
@bridge.bridge_command()
|
||||
async def karma(self, ctx, *, keyword: str | None = None) -> None:
|
||||
"""With no arguments, top 10 karma is provided; a keyword can also be provided to lookup."""
|
||||
try:
|
||||
if not keyword:
|
||||
top_10_embed: discord.Embed = await self.util.get_top_embed()
|
||||
return await ctx.respond(embed=top_10_embed)
|
||||
|
||||
keyword: str = discord.utils.escape_markdown(keyword)
|
||||
|
||||
mentions: list[str] = regex.findall(self.mention_regex_no_flag, keyword)
|
||||
|
||||
for mention in mentions:
|
||||
try:
|
||||
mentioned_uid = int(mention[1])
|
||||
guild = self.bot.get_guild(ctx.guild.id)
|
||||
guild_member = guild.get_member(mentioned_uid)
|
||||
display = guild_member.display_name
|
||||
keyword = keyword.replace(mention[0], display)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
score: int = await self.util.get_karma(keyword)
|
||||
description: str = f"**{keyword}** has a karma of *{score}*"
|
||||
if isinstance(score, dict) and score.get('err'):
|
||||
description: str = f"*{score.get('errorText')}*"
|
||||
embed: discord.Embed = discord.Embed(title=f"Karma for {keyword}",
|
||||
description=description)
|
||||
return await ctx.respond(embed=embed)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
try:
|
||||
self.update_karma_chan.cancel()
|
||||
except:
|
||||
"""Safe to ignore"""
|
||||
pass
|
||||
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Karma(bot))
|
250
cogs/lovehate.py
Normal file
250
cogs/lovehate.py
Normal file
@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=broad-exception-caught
|
||||
|
||||
import traceback
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Optional
|
||||
import discord
|
||||
import aiosqlite as sqlite3
|
||||
from discord.ext import bridge, commands
|
||||
from util.lovehate_db import DB
|
||||
from constructors import LoveHateException
|
||||
|
||||
|
||||
class LoveHate(commands.Cog):
|
||||
"""LoveHate Cog for Havoc"""
|
||||
def __init__(self, bot):
|
||||
self.bot: discord.Bot = bot
|
||||
self.db = DB(self.bot)
|
||||
|
||||
def join_with_and(self, items: list) -> str:
|
||||
"""
|
||||
Join list with and added before last item
|
||||
Args:
|
||||
items (list)
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
if len(items) > 1:
|
||||
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:
|
||||
loves: list[tuple] = await self.db.get_lovehates(user=ctx.author.display_name, loves=True)
|
||||
if not loves:
|
||||
return await ctx.respond("You don't seem to love anything...")
|
||||
|
||||
out_loves: list = []
|
||||
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: list[tuple] = 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...")
|
||||
|
||||
out_loves_str: 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:
|
||||
thing: str = ctx.author.display_name
|
||||
if discord.utils.raw_mentions(thing):
|
||||
# There are mentions
|
||||
thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention
|
||||
thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name
|
||||
|
||||
who_loves: list[tuple] = await self.db.get_wholovehates(thing=thing,
|
||||
loves=True)
|
||||
if not who_loves:
|
||||
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:
|
||||
thing: str = ctx.author.display_name
|
||||
if discord.utils.raw_mentions(thing):
|
||||
# There are mentions
|
||||
thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention
|
||||
thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name
|
||||
|
||||
who_hates: list[tuple] = 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 = []
|
||||
for hater in who_hates:
|
||||
(hater,) = hater
|
||||
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}")
|
||||
except Exception as e:
|
||||
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)
|
||||
except Exception as e:
|
||||
await ctx.respond(f"Error: {str(e)}")
|
||||
traceback.print_exc()
|
||||
|
||||
@bridge.bridge_command()
|
||||
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:
|
||||
hates: list[tuple] = await self.db.get_lovehates(user=ctx.author.display_name,
|
||||
hates=True)
|
||||
if not hates:
|
||||
return await ctx.respond("You don't seem to hate anything...")
|
||||
|
||||
out_hates: list = []
|
||||
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)
|
||||
|
||||
return await ctx.respond(f"{ctx.author.mention} hates {out_hates_str}")
|
||||
|
||||
hates: list[tuple] = 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...")
|
||||
|
||||
out_hates_str: str = self.join_with_and(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'])
|
||||
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: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name
|
||||
|
||||
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'])
|
||||
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: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name
|
||||
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))
|
458
cogs/meme.py
Normal file
458
cogs/meme.py
Normal file
@ -0,0 +1,458 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
import os
|
||||
import traceback
|
||||
import json
|
||||
import io
|
||||
import asyncio
|
||||
import random
|
||||
from typing import LiteralString, Optional
|
||||
import logging
|
||||
import textwrap
|
||||
import regex
|
||||
import requests
|
||||
import discord
|
||||
from aiohttp import ClientSession
|
||||
from discord.ext import bridge, commands, tasks
|
||||
from jesusmemes import JesusMemeGenerator
|
||||
import scrapers.reddit_scrape as memeg
|
||||
import scrapers.explosm_scrape as explosmg
|
||||
import scrapers.xkcd_scrape as xkcdg
|
||||
import scrapers.smbc_scrape as smbcg
|
||||
import scrapers.qc_scrape as qcg
|
||||
import scrapers.dinosaur_scrape as dinog
|
||||
import scrapers.onion_scrape as oniong
|
||||
import scrapers.thn_scrape as thng
|
||||
import constants
|
||||
# pylint: disable=global-statement, bare-except, invalid-name, line-too-long
|
||||
|
||||
meme_choices = []
|
||||
BOT_CHANIDS = []
|
||||
|
||||
class Helper:
|
||||
"""Meme Helper"""
|
||||
def load_meme_choices(self) -> None:
|
||||
"""Load Available Meme Templates from JSON File"""
|
||||
global meme_choices
|
||||
|
||||
memes_file: str|LiteralString = os.path.join(os.path.dirname(__file__), "memes.json")
|
||||
with open(memes_file, 'r', encoding='utf-8') as f:
|
||||
meme_choices = json.loads(f.read())
|
||||
|
||||
class MemeView(discord.ui.View):
|
||||
"""Meme Selection discord.ui.View"""
|
||||
helper = Helper()
|
||||
helper.load_meme_choices()
|
||||
@discord.ui.select(
|
||||
placeholder = "Choose a Meme!",
|
||||
min_values = 1,
|
||||
max_values = 1,
|
||||
options = [
|
||||
discord.SelectOption(
|
||||
label=meme_label
|
||||
) for meme_label in meme_choices[0:24]
|
||||
]
|
||||
)
|
||||
async def select_callback(self, select: discord.ui.Select,
|
||||
interaction: discord.Interaction) -> None:
|
||||
"""Meme Selection Callback"""
|
||||
modal: discord.ui.Modal = MemeModal(meme=select.values[0], title="Meme Selected")
|
||||
await interaction.response.send_modal(modal)
|
||||
|
||||
class MemeModal(discord.ui.Modal):
|
||||
"""Meme Creation discord.ui.Modal"""
|
||||
def __init__(self, *args, meme: Optional[str] = None, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
self.selected_meme: str = meme
|
||||
self.meme_generator = JesusMemeGenerator()
|
||||
self.TEXT_LIMIT: int = 80
|
||||
|
||||
self.add_item(discord.ui.InputText(label="Top Text",
|
||||
style=discord.InputTextStyle.singleline))
|
||||
self.add_item(discord.ui.InputText(label="Bottom Text",
|
||||
style=discord.InputTextStyle.singleline))
|
||||
|
||||
async def callback(self, interaction: discord.Interaction) -> None:
|
||||
selected_meme: str = self.selected_meme
|
||||
meme_top_line: str = self.children[0].value.strip()
|
||||
meme_bottom_line: str = self.children[1].value.strip()
|
||||
if len(meme_top_line) > self.TEXT_LIMIT or len(meme_bottom_line) > self.TEXT_LIMIT:
|
||||
await interaction.response.send_message("ERR: Text is limited to 80 characters for each the top and bottom lines.")
|
||||
return
|
||||
|
||||
meme_link: str = await self.meme_generator.create_meme(top_line=meme_top_line, bottom_line=meme_bottom_line, meme=selected_meme)
|
||||
|
||||
embed: discord.Embed = discord.Embed(title="Generated Meme")
|
||||
embed.set_image(url=meme_link)
|
||||
embed.add_field(name="Meme", value=self.selected_meme, inline=True)
|
||||
await interaction.response.send_message(embeds=[embed])
|
||||
return
|
||||
|
||||
class Meme(commands.Cog):
|
||||
"""Meme Cog for Havoc"""
|
||||
|
||||
def __init__(self, bot) -> None:
|
||||
self.bot: discord.Bot = bot
|
||||
self.meme_choices: list = []
|
||||
self.meme_counter: int = 0
|
||||
self.THREADS: dict[dict[list]] = {
|
||||
# Format: Guild1: [ChanId : [Webhook, ThreadId], Guild2: [ChanId : [Webhook, ThreadId]
|
||||
'comic_explosm': {
|
||||
1298729744216359055: [constants.EXPLOSM_WEBHOOK, 1299165855493390367],
|
||||
1306414795049926676: [constants.EXPLOSM_WEBHOOK2, 1306416492304138364],
|
||||
},
|
||||
'comic_xkcd': {
|
||||
1298729744216359055: [constants.XKCD_WEBHOOK, 1299165928755433483],
|
||||
1306414795049926676: [constants.XKCD_WEBHOOK2, 1306416681991798854],
|
||||
},
|
||||
'comic_smbc': {
|
||||
1298729744216359055: [constants.SMBC_WEBHOOK, 1299166071038808104],
|
||||
1306414795049926676: [constants.SMBC_WEBHOOK2, 1306416842511745024],
|
||||
},
|
||||
'comic_qc': {
|
||||
1298729744216359055: [constants.QC_WEBHOOK, 1299392115364593674],
|
||||
1306414795049926676: [constants.QC_WEBHOOK2, 1306417084774744114],
|
||||
},
|
||||
'comic_dino': {
|
||||
1298729744216359055: [constants.DINO_WEBHOOK, 1299771918886506557],
|
||||
1306414795049926676: [constants.DINO_WEBHOOK2, 1306417286713704548],
|
||||
}
|
||||
}
|
||||
|
||||
self.NO_THREAD_WEBHOOKS: dict[list] = {
|
||||
'theonion': [constants.ONION_WEBHOOK, constants.ONION_WEBHOOK2],
|
||||
'thn': [constants.THN_WEBHOOK],
|
||||
'memes': [constants.MEME_WEBHOOK1, constants.MEME_WEBHOOK2],
|
||||
}
|
||||
|
||||
global BOT_CHANIDS
|
||||
BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit
|
||||
|
||||
self.meme_stream_loop.start()
|
||||
self.explosm_loop.start()
|
||||
|
||||
def is_spamchan() -> bool: # pylint: disable=no-method-argument
|
||||
"""Check if channel is spamchan"""
|
||||
def predicate(ctx):
|
||||
try:
|
||||
if not ctx.channel.id in BOT_CHANIDS:
|
||||
logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS)
|
||||
return ctx.channel.id in BOT_CHANIDS
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return commands.check(predicate)
|
||||
|
||||
|
||||
|
||||
async def do_autos(self, only_comics: Optional[bool] = False) -> None:
|
||||
"""
|
||||
Run Auto Posters
|
||||
Args:
|
||||
only_comics (Optional[bool]): default False
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
meme_grabber = memeg.MemeGrabber()
|
||||
explosm_grabber = explosmg.ExplosmGrabber()
|
||||
xkcd_grabber = xkcdg.XKCDGrabber()
|
||||
smbc_grabber = smbcg.SMBCGrabber()
|
||||
qc_grabber = qcg.QCGrabber()
|
||||
dino_grabber = dinog.DinosaurGrabber()
|
||||
onion_grabber = oniong.OnionGrabber()
|
||||
thn_grabber = thng.THNGrabber()
|
||||
explosm_comics = xkcd_comics = smbc_comics = qc_comics = dino_comics\
|
||||
= onions = thns = []
|
||||
memes: list[tuple] = await meme_grabber.get()
|
||||
try:
|
||||
try:
|
||||
explosm_comics: list[tuple] = await explosm_grabber.get()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
xkcd_comics: list[tuple] = await xkcd_grabber.get()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
smbc_comics: list[tuple] = await smbc_grabber.get()
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
qc_comics: list[tuple] = await qc_grabber.get()
|
||||
print(f"QC: {qc_comics}")
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
dino_comics: list[tuple] = await dino_grabber.get()
|
||||
except Exception as e:
|
||||
logging.debug("Dino failed: %s", str(e))
|
||||
pass
|
||||
try:
|
||||
onions: list[tuple] = await onion_grabber.get()
|
||||
except Exception as e:
|
||||
logging.debug("Onion failed: %s", str(e))
|
||||
pass
|
||||
try:
|
||||
thns: list[tuple] = await thn_grabber.get()
|
||||
except Exception as e:
|
||||
logging.debug("THNs failed: %s", str(e))
|
||||
pass
|
||||
except:
|
||||
traceback.print_exc()
|
||||
agents: list[str] = constants.HTTP_UA_LIST
|
||||
headers: dict = {
|
||||
'User-Agent': random.choice(agents)
|
||||
}
|
||||
if not only_comics:
|
||||
try:
|
||||
for meme in memes:
|
||||
(meme_id, meme_title, meme_url) = meme # pylint: disable=unused-variable
|
||||
request = requests.get(meme_url, stream=True, timeout=(5, 30), headers=headers)
|
||||
if not request.status_code == 200:
|
||||
continue
|
||||
meme_content: bytes = request.raw.read()
|
||||
for meme_hook in self.NO_THREAD_WEBHOOKS.get('memes'):
|
||||
meme_image: io.BytesIO = io.BytesIO(meme_content)
|
||||
ext: str = meme_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
async with ClientSession() as session:
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(meme_hook,
|
||||
session=session)
|
||||
await webhook.send(file=discord.File(meme_image,
|
||||
filename=f'img.{ext}'), username="r/memes")
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for comic in explosm_comics:
|
||||
(comic_title, comic_url) = comic
|
||||
comic_title: str = discord.utils.escape_markdown(comic_title)
|
||||
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers)
|
||||
comic_request.raise_for_status()
|
||||
comic_content: bytes = comic_request.raw.read()
|
||||
ext: str = comic_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
|
||||
async with ClientSession() as session:
|
||||
for chanid, _hook in self.THREADS.get('comic_explosm').items():
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_content)
|
||||
channel: int = chanid
|
||||
hook_uri: str = _hook[0]
|
||||
thread_id: int = _hook[1]
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
thread: discord.Thread = self.bot.get_channel(channel)\
|
||||
.get_thread(thread_id)
|
||||
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'),
|
||||
username="Cyanide & Happiness", thread=thread)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for comic in xkcd_comics:
|
||||
(comic_title, comic_url) = comic
|
||||
comic_title: str = discord.utils.escape_markdown(comic_title)
|
||||
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers)
|
||||
comic_request.raise_for_status()
|
||||
comic_content: bytes = comic_request.raw.read()
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_request.raw.read())
|
||||
ext: str = comic_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
|
||||
async with ClientSession() as session:
|
||||
for chanid, _hook in self.THREADS.get('comic_xkcd').items():
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_content)
|
||||
channel: int = chanid
|
||||
hook_uri: str = _hook[0]
|
||||
thread_id: int = _hook[1]
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
thread: discord.Thread = self.bot.get_channel(channel)\
|
||||
.get_thread(thread_id)
|
||||
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'),
|
||||
username="xkcd", thread=thread)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for comic in smbc_comics:
|
||||
(comic_title, comic_url) = comic
|
||||
comic_title: str = discord.utils.escape_markdown(comic_title)
|
||||
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers)
|
||||
comic_request.raise_for_status()
|
||||
comic_content: bytes = comic_request.raw.read()
|
||||
ext: str = comic_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
|
||||
async with ClientSession() as session:
|
||||
for chanid, _hook in self.THREADS.get('comic_smbc').items():
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_content)
|
||||
channel: int = chanid
|
||||
hook_uri: str = _hook[0]
|
||||
thread_id: int = _hook[1]
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
thread = self.bot.get_channel(channel)\
|
||||
.get_thread(thread_id)
|
||||
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'),
|
||||
username="SMBC", thread=thread)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for comic in qc_comics:
|
||||
logging.debug("Trying QC...")
|
||||
(comic_title, comic_url) = comic
|
||||
comic_title: str = discord.utils.escape_markdown(comic_title)
|
||||
comic_url: str = regex.sub(r'^http://ww\.', 'http://www.',
|
||||
comic_url)
|
||||
comic_url: str = regex.sub(r'\.pmg$', '.png',
|
||||
comic_url)
|
||||
comic_request = requests.get(comic_url, stream=True,
|
||||
timeout=(5, 20), headers=headers)
|
||||
comic_request.raise_for_status()
|
||||
comic_content: bytes = comic_request.raw.read()
|
||||
ext: str = comic_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
|
||||
async with ClientSession() as session:
|
||||
for chanid, _hook in self.THREADS.get('comic_qc').items():
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_content)
|
||||
channel: int = chanid
|
||||
hook_uri: str = _hook[0]
|
||||
thread_id: int = _hook[1]
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
thread: discord.Thread = self.bot.get_channel(channel)\
|
||||
.get_thread(thread_id)
|
||||
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'),
|
||||
username="Questionable Content", thread=thread)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
try:
|
||||
for comic in dino_comics:
|
||||
(comic_title, comic_url) = comic
|
||||
comic_title: str = discord.utils.escape_markdown(comic_title)
|
||||
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers)
|
||||
comic_request.raise_for_status()
|
||||
comic_content: bytes = comic_request.raw.read()
|
||||
ext = comic_url.split(".")[-1]\
|
||||
.split("?")[0].split("&")[0]
|
||||
|
||||
async with ClientSession() as session:
|
||||
for chanid, _hook in self.THREADS.get('comic_dino').items():
|
||||
comic_image: io.BytesIO = io.BytesIO(comic_content)
|
||||
channel: int = chanid
|
||||
hook_uri: str = _hook[0]
|
||||
thread_id: int = _hook[1]
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
thread: discord.Thread = self.bot.get_channel(channel)\
|
||||
.get_thread(thread_id)
|
||||
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'),
|
||||
username="Dinosaur Comics", thread=thread)
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for onion in onions:
|
||||
(onion_title, onion_description, onion_link, onion_video) = onion
|
||||
onion_description: list[str] = textwrap.wrap(text=onion_description,
|
||||
width=860, max_lines=1)[0]
|
||||
embed: discord.Embed = discord.Embed(title=onion_title)
|
||||
embed.add_field(name="Content", value=f"{onion_description[0:960]}\n-# {onion_link}")
|
||||
async with ClientSession() as session:
|
||||
for hook in self.NO_THREAD_WEBHOOKS.get('theonion'):
|
||||
hook_uri: str = hook
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
await webhook.send(embed=embed, username="The Onion")
|
||||
if onion_video:
|
||||
await webhook.send(f"^ video: {onion_video}")
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for thn in thns:
|
||||
logging.debug("Trying thn...")
|
||||
(thn_title, thn_description, thn_link, thn_pubdate, thn_video) = thn
|
||||
thn_description: list[str] = textwrap.wrap(text=thn_description,
|
||||
width=860, max_lines=1)[0]
|
||||
embed: discord.Embed = discord.Embed(title=thn_title)
|
||||
embed.add_field(name="Content", value=f"{thn_description[0:960]}\n-# {thn_link}")
|
||||
embed.add_field(name="Published", value=thn_pubdate, inline=False)
|
||||
async with ClientSession() as session:
|
||||
for hook in self.NO_THREAD_WEBHOOKS.get('thn'):
|
||||
hook_uri: str = hook
|
||||
webhook: discord.Webhook = discord.Webhook.from_url(hook_uri,
|
||||
session=session)
|
||||
await webhook.send(embed=embed, username="The Hacker News")
|
||||
if thn_video:
|
||||
await webhook.send(f"^ video: {thn_video}")
|
||||
await asyncio.sleep(2)
|
||||
except:
|
||||
pass
|
||||
except:
|
||||
# await self.bot.get_channel(self.MEMESTREAM_CHANID).send(f"FUCK, MY MEEMER! YOU DENTED MY MEEMER!")
|
||||
traceback.print_exc()
|
||||
|
||||
@tasks.loop(hours=12.0)
|
||||
async def meme_stream_loop(self) -> None:
|
||||
"""Meme Stream Loop (r/memes)"""
|
||||
try:
|
||||
await asyncio.sleep(10) # Try to ensure we are ready first
|
||||
self.meme_counter += 1
|
||||
if self.meme_counter == 1:
|
||||
return await self.do_autos(only_comics=True) # Skip first iteration!
|
||||
|
||||
await self.do_autos()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@tasks.loop(hours=0.5)
|
||||
async def explosm_loop(self) -> None:
|
||||
"""Comic Loop"""
|
||||
try:
|
||||
await asyncio.sleep(10) # Try to ensure we are ready first
|
||||
await self.do_autos(only_comics=True)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
@bridge.bridge_command()
|
||||
@is_spamchan() # pylint: disable=too-many-function-args
|
||||
async def meme(self, ctx) -> None:
|
||||
"""Create Meme"""
|
||||
await ctx.respond(view=MemeView())
|
||||
|
||||
@bridge.bridge_command(hidden=True)
|
||||
@commands.is_owner()
|
||||
async def domemestream(self, ctx) -> None:
|
||||
"""Run Meme Stream Auto Post"""
|
||||
try:
|
||||
await ctx.respond("Trying!", ephemeral=True)
|
||||
await self.do_autos()
|
||||
except:
|
||||
await ctx.respond("Fuck! :(", ephemeral=True)
|
||||
traceback.print_exc()
|
||||
|
||||
@bridge.bridge_command(hidden=True)
|
||||
@commands.is_owner()
|
||||
async def doexplosm(self, ctx) -> None:
|
||||
"""Run Comic Auto Posters"""
|
||||
try:
|
||||
await ctx.respond("Trying!", ephemeral=True)
|
||||
await self.do_autos(only_comics=True)
|
||||
except:
|
||||
await ctx.respond("Fuck! :(", ephemeral=True)
|
||||
traceback.print_exc()
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
self.meme_stream_loop.cancel()
|
||||
self.explosm_loop.cancel()
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Meme(bot))
|
14
cogs/memes.json
Normal file
14
cogs/memes.json
Normal file
@ -0,0 +1,14 @@
|
||||
[
|
||||
"10-Guy",
|
||||
"1990s-First-World-Problems",
|
||||
"Bill-Nye-The-Science-Guy",
|
||||
"Dont-You-Squidward",
|
||||
"Grumpy-Cat",
|
||||
"I-Was-Told-There-Would-Be",
|
||||
"I-Know-Fuck-Me-Right",
|
||||
"Putin",
|
||||
"Michael-Jackson-Popcorn",
|
||||
"Peter-Griffin-News",
|
||||
"Shut-Up-And-Take-My-Money-Fry",
|
||||
"Relaxed-Office-Guy"
|
||||
]
|
1253
cogs/misc.py
Normal file
1253
cogs/misc.py
Normal file
File diff suppressed because it is too large
Load Diff
388
cogs/misc_util.py
Normal file
388
cogs/misc_util.py
Normal file
@ -0,0 +1,388 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
import os
|
||||
import logging
|
||||
import traceback
|
||||
import random
|
||||
import datetime
|
||||
import pytz
|
||||
from typing import Optional, LiteralString
|
||||
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|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']
|
||||
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)
|
||||
|
||||
def sqlite_dict_factory(self, cursor: sqlite3.Cursor, row: sqlite3.Row) -> dict:
|
||||
"""
|
||||
SQLite Dict Factory for Rows Returned
|
||||
Args:
|
||||
cursor (sqlite3.Row)
|
||||
row (sqlite3.Row)
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
fields = [column[0] for column in cursor.description]
|
||||
return { key: value for key, value in zip(fields, row) }
|
||||
|
||||
|
||||
async def get_counter(self, counter: Optional[str] = None) -> dict:
|
||||
"""
|
||||
Get Counter
|
||||
Args:
|
||||
counter (Optional[str])
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
async with sqlite3.connect(self.dbs.get('stats'),
|
||||
timeout=3) as db_conn:
|
||||
db_conn.row_factory = self.sqlite_dict_factory
|
||||
query: str = "SELECT ? FROM stats LIMIT 1"
|
||||
if not counter:
|
||||
query: str = "SELECT * FROM stats LIMIT 1"
|
||||
async with await db_conn.execute(query, (counter,) if counter else None) as db_cursor:
|
||||
result: dict = await db_cursor.fetchone()
|
||||
return result
|
||||
|
||||
async def get_stats_embed(self) -> Embed:
|
||||
"""
|
||||
Get Stats Embed
|
||||
Returns:
|
||||
Embed
|
||||
"""
|
||||
counters: dict = await self.get_counter()
|
||||
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: str = 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
|
||||
"""
|
||||
async with sqlite3.connect(self.dbs.get('stats'),
|
||||
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: Optional[str] = None) -> tuple[str, str]:
|
||||
"""
|
||||
Get Definition from UD
|
||||
Args:
|
||||
term (Optional[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) -> tuple:
|
||||
"""
|
||||
Get Whisky
|
||||
Returns:
|
||||
tuple
|
||||
"""
|
||||
whisky_db: str|LiteralString = self.dbs.get('whisky')
|
||||
db_conn = await sqlite3.connect(database=whisky_db, timeout=2)
|
||||
db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1"
|
||||
db_cursor: sqlite3.Cursor = await db_conn.execute(db_query)
|
||||
db_result: tuple = await db_cursor.fetchone()
|
||||
|
||||
(name, category, description) = db_result
|
||||
name: str = regex.sub(r'(^\p{White_Space}|\r|\n)', '',
|
||||
regex.sub(r'\p{White_Space}{2,}', ' ',
|
||||
name.strip()))
|
||||
category: str = regex.sub(r'(^\p{White_Space}|\r|\n)', '',
|
||||
regex.sub(r'\p{White_Space}{2,}', ' ',
|
||||
category.strip()))
|
||||
description: str = 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) -> tuple:
|
||||
"""
|
||||
Get Drink
|
||||
Returns:
|
||||
tuple
|
||||
"""
|
||||
drinks_db: str|LiteralString = self.dbs.get('drinks')
|
||||
db_conn = await sqlite3.connect(database=drinks_db, timeout=2)
|
||||
db_query: str = "SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1"
|
||||
db_cursor: sqlite3.Cursor = await db_conn.execute(db_query)
|
||||
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) -> tuple:
|
||||
"""
|
||||
Get Strain
|
||||
Args:
|
||||
strain (Optional[str])
|
||||
Returns:
|
||||
tuple
|
||||
"""
|
||||
strains_db: str|LiteralString = self.dbs.get('strains')
|
||||
db_conn = await sqlite3.connect(database=strains_db, timeout=2)
|
||||
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: str = "SELECT name, description FROM strains_w_desc WHERE name LIKE ?"
|
||||
db_params: tuple = (f"%{strain.strip()}%",)
|
||||
|
||||
db_cursor: sqlite3.Cursor = await db_conn.execute(db_query, db_params)
|
||||
db_result: tuple = await db_cursor.fetchone()
|
||||
|
||||
return db_result
|
||||
|
||||
async def get_qajoke(self) -> tuple:
|
||||
"""
|
||||
Get QA Joke
|
||||
Returns:
|
||||
tuple
|
||||
"""
|
||||
qajoke_db: str|LiteralString = self.dbs.get('qajoke')
|
||||
async with sqlite3.connect(database=qajoke_db, timeout=2) as db:
|
||||
async with await db.execute('SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1') as cursor:
|
||||
(question, answer) = await cursor.fetchone()
|
||||
return (question, answer)
|
||||
return None
|
||||
|
||||
async def get_rjoke(self) -> tuple:
|
||||
"""
|
||||
Get r/joke Joke
|
||||
Returns:
|
||||
tuple
|
||||
"""
|
||||
rjokes_db: str|LiteralString = self.dbs.get('rjokes')
|
||||
async with sqlite3.connect(database=rjokes_db, timeout=2) as db:
|
||||
async with await db.execute('SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1') 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')
|
||||
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: dict = await request.json()
|
||||
fact: str = json.get('fact')
|
||||
return fact
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return f"Failed to get a random fact :( [{str(e)}]"
|
||||
|
||||
async def get_cookie(self) -> dict:
|
||||
"""
|
||||
Get Cookie
|
||||
Returns:
|
||||
dict
|
||||
"""
|
||||
async with sqlite3.connect(self.dbs.get('cookies'), timeout=2) as db_conn:
|
||||
async with await db_conn.execute("SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1") as db_cursor:
|
||||
(name, origin, image_url) = await db_cursor.fetchone()
|
||||
return {
|
||||
'name': name,
|
||||
'origin': origin,
|
||||
'image_url': image_url
|
||||
}
|
||||
|
||||
|
||||
def get_coffee(self) -> str:
|
||||
"""
|
||||
Get Coffee
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
try:
|
||||
randomCoffee: str = random.choice(self.COFFEES)
|
||||
if self.LAST_5_COFFEES and randomCoffee in self.LAST_5_COFFEES:
|
||||
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 False
|
||||
|
||||
def get_days_to_xmas(self) -> tuple[int|float]:
|
||||
"""
|
||||
Get # of Days until Xmas
|
||||
Returns:
|
||||
tuple[int|float]
|
||||
"""
|
||||
today: datetime = datetime.datetime.now(tz=pytz.UTC)
|
||||
xmas: datetime = datetime.datetime(
|
||||
year=today.year,
|
||||
month=12,
|
||||
day=25,
|
||||
tzinfo=pytz.UTC,
|
||||
)
|
||||
td: datetime.timedelta = (xmas - today) # pylint: disable=superfluous-parens
|
||||
days, hours, minutes, seconds, us, ms = self.tdTuple(td)
|
||||
|
||||
return (days, hours, minutes, seconds, ms, us)
|
||||
|
||||
async def get_randmsg(self) -> str:
|
||||
"""
|
||||
Get Random Message from randmsg.db
|
||||
Returns:
|
||||
str
|
||||
"""
|
||||
randmsg_db = self.dbs.get('randmsg')
|
||||
async with sqlite3.connect(database=randmsg_db,
|
||||
timeout=2) as db_conn:
|
||||
async with await db_conn.execute("SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1") as db_cursor:
|
||||
(result,) = await db_cursor.fetchone()
|
||||
return result
|
285
cogs/owner.py
Normal file
285
cogs/owner.py
Normal file
@ -0,0 +1,285 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=bare-except, broad-exception-caught
|
||||
|
||||
import io
|
||||
import random
|
||||
import asyncio
|
||||
import traceback
|
||||
from typing import Optional
|
||||
import discord
|
||||
import requests
|
||||
from discord.ext import bridge, commands
|
||||
import util
|
||||
|
||||
class Owner(commands.Cog):
|
||||
"""Owner Cog for Havoc"""
|
||||
def __init__(self, bot) -> None:
|
||||
self.bot: discord.Bot = bot
|
||||
self.former_roles_store: dict = {}
|
||||
self._temperature: int = random.randrange(20, 30)
|
||||
|
||||
@bridge.bridge_command(guild_ids=[1145182936002482196])
|
||||
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")
|
||||
|
||||
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")
|
||||
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: int = _temperature
|
||||
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 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:
|
||||
"""
|
||||
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)
|
||||
|
||||
channel: str = parameters[0]
|
||||
channel_mentions: list[str] = discord.utils.raw_channel_mentions(channel)
|
||||
if channel_mentions:
|
||||
channel: str = str(channel_mentions[0])
|
||||
msg: str = " ".join(parameters[1:])
|
||||
sent = await util.discord_helpers.send_message(self.bot, channel=channel,
|
||||
message=msg)
|
||||
if not sent:
|
||||
return await ctx.respond("**Failed.**", ephemeral=True)
|
||||
return await ctx.respond("**Done.**", ephemeral=True)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@commands.is_owner()
|
||||
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()))
|
||||
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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
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
|
||||
await asyncio.sleep(3)
|
||||
interaction = await ctx.interaction.original_response()
|
||||
await interaction.delete()
|
||||
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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
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 = discord.File(image, filename=f'img.{ext}')
|
||||
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:
|
||||
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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
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
|
||||
_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 = discord.File(image, filename=f'img.{ext}')
|
||||
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:
|
||||
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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
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
|
||||
_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 = 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:
|
||||
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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
try:
|
||||
if not ctx.guild.id == 1145182936002482196:
|
||||
return # Not home server!
|
||||
audit_reason: str = f"Einsperren von {ctx.user.display_name}"
|
||||
member: discord.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\
|
||||
else ctx.guild.get_role(1235406301614309386)
|
||||
member_roles: list[discord.Role] = [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:
|
||||
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
|
||||
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:
|
||||
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:
|
||||
await member.edit(roles=[]) # No roles
|
||||
else:
|
||||
former_roles: list[discord.Role] = self.former_roles_store.get(member.id)
|
||||
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.")
|
||||
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))
|
320
cogs/quote.py
Normal file
320
cogs/quote.py
Normal file
@ -0,0 +1,320 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=bare-except, broad-exception-caught
|
||||
|
||||
"""
|
||||
Quote cog for Havoc
|
||||
"""
|
||||
|
||||
import traceback
|
||||
import time
|
||||
import os
|
||||
import datetime
|
||||
import asyncio
|
||||
import discord
|
||||
import aiosqlite as sqlite3
|
||||
from discord.ext import bridge, commands
|
||||
|
||||
class DB:
|
||||
"""DB Utility for Quote Cog"""
|
||||
def __init__(self, bot):
|
||||
self.bot = 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:
|
||||
await self.bot.get_channel(self.hp_chanid).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 = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)])
|
||||
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:
|
||||
return traceback.format_exc()
|
||||
|
||||
async def fetch_quote(self, random: bool = False, quoteid: int = None, added_by: str = None, quoted_user: str = None, content: 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 = 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 = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)])
|
||||
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:
|
||||
return traceback.format_exc()
|
||||
|
||||
|
||||
class Quote(commands.Cog):
|
||||
"""Quote Cog for Havoc"""
|
||||
def __init__(self, bot):
|
||||
self.bot = bot
|
||||
self.db = DB(self.bot)
|
||||
|
||||
def is_homeserver(): # pylint: disable=no-method-argument
|
||||
"""Check if channel/interaction is within homeserver"""
|
||||
def predicate(ctx):
|
||||
try:
|
||||
return ctx.guild.id == 1145182936002482196
|
||||
except:
|
||||
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:
|
||||
await self.bot.get_channel(hp_chanid).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:
|
||||
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:
|
||||
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=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))
|
108
cogs/radio.py
Normal file
108
cogs/radio.py
Normal file
@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=bare-except, broad-exception-caught, invalid-name
|
||||
|
||||
import logging
|
||||
import traceback
|
||||
from discord.ext import bridge, commands, tasks
|
||||
import discord
|
||||
|
||||
class Radio(commands.Cog):
|
||||
"""Radio Cog for Havoc"""
|
||||
def __init__(self, bot: discord.Bot) -> None:
|
||||
self.bot: discord.Bot = bot
|
||||
self.channels: dict[tuple] = {
|
||||
'sfm': (1145182936002482196, 1221615558492029050), # Tuple: Guild Id, Chan Id
|
||||
}
|
||||
self.STREAM_URL: str = "https://relay.sfm.codey.lol/aces.ogg"
|
||||
|
||||
try:
|
||||
self.radio_state_loop.cancel()
|
||||
except Exception as 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(): # pylint: disable=no-method-argument
|
||||
"""Check if channel is radio chan"""
|
||||
def predicate(ctx):
|
||||
try:
|
||||
return ctx.channel.id == 1221615558492029050
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
return commands.check(predicate)
|
||||
|
||||
@bridge.bridge_command()
|
||||
@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']
|
||||
channel: discord.TextChannel = self.bot.get_guild(radio_guild)\
|
||||
.get_channel(radio_chan)
|
||||
if not self.bot.voice_clients:
|
||||
await channel.connect()
|
||||
try:
|
||||
try:
|
||||
self.radio_state_loop.cancel()
|
||||
except Exception as 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...")
|
||||
traceback.print_exc()
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return
|
||||
|
||||
@tasks.loop(seconds=2.0)
|
||||
async def radio_state_loop(self) -> None:
|
||||
"""Radio State Loop"""
|
||||
try:
|
||||
(radio_guild, radio_chan) = self.channels['sfm']
|
||||
try:
|
||||
vc: discord.VoiceClient = self.bot.voice_clients[-1]
|
||||
except:
|
||||
logging.debug("No voice client, establishing new VC connection...")
|
||||
channel = self.bot.get_guild(radio_guild)\
|
||||
.get_channel(radio_chan)
|
||||
await channel.connect()
|
||||
vc = self.bot.voice_clients[-1]
|
||||
if not(vc.is_playing()) or vc.is_paused():
|
||||
logging.info("Detected VC not playing... playing!")
|
||||
source = discord.FFmpegOpusAudio(self.STREAM_URL,
|
||||
before_options="-timeout 3000000")
|
||||
vc.play(source, after=lambda e: logging.info("Error: %s", e)\
|
||||
if e else None)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
|
||||
# pylint: enable=superfluous-parens
|
||||
|
||||
def cog_unload(self) -> None:
|
||||
"""Run on Cog Unload"""
|
||||
self.radio_state_loop.cancel()
|
||||
|
||||
def setup(bot) -> None:
|
||||
"""Run on Cog Load"""
|
||||
bot.add_cog(Radio(bot))
|
184
cogs/sing.py
Normal file
184
cogs/sing.py
Normal file
@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=bare-except, broad-exception-caught, global-statement, invalid-name
|
||||
|
||||
import traceback
|
||||
import logging
|
||||
from typing import Optional, Pattern
|
||||
import urllib
|
||||
import discord
|
||||
import regex
|
||||
from util.sing_util import Utility
|
||||
from discord.ext import bridge, commands
|
||||
|
||||
BOT_CHANIDS = []
|
||||
|
||||
class Sing(commands.Cog):
|
||||
"""Sing Cog for Havoc"""
|
||||
def __init__(self, bot):
|
||||
self.bot: discord.Bot = 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(): # pylint: disable=no-method-argument
|
||||
"""Check if channel is spam chan"""
|
||||
def predicate(ctx):
|
||||
try:
|
||||
if not ctx.channel.id in BOT_CHANIDS:
|
||||
logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS)
|
||||
return ctx.channel.id in BOT_CHANIDS
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
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:
|
||||
return
|
||||
|
||||
for _activity in ctx.author.activities:
|
||||
if _activity.type == discord.ActivityType.listening:
|
||||
activity: discord.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
|
||||
|
||||
(search_artist, search_song, search_subsearch) = self.utility.parse_song_input(song, activity)
|
||||
|
||||
# await ctx.respond(f"So, {search_song} by {search_artist}? Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging
|
||||
search_result: list[str] = await self.utility.lyric_search(search_artist, search_song,
|
||||
search_subsearch)
|
||||
|
||||
if len(search_result) == 1:
|
||||
return await ctx.respond(search_result[0].strip())
|
||||
|
||||
(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: list[str] = 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: str = f"Found on: {search_result_src}"
|
||||
section: str = 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.replace("\n", "\n\n"))
|
||||
)
|
||||
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:]:
|
||||
await ctx.send(embed=embed)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return 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
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
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!
|
||||
activity: Optional[discord.Activity] = None
|
||||
for _activity in ctx.interaction.guild.get_member(member_id).activities:
|
||||
if _activity.type == discord.ActivityType.listening:
|
||||
activity: discord.Activity = _activity
|
||||
parsed: 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)
|
||||
if IS_SPAMCHAN:
|
||||
await ctx.respond(f"***Reading activity of {member_display}...***")
|
||||
|
||||
(search_artist, search_song, search_subsearch) = parsed
|
||||
await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord
|
||||
search_result: list = await self.utility.lyric_search(search_artist, search_song,
|
||||
search_subsearch)
|
||||
|
||||
if len(search_result) == 1:
|
||||
return await ctx.send(search_result[0].strip())
|
||||
|
||||
(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 index is shortened lyrics
|
||||
|
||||
if not IS_SPAMCHAN:
|
||||
search_result_wrapped: list[str] = search_result_wrapped_short # Swap for shortened lyrics if not spam chan
|
||||
|
||||
embeds: list[Optional[discord.Embed]] = []
|
||||
c: int = 0
|
||||
footer: str = "To be continued..." #Placeholder
|
||||
for section in search_result_wrapped:
|
||||
c+=1
|
||||
if c == len(search_result_wrapped):
|
||||
footer: str = 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.replace("\n", "\n\n"))
|
||||
)
|
||||
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)
|
||||
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))
|
15
constructors.py
Normal file
15
constructors.py
Normal file
@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
"""
|
||||
AI
|
||||
"""
|
||||
class AIException(Exception):
|
||||
"""AI Exception (generic)"""
|
||||
pass
|
||||
|
||||
"""
|
||||
LoveHate
|
||||
"""
|
||||
class LoveHateException(Exception):
|
||||
"""Love Hate Exception (generic)"""
|
||||
pass
|
97
disc_havoc.py
Normal file
97
disc_havoc.py
Normal file
@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3.12
|
||||
# pylint: disable=bare-except, invalid-name, import-outside-toplevel
|
||||
|
||||
import os
|
||||
import logging
|
||||
import importlib
|
||||
import discord
|
||||
import setproctitle
|
||||
import hypercorn
|
||||
import hypercorn.asyncio
|
||||
from dotenv import load_dotenv
|
||||
from discord.ext import bridge, commands
|
||||
from termcolor import colored
|
||||
import api
|
||||
|
||||
logging.basicConfig(level=logging.INFO,
|
||||
format='%(asctime)s %(message)s',
|
||||
encoding='utf-8')
|
||||
setproctitle.setproctitle('disc-havoc')
|
||||
|
||||
owners = [1172340700663255091, 992437729927376996]
|
||||
BOT_CHANIDS = [
|
||||
1145182936442875997,
|
||||
1157535700774834236,
|
||||
1156710277266542624,
|
||||
1179232748385341530,
|
||||
1219638300654964807,
|
||||
1193632849740439665,
|
||||
1202288798315335831,
|
||||
1157529874936909934,
|
||||
1272333206066167959,
|
||||
1228740577068322839,
|
||||
1228740577068322841,
|
||||
1324142398741151784,
|
||||
]
|
||||
|
||||
cogs_list = [
|
||||
'misc',
|
||||
'owner',
|
||||
'sing',
|
||||
'meme',
|
||||
'ai',
|
||||
'karma',
|
||||
'lovehate',
|
||||
'quote',
|
||||
'radio',
|
||||
]
|
||||
|
||||
bot_activity = discord.CustomActivity(name="I made cookies!")
|
||||
|
||||
load_dotenv()
|
||||
|
||||
intents = discord.Intents.all()
|
||||
intents.message_content = True
|
||||
bot = bridge.Bot(command_prefix=".", intents=intents,
|
||||
owner_ids=owners, activity=bot_activity,
|
||||
help_command=commands.MinimalHelpCommand())
|
||||
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
"""Run on Bot Ready"""
|
||||
logging.info("%s online!", bot.user)
|
||||
|
||||
def load_exts(initialRun=True):
|
||||
"""Load Cogs/Extensions"""
|
||||
load_method = bot.load_extension if initialRun else bot.reload_extension
|
||||
|
||||
for cog in cogs_list:
|
||||
logging.info("Loading: %s", cog)
|
||||
load_method(f'cogs.{cog}')
|
||||
|
||||
# asyncio.get_event_loop().create_task(bot.sync_commands())
|
||||
|
||||
importlib.reload(api)
|
||||
from api import API # pylint: disable=unused-import
|
||||
api_config = hypercorn.config.Config()
|
||||
api_config.bind = "10.10.10.100:5992"
|
||||
api_instance = api.API(bot)
|
||||
try:
|
||||
bot.fapi_task.cancel()
|
||||
except:
|
||||
pass
|
||||
|
||||
logging.info("Starting FAPI Task")
|
||||
|
||||
bot.fapi_task = bot.loop.create_task(hypercorn.asyncio.serve(api_instance.api_app, api_config))
|
||||
|
||||
def __init__():
|
||||
logging.info(colored(f"Log level: {logging.getLevelName(logging.root.level)}", "red", attrs=['reverse']))
|
||||
bot.BOT_CHANIDS = BOT_CHANIDS
|
||||
bot.load_exts = load_exts
|
||||
bot.load_exts()
|
||||
bot.run(os.getenv('TOKEN'))
|
||||
|
||||
if __name__ == "__main__":
|
||||
__init__()
|
13
requirements.txt
Normal file
13
requirements.txt
Normal file
@ -0,0 +1,13 @@
|
||||
aiosqlite==0.20.0
|
||||
beautifulsoup4==4.12.3
|
||||
edge_tts==6.1.12
|
||||
feedparser==6.0.11
|
||||
Flask==3.0.3
|
||||
nvdlib==0.7.7
|
||||
openai==1.54.3
|
||||
requests_async==0.6.2
|
||||
shazamio==0.7.0
|
||||
streamrip==2.0.5
|
||||
Unidecode==1.3.6
|
||||
Unidecode==1.3.8
|
||||
websockets==12.0
|
6
util/__init__.py
Normal file
6
util/__init__.py
Normal file
@ -0,0 +1,6 @@
|
||||
#!/usr/bin/env python3.12
|
||||
|
||||
import importlib
|
||||
|
||||
from . import discord_helpers
|
||||
importlib.reload(discord_helpers)
|
52
util/discord_helpers.py
Normal file
52
util/discord_helpers.py
Normal file
@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import re
|
||||
import discord
|
||||
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
|
||||
"""
|
||||
Get Channel by Name
|
||||
Args:
|
||||
bot (discord.Bot)
|
||||
channel (str)
|
||||
guild (int|None)
|
||||
Returns:
|
||||
Optional[Any]
|
||||
"""
|
||||
channel: str = re.sub(r'^#', '', channel.strip())
|
||||
if not guild:
|
||||
return discord.utils.get(bot.get_all_channels(),
|
||||
name=channel)
|
||||
else:
|
||||
channels: list = bot.get_guild(guild).channels
|
||||
for _channel in channels:
|
||||
if _channel.name.lower() == channel.lower().strip():
|
||||
return _channel
|
||||
return
|
||||
|
||||
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)
|
||||
message (str)
|
||||
guild (int|None)
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
if channel.isnumeric():
|
||||
channel: int = int(channel)
|
||||
_channel = bot.get_channel(channel)
|
||||
else:
|
||||
channel: str = re.sub(r'^#', '', channel.strip())
|
||||
_channel = await get_channel_by_name(bot=bot,
|
||||
channel=channel, guild=guild)
|
||||
await _channel.send(message)
|
162
util/lovehate_db.py
Normal file
162
util/lovehate_db.py
Normal file
@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import os
|
||||
import logging
|
||||
from typing import Optional, LiteralString
|
||||
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) -> 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:
|
||||
list[tuple]
|
||||
"""
|
||||
|
||||
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?"
|
||||
params: tuple = tuple()
|
||||
flag: Optional[int] = None
|
||||
|
||||
if hates and loves:
|
||||
raise LoveHateException("Both hates and loves may not be True")
|
||||
elif hates:
|
||||
flag: int = -1
|
||||
elif loves:
|
||||
flag: int = 1
|
||||
elif not hates and not loves:
|
||||
raise LoveHateException("Neither loves nor hates were requested")
|
||||
|
||||
params: tuple = (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()
|
||||
logging.debug("Result: %s", result)
|
||||
if not result:
|
||||
return False
|
||||
return result
|
||||
|
||||
async def get_lovehates(self, loves: bool = False, hates: bool = False,
|
||||
user: str = None, thing: str = None) -> 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:
|
||||
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")
|
||||
|
||||
flag: Optional[int] = None
|
||||
|
||||
if hates and loves:
|
||||
raise LoveHateException("Both hates and loves may not be True")
|
||||
elif hates:
|
||||
flag: int = -1
|
||||
elif loves:
|
||||
flag: int = 1
|
||||
elif not hates and not loves:
|
||||
raise LoveHateException("Neither loves nor hates were requested")
|
||||
|
||||
if user:
|
||||
query: str = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?"
|
||||
params: tuple = (user, flag,)
|
||||
elif thing:
|
||||
query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?"
|
||||
params: tuple = (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 = await db_cursor.fetchall()
|
||||
if not result:
|
||||
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,)
|
||||
|
||||
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:
|
||||
result = await db_cursor.fetchone()
|
||||
if not result:
|
||||
return None
|
||||
(_, flag) = result
|
||||
return flag
|
||||
|
||||
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:
|
||||
str
|
||||
"""
|
||||
if not flag 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,)
|
||||
|
||||
already_opinionated: bool = await self.check_existence(user, thing)
|
||||
if already_opinionated:
|
||||
if flag == 0:
|
||||
db_query: str = "DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?"
|
||||
else:
|
||||
loves_or_hates: str = "loves"
|
||||
if already_opinionated == -1:
|
||||
loves_or_hates: str = "hates"
|
||||
raise LoveHateException(f"But {user} already {loves_or_hates} {thing}...")
|
||||
else:
|
||||
match flag:
|
||||
case -1:
|
||||
db_query: str = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, -1, ?)"
|
||||
case 1:
|
||||
db_query: str = "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")
|
||||
match flag:
|
||||
case -1:
|
||||
return f"We're done here, {user} hates {thing}."
|
||||
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]")
|
139
util/sing_util.py
Normal file
139
util/sing_util.py
Normal file
@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3.12
|
||||
import logging
|
||||
import regex
|
||||
import aiohttp
|
||||
import textwrap
|
||||
import traceback
|
||||
from typing import Optional
|
||||
from discord import Activity
|
||||
|
||||
class Utility:
|
||||
"""Sing Utility"""
|
||||
def __init__(self):
|
||||
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) -> 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:
|
||||
bool|tuple
|
||||
"""
|
||||
logging.debug("Activity? %s", activity)
|
||||
try:
|
||||
if (not song or len(song) < 2) and not activity:
|
||||
# pylint: disable=superfluous-parens
|
||||
return False
|
||||
# pylint: enable=superfluous-parens
|
||||
if not song and activity:
|
||||
match activity.name.lower():
|
||||
case "codey toons" | "cider" | "sonixd":
|
||||
search_artist: str = " ".join(str(activity.state)\
|
||||
.strip().split(" ")[1:])
|
||||
search_artist: str = regex.sub(r"(\s{0,})(\[(spotify|tidal|sonixd|browser|yt music)])$", "",
|
||||
search_artist.strip(), flags=regex.IGNORECASE)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "tidal hi-fi":
|
||||
search_artist: str = str(activity.state)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "spotify":
|
||||
search_artist: str = str(activity.title)
|
||||
search_song: str = str(activity.artist)
|
||||
song: str = f"{search_artist} : {search_song}"
|
||||
case "serious.fm" | "cocks.fm" | "something":
|
||||
if not activity.details:
|
||||
song: str = str(activity.state)
|
||||
else:
|
||||
search_artist: str = str(activity.state)
|
||||
search_song: str = str(activity.details)
|
||||
song: str = 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
|
||||
search_artist: str = song.split(search_split_by)[0].strip()
|
||||
search_song: str = "".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: str = 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: str = "".join(song.split(search_split_by)[2:]) # Lyric text from split index 2 and beyond
|
||||
return (search_artist, search_song, search_subsearch)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
return False
|
||||
|
||||
async def lyric_search(self, artist: str, song: str,
|
||||
sub: Optional[str] = None) -> list[str]:
|
||||
"""
|
||||
Lyric Search
|
||||
Args:
|
||||
artist (str): Artist to search
|
||||
song (str): Song to search
|
||||
sub (Optional[str]): Lyrics for subsearch
|
||||
"""
|
||||
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,
|
||||
}
|
||||
|
||||
if len(song.strip()) < 1:
|
||||
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()
|
||||
|
||||
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:
|
||||
request.raise_for_status()
|
||||
response: dict = await request.json()
|
||||
if response.get('err'):
|
||||
return [f"ERR: {response.get('errorText')}"]
|
||||
|
||||
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')),
|
||||
'time': float(response.get('time')),
|
||||
}
|
||||
|
||||
lyrics = response_obj.get('lyrics')
|
||||
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)
|
||||
|
||||
return [
|
||||
(
|
||||
response_obj.get('artist'), response_obj.get('song'), response_obj.get('src'),
|
||||
f"{int(response_obj.get('confidence'))}%",
|
||||
f"{response_obj.get('time', -666.0):.4f}s",
|
||||
),
|
||||
response_obj.get('lyrics'),
|
||||
response_obj.get('lyrics_short'),
|
||||
]
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
return [f"Retrieval failed: {str(e)}"]
|
Loading…
x
Reference in New Issue
Block a user