reformat (black)

This commit is contained in:
codey 2025-04-17 14:35:56 -04:00
parent d12b066c8e
commit 1bb482315e
20 changed files with 1928 additions and 1326 deletions

8
api.py
View File

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

View File

@ -1,6 +1,7 @@
import sys import sys
from os import path from os import path
sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) )
sys.path.append(path.dirname(path.dirname(path.abspath(__file__))))
import constants import constants
import traceback import traceback
import time import time
@ -14,8 +15,10 @@ from aiohttp import ClientSession, ClientTimeout
from discord.ext import bridge, commands, tasks from discord.ext import bridge, commands, tasks
from disc_havoc import Havoc from disc_havoc import Havoc
class Util: class Util:
"""Karma Utility""" """Karma Utility"""
def __init__(self, bot: Havoc): def __init__(self, bot: Havoc):
self.bot: Havoc = bot self.bot: Havoc = bot
self.api_key: str = constants.PRV_API_KEY self.api_key: str = constants.PRV_API_KEY
@ -37,14 +40,17 @@ class Util:
""" """
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.karma_retrieval_url, async with await session.post(
json={'keyword': keyword}, self.karma_retrieval_url,
json={"keyword": keyword},
headers={ headers={
'content-type': 'application/json; charset=utf-8', "content-type": "application/json; charset=utf-8",
'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}', "X-Authd-With": f"Bearer {constants.KARMA_API_KEY}",
}, timeout=ClientTimeout(connect=3, sock_read=5)) as request: },
timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
resp = await request.json() resp = await request.json()
return resp.get('count') return resp.get("count")
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return False return False
@ -60,21 +66,24 @@ class Util:
""" """
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.karma_top_10_url, async with await session.post(
json = { self.karma_top_10_url,
'n': n, json={
"n": n,
}, },
headers={ headers={
'content-type': 'application/json; charset=utf-8', "content-type": "application/json; charset=utf-8",
'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}' "X-Authd-With": f"Bearer {constants.KARMA_API_KEY}",
}, timeout=ClientTimeout(connect=3, sock_read=5)) as request: },
timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
resp: dict = await request.json() resp: dict = await request.json()
return resp return resp
except: except:
traceback.print_exc() traceback.print_exc()
return None return None
async def get_top_embed(self, n:int = 10) -> Optional[discord.Embed]: async def get_top_embed(self, n: int = 10) -> Optional[discord.Embed]:
""" """
Get Top Karma Embed Get Top Karma Embed
@ -88,14 +97,18 @@ class Util:
return None return None
top_formatted: str = "" top_formatted: str = ""
for x, item in enumerate(top): for x, item in enumerate(top):
top_formatted += f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n" top_formatted += (
f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n"
)
top_formatted = top_formatted.strip() top_formatted = top_formatted.strip()
embed: discord.Embed = discord.Embed(title=f"Top {n} Karma", embed: discord.Embed = discord.Embed(
description=top_formatted, title=f"Top {n} Karma", description=top_formatted, colour=0xFF00FF
colour=0xff00ff) )
return embed return embed
async def update_karma(self, display: str, _id: int, keyword: str, flag: int) -> bool: async def update_karma(
self, display: str, _id: int, keyword: str, flag: int
) -> bool:
""" """
Update Karma for Keyword Update Karma for Keyword
Args: Args:
@ -111,27 +124,28 @@ class Util:
return False return False
reqObj: dict = { reqObj: dict = {
'granter': f"Discord: {display} ({_id})", "granter": f"Discord: {display} ({_id})",
'keyword': keyword, "keyword": keyword,
'flag': flag, "flag": flag,
} }
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.karma_update_url, async with await session.post(
self.karma_update_url,
json=reqObj, json=reqObj,
headers={ headers={
'content-type': 'application/json; charset=utf-8', "content-type": "application/json; charset=utf-8",
'X-Authd-With': f'Bearer {self.api_key}', "X-Authd-With": f"Bearer {self.api_key}",
}, },
timeout=ClientTimeout(connect=3, sock_read=5)) as request: timeout=ClientTimeout(connect=3, sock_read=5),
) as request:
result = await request.json() result = await request.json()
return result.get('success', False) return result.get("success", False)
except: except:
traceback.print_exc() traceback.print_exc()
return False return False
async def check_cooldown(self, user_id: int) -> bool: async def check_cooldown(self, user_id: int) -> bool:
""" """
Check if member has met cooldown period prior to adjusting karma Check if member has met cooldown period prior to adjusting karma
@ -149,17 +163,19 @@ class Util:
return True return True
class Karma(commands.Cog): class Karma(commands.Cog):
"""Karma Cog for Havoc""" """Karma Cog for Havoc"""
def __init__(self, bot: Havoc): def __init__(self, bot: Havoc):
importlib.reload(constants) importlib.reload(constants)
self.bot: Havoc = bot self.bot: Havoc = bot
self.util = Util(self.bot) self.util = Util(self.bot)
# self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)') # self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)')
self.karma_regex: Pattern = regex.compile(r'(\b\w+(?:\s+\w+)*)(\+\+($|\s)|\-\-($|\s))') self.karma_regex: Pattern = regex.compile(
self.mention_regex: Pattern = regex.compile(r'(<@([0-9]{17,20})>)(\+\+|\-\-)') r"(\b\w+(?:\s+\w+)*)(\+\+($|\s)|\-\-($|\s))"
self.mention_regex_no_flag: Pattern = regex.compile(r'(<@([0-9]{17,20})>+)') )
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_chanid: int = 1307065684785893406
self.karma_msgid: int = 1325442184572567686 self.karma_msgid: int = 1325442184572567686
@ -170,7 +186,6 @@ class Karma(commands.Cog):
except Exception as e: except Exception as e:
pass pass
@tasks.loop(seconds=30, reconnect=True) @tasks.loop(seconds=30, reconnect=True)
async def update_karma_chan(self) -> None: async def update_karma_chan(self) -> None:
"""Update the Karma Chan Leaderboard""" """Update the Karma Chan Leaderboard"""
@ -180,12 +195,13 @@ class Karma(commands.Cog):
if not isinstance(channel, discord.TextChannel): if not isinstance(channel, discord.TextChannel):
return return
message_to_edit = await channel.fetch_message(self.karma_msgid) message_to_edit = await channel.fetch_message(self.karma_msgid)
await message_to_edit.edit(embed=top_embed, await message_to_edit.edit(
content="## This message will automatically update periodically.") embed=top_embed,
content="## This message will automatically update periodically.",
)
except: except:
traceback.print_exc() traceback.print_exc()
@commands.Cog.listener() @commands.Cog.listener()
async def on_message(self, message: discord.Message) -> None: async def on_message(self, message: discord.Message) -> None:
""" """
@ -196,21 +212,26 @@ class Karma(commands.Cog):
return return
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
if message.channel.id == self.karma_chanid and not message.author.id == self.bot.user.id: if (
message.channel.id == self.karma_chanid
and not message.author.id == self.bot.user.id
):
"""Message to #karma not by Havoc, delete it""" """Message to #karma not by Havoc, delete it"""
await message.delete(reason="Messages to #karma are not allowed") await message.delete(reason="Messages to #karma are not allowed")
removal_embed: discord.Embed = discord.Embed( removal_embed: discord.Embed = discord.Embed(
title="Message Deleted", 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." description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed.",
) )
await message.author.send(embed=removal_embed) await message.author.send(embed=removal_embed)
if message.author.id == self.bot.user.id: # Bots own message if message.author.id == self.bot.user.id: # Bots own message
return return
if not message.guild: if not message.guild:
return return
if not message.guild.id in [1145182936002482196, 1228740575235149855]: # Not a valid guild for cmd if not message.guild.id in [
1145182936002482196,
1228740575235149855,
]: # Not a valid guild for cmd
return return
message_content: str = message.content.strip() message_content: str = message.content.strip()
@ -235,7 +256,9 @@ class Karma(commands.Cog):
message_content = discord.utils.escape_markdown(message_content) message_content = discord.utils.escape_markdown(message_content)
karma_regex: list[str] = regex.findall(self.karma_regex, message_content.strip()) karma_regex: list[str] = regex.findall(
self.karma_regex, message_content.strip()
)
if not karma_regex: # Not a request to adjust karma if not karma_regex: # Not a request to adjust karma
return return
@ -274,8 +297,9 @@ class Karma(commands.Cog):
self.util.timers[message.author.id] = now self.util.timers[message.author.id] = now
updated: bool = await self.util.update_karma(message.author.display_name, updated: bool = await self.util.update_karma(
message.author.id, keyword, flag) message.author.display_name, message.author.id, keyword, flag
)
if updated: if updated:
return await message.add_reaction(emoji="👍") return await message.add_reaction(emoji="👍")
@ -299,7 +323,9 @@ class Karma(commands.Cog):
guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id) guild: Optional[discord.Guild] = self.bot.get_guild(ctx.guild.id)
if not guild: if not guild:
return return
guild_member: Optional[discord.Member] = guild.get_member(mentioned_uid) guild_member: Optional[discord.Member] = guild.get_member(
mentioned_uid
)
if not guild_member: if not guild_member:
return return
display = guild_member.display_name display = guild_member.display_name
@ -310,8 +336,9 @@ class Karma(commands.Cog):
score: int = await self.util.get_karma(keyword) score: int = await self.util.get_karma(keyword)
description: str = f"**{keyword}** has a karma of *{score}*" description: str = f"**{keyword}** has a karma of *{score}*"
embed: discord.Embed = discord.Embed(title=f"Karma for {keyword}", embed: discord.Embed = discord.Embed(
description=description) title=f"Karma for {keyword}", description=description
)
return await ctx.respond(embed=embed) return await ctx.respond(embed=embed)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")

View File

@ -5,8 +5,10 @@ from discord.ext import bridge, commands
from util.lovehate_db import DB from util.lovehate_db import DB
from disc_havoc import Havoc from disc_havoc import Havoc
class LoveHate(commands.Cog): class LoveHate(commands.Cog):
"""LoveHate Cog for Havoc""" """LoveHate Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.db = DB(self.bot) self.db = DB(self.bot)
@ -21,9 +23,8 @@ class LoveHate(commands.Cog):
str str
""" """
if len(items) > 1: if len(items) > 1:
return ', '.join(items[:-1]) + ' and ' + items[-1] return ", ".join(items[:-1]) + " and " + items[-1]
return items[0] if items else '' return items[0] if items else ""
@bridge.bridge_command() @bridge.bridge_command()
async def loves(self, ctx, user: Optional[str] = None) -> None: async def loves(self, ctx, user: Optional[str] = None) -> None:
@ -33,8 +34,9 @@ class LoveHate(commands.Cog):
try: try:
if not user: if not user:
display_name = ctx.author.display_name display_name = ctx.author.display_name
loves: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name, loves: Union[list[tuple], bool] = await self.db.get_lovehates(
loves=True) user=display_name, loves=True
)
if not loves: if not loves:
return await ctx.respond("You don't seem to love anything...") return await ctx.respond("You don't seem to love anything...")
@ -83,8 +85,9 @@ class LoveHate(commands.Cog):
if not _thing: if not _thing:
return return
who_loves: Union[list, bool] = await self.db.get_wholovehates(thing=_thing, who_loves: Union[list, bool] = await self.db.get_wholovehates(
loves=True) thing=_thing, loves=True
)
if not isinstance(who_loves, list): if not isinstance(who_loves, list):
return await ctx.respond(f"I couldn't find anyone who loves {thing}...") return await ctx.respond(f"I couldn't find anyone who loves {thing}...")
@ -123,8 +126,9 @@ class LoveHate(commands.Cog):
return return
_thing = thing_member.display_name _thing = thing_member.display_name
who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(thing=_thing, who_hates: Union[list[tuple], bool] = await self.db.get_wholovehates(
hates=True) thing=_thing, hates=True
)
if not who_hates: if not who_hates:
return await ctx.respond(f"I couldn't find anyone who hates {thing}...") return await ctx.respond(f"I couldn't find anyone who hates {thing}...")
@ -144,15 +148,13 @@ class LoveHate(commands.Cog):
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"Error: {str(e)}") return await ctx.respond(f"Error: {str(e)}")
@bridge.bridge_command() @bridge.bridge_command()
async def dontcare(self, ctx, thing: str) -> None: async def dontcare(self, ctx, thing: str) -> None:
""" """
Make me forget your opinion on <thing> Make me forget your opinion on <thing>
""" """
try: try:
stop_caring: str = await self.db.update(ctx.author.display_name, stop_caring: str = await self.db.update(ctx.author.display_name, thing, 0)
thing, 0)
return await ctx.respond(stop_caring) return await ctx.respond(stop_caring)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
@ -166,8 +168,9 @@ class LoveHate(commands.Cog):
try: try:
if not user: if not user:
display_name = ctx.author.display_name display_name = ctx.author.display_name
hates: Union[list[tuple], bool] = await self.db.get_lovehates(user=display_name, hates: Union[list[tuple], bool] = await self.db.get_lovehates(
hates=True) user=display_name, hates=True
)
if not hates: if not hates:
return await ctx.respond("You don't seem to hate anything...") return await ctx.respond("You don't seem to hate anything...")
else: else:
@ -188,7 +191,7 @@ class LoveHate(commands.Cog):
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
@bridge.bridge_command(aliases=['sarcastichate']) @bridge.bridge_command(aliases=["sarcastichate"])
async def love(self, ctx, *, thing: str) -> None: async def love(self, ctx, *, thing: str) -> None:
""" """
Love <thing> Love <thing>
@ -205,14 +208,13 @@ class LoveHate(commands.Cog):
return return
thing = thing_member.display_name thing = thing_member.display_name
love: str = await self.db.update(ctx.author.display_name, love: str = await self.db.update(ctx.author.display_name, thing, 1)
thing, 1)
return await ctx.respond(love) return await ctx.respond(love)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")
traceback.print_exc() traceback.print_exc()
@bridge.bridge_command(aliases=['sarcasticlove']) @bridge.bridge_command(aliases=["sarcasticlove"])
async def hate(self, ctx, *, thing: str) -> None: async def hate(self, ctx, *, thing: str) -> None:
""" """
Hate <thing> Hate <thing>
@ -228,8 +230,7 @@ class LoveHate(commands.Cog):
if not thing_member: if not thing_member:
return return
thing = thing_member.display_name thing = thing_member.display_name
hate: str = await self.db.update(ctx.author.display_name, hate: str = await self.db.update(ctx.author.display_name, thing, -1)
thing, -1)
return await ctx.respond(hate) return await ctx.respond(hate)
except Exception as e: except Exception as e:
await ctx.respond(f"Error: {str(e)}") await ctx.respond(f"Error: {str(e)}")

View File

@ -4,10 +4,7 @@ import json
import io import io
import asyncio import asyncio
import random import random
from typing import (LiteralString, from typing import LiteralString, Optional, Any, Union
Optional,
Any,
Union)
import aiosqlite as sqlite3 import aiosqlite as sqlite3
import logging import logging
import textwrap import textwrap
@ -35,50 +32,66 @@ BOT_CHANIDS = []
TODO: Cleanup new meme leaderboard stuff TODO: Cleanup new meme leaderboard stuff
""" """
class Helper: class Helper:
"""Meme Helper""" """Meme Helper"""
def load_meme_choices(self) -> None: def load_meme_choices(self) -> None:
"""Load Available Meme Templates from JSON File""" """Load Available Meme Templates from JSON File"""
global meme_choices global meme_choices
memes_file: str|LiteralString = os.path.join(os.path.dirname(__file__), "memes.json") memes_file: str | LiteralString = os.path.join(
with open(memes_file, 'r', encoding='utf-8') as f: os.path.dirname(__file__), "memes.json"
)
with open(memes_file, "r", encoding="utf-8") as f:
meme_choices = json.loads(f.read()) meme_choices = json.loads(f.read())
class MemeView(discord.ui.View): class MemeView(discord.ui.View):
"""Meme Selection discord.ui.View""" """Meme Selection discord.ui.View"""
helper = Helper() helper = Helper()
helper.load_meme_choices() helper.load_meme_choices()
@discord.ui.select( @discord.ui.select(
placeholder = "Choose a Meme!", placeholder="Choose a Meme!",
min_values = 1, min_values=1,
max_values = 1, max_values=1,
options = [ options=[
discord.SelectOption( discord.SelectOption(label=meme_label) for meme_label in meme_choices[0:24]
label=meme_label ],
) for meme_label in meme_choices[0:24]
]
) )
async def select_callback(self, select: discord.ui.Select, async def select_callback(
interaction: discord.Interaction) -> None: self, select: discord.ui.Select, interaction: discord.Interaction
) -> None:
"""Meme Selection Callback""" """Meme Selection Callback"""
if not isinstance(select.values[0], str): if not isinstance(select.values[0], str):
return return
modal: discord.ui.Modal = MemeModal(meme=select.values[0], title="Meme Selected") modal: discord.ui.Modal = MemeModal(
meme=select.values[0], title="Meme Selected"
)
await interaction.response.send_modal(modal) await interaction.response.send_modal(modal)
class MemeModal(discord.ui.Modal): class MemeModal(discord.ui.Modal):
"""Meme Creation discord.ui.Modal""" """Meme Creation discord.ui.Modal"""
def __init__(self, *args, meme: Optional[str] = None, **kwargs) -> None: def __init__(self, *args, meme: Optional[str] = None, **kwargs) -> None:
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.selected_meme: Optional[str] = meme self.selected_meme: Optional[str] = meme
self.meme_generator = JesusMemeGenerator() self.meme_generator = JesusMemeGenerator()
self.TEXT_LIMIT: int = 80 self.TEXT_LIMIT: int = 80
self.add_item(discord.ui.InputText(label="Top Text", self.add_item(
style=discord.InputTextStyle.singleline)) discord.ui.InputText(
self.add_item(discord.ui.InputText(label="Bottom Text", label="Top Text", style=discord.InputTextStyle.singleline
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: async def callback(self, interaction: discord.Interaction) -> None:
if not self.selected_meme: # No meme selected if not self.selected_meme: # No meme selected
@ -86,17 +99,24 @@ class MemeModal(discord.ui.Modal):
selected_meme: str = self.selected_meme selected_meme: str = self.selected_meme
if not self.children or len(self.children) < 2: # Invalid request if not self.children or len(self.children) < 2: # Invalid request
return return
if not isinstance(self.children[0].value, str)\ if not isinstance(self.children[0].value, str) or not isinstance(
or not isinstance(self.children[1].value, str): # Invalid request self.children[1].value, str
): # Invalid request
return return
meme_top_line: str = self.children[0].value.strip() meme_top_line: str = self.children[0].value.strip()
meme_bottom_line: str = self.children[1].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: if (
await interaction.response.send_message("ERR: Text is limited to 80 characters for each the top and bottom lines.") 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 return
meme_link: Optional[str] = await self.meme_generator.create_meme(top_line=meme_top_line, meme_link: Optional[str] = await self.meme_generator.create_meme(
bottom_line=meme_bottom_line, meme=selected_meme) top_line=meme_top_line, bottom_line=meme_bottom_line, meme=selected_meme
)
if not meme_link: if not meme_link:
await interaction.response.send_message("Failed!") await interaction.response.send_message("Failed!")
return return
@ -106,43 +126,45 @@ class MemeModal(discord.ui.Modal):
await interaction.response.send_message(embeds=[embed]) await interaction.response.send_message(embeds=[embed])
return return
class Meme(commands.Cog): class Meme(commands.Cog):
"""Meme Cog for Havoc""" """Meme Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.stats_db_path: LiteralString = os.path.join("/usr/local/share", self.stats_db_path: LiteralString = os.path.join(
"sqlite_dbs", "stats.db") "/usr/local/share", "sqlite_dbs", "stats.db"
)
self.meme_choices: list = [] self.meme_choices: list = []
self.meme_counter: int = 0 self.meme_counter: int = 0
self.THREADS: dict[str, dict[int, list]] = { self.THREADS: dict[str, dict[int, list]] = {
# Format: Guild1: [ChanId : [Webhook, ThreadId], Guild2: [ChanId : [Webhook, ThreadId] # Format: Guild1: [ChanId : [Webhook, ThreadId], Guild2: [ChanId : [Webhook, ThreadId]
'comic_explosm': { "comic_explosm": {
1298729744216359055: [constants.EXPLOSM_WEBHOOK, 1299165855493390367], 1298729744216359055: [constants.EXPLOSM_WEBHOOK, 1299165855493390367],
1306414795049926676: [constants.EXPLOSM_WEBHOOK2, 1306416492304138364], 1306414795049926676: [constants.EXPLOSM_WEBHOOK2, 1306416492304138364],
}, },
'comic_xkcd': { "comic_xkcd": {
1298729744216359055: [constants.XKCD_WEBHOOK, 1299165928755433483], 1298729744216359055: [constants.XKCD_WEBHOOK, 1299165928755433483],
1306414795049926676: [constants.XKCD_WEBHOOK2, 1306416681991798854], 1306414795049926676: [constants.XKCD_WEBHOOK2, 1306416681991798854],
}, },
'comic_smbc': { "comic_smbc": {
1298729744216359055: [constants.SMBC_WEBHOOK, 1299166071038808104], 1298729744216359055: [constants.SMBC_WEBHOOK, 1299166071038808104],
1306414795049926676: [constants.SMBC_WEBHOOK2, 1306416842511745024], 1306414795049926676: [constants.SMBC_WEBHOOK2, 1306416842511745024],
}, },
'comic_qc': { "comic_qc": {
1298729744216359055: [constants.QC_WEBHOOK, 1299392115364593674], 1298729744216359055: [constants.QC_WEBHOOK, 1299392115364593674],
1306414795049926676: [constants.QC_WEBHOOK2, 1306417084774744114], 1306414795049926676: [constants.QC_WEBHOOK2, 1306417084774744114],
}, },
'comic_dino': { "comic_dino": {
1298729744216359055: [constants.DINO_WEBHOOK, 1299771918886506557], 1298729744216359055: [constants.DINO_WEBHOOK, 1299771918886506557],
1306414795049926676: [constants.DINO_WEBHOOK2, 1306417286713704548], 1306414795049926676: [constants.DINO_WEBHOOK2, 1306417286713704548],
} },
} }
self.NO_THREAD_WEBHOOKS: dict[str, list] = { self.NO_THREAD_WEBHOOKS: dict[str, list] = {
'theonion': [constants.ONION_WEBHOOK, constants.ONION_WEBHOOK2], "theonion": [constants.ONION_WEBHOOK, constants.ONION_WEBHOOK2],
'thn': [constants.THN_WEBHOOK], "thn": [constants.THN_WEBHOOK],
'memes': [constants.MEME_WEBHOOK1, constants.MEME_WEBHOOK2], "memes": [constants.MEME_WEBHOOK1, constants.MEME_WEBHOOK2],
} }
global BOT_CHANIDS global BOT_CHANIDS
@ -155,6 +177,7 @@ class Meme(commands.Cog):
def is_spamchan() -> bool: # type: ignore def is_spamchan() -> bool: # type: ignore
"""Check if channel is spamchan""" """Check if channel is spamchan"""
def predicate(ctx): def predicate(ctx):
try: try:
if not ctx.channel.id in BOT_CHANIDS: if not ctx.channel.id in BOT_CHANIDS:
@ -163,10 +186,10 @@ class Meme(commands.Cog):
except: except:
traceback.print_exc() traceback.print_exc()
return False return False
return commands.check(predicate) # type: ignore return commands.check(predicate) # type: ignore
async def leaderboard_increment(self, async def leaderboard_increment(self, uid: int) -> None:
uid: int) -> None:
""" """
Increment leaderboard for uid Increment leaderboard for uid
Args: Args:
@ -175,7 +198,6 @@ class Meme(commands.Cog):
None None
""" """
if not uid in self.meme_leaderboard: if not uid in self.meme_leaderboard:
self.meme_leaderboard[uid] = 1 self.meme_leaderboard[uid] = 1
else: else:
@ -199,23 +221,23 @@ class Meme(commands.Cog):
try: try:
await self.update_meme_lb() await self.update_meme_lb()
except Exception as e: except Exception as e:
logging.info("Failed to update meme leaderboard following increment: %s", logging.info(
str(e)) "Failed to update meme leaderboard following increment: %s", str(e)
)
async def init_meme_leaderboard(self) -> None: async def init_meme_leaderboard(self) -> None:
""" """
INIT MEME LEADERBOARD INIT MEME LEADERBOARD
""" """
self.meme_leaderboard: dict [int, int] = {} self.meme_leaderboard: dict[int, int] = {}
async with sqlite3.connect(self.stats_db_path, timeout=2) as db_conn: async with sqlite3.connect(self.stats_db_path, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row db_conn.row_factory = sqlite3.Row
db_query: str = "SELECT discord_uid, count FROM memes WHERE count > 0" db_query: str = "SELECT discord_uid, count FROM memes WHERE count > 0"
async with db_conn.execute(db_query) as db_cursor: async with db_conn.execute(db_query) as db_cursor:
results = await db_cursor.fetchall() results = await db_cursor.fetchall()
for result in results: for result in results:
uid = result['discord_uid'] uid = result["discord_uid"]
count = result['count'] count = result["count"]
self.meme_leaderboard[uid] = count self.meme_leaderboard[uid] = count
@commands.Cog.listener() @commands.Cog.listener()
@ -284,28 +306,34 @@ class Meme(commands.Cog):
except: except:
traceback.print_exc() traceback.print_exc()
agents: list[str] = constants.HTTP_UA_LIST agents: list[str] = constants.HTTP_UA_LIST
headers: dict = { headers: dict = {"User-Agent": random.choice(agents)}
'User-Agent': random.choice(agents)
}
if not only_comics: if not only_comics:
try: try:
for meme in memes: for meme in memes:
if not meme: if not meme:
continue continue
(meme_id, meme_title, meme_url) = meme (meme_id, meme_title, meme_url) = meme
request = requests.get(meme_url, stream=True, timeout=(5, 30), headers=headers) request = requests.get(
meme_url, stream=True, timeout=(5, 30), headers=headers
)
if not request.status_code == 200: if not request.status_code == 200:
continue continue
meme_content: bytes = request.raw.read() meme_content: bytes = request.raw.read()
for meme_hook in self.NO_THREAD_WEBHOOKS.get('memes', {}): for meme_hook in self.NO_THREAD_WEBHOOKS.get("memes", {}):
meme_image: io.BytesIO = io.BytesIO(meme_content) meme_image: io.BytesIO = io.BytesIO(meme_content)
ext: str = meme_url.split(".")[-1]\ ext: str = (
.split("?")[0].split("&")[0] meme_url.split(".")[-1].split("?")[0].split("&")[0]
)
async with ClientSession() as session: async with ClientSession() as session:
webhook: discord.Webhook = discord.Webhook.from_url(meme_hook, webhook: discord.Webhook = discord.Webhook.from_url(
session=session) meme_hook, session=session
await webhook.send(file=discord.File(meme_image, )
filename=f'img.{ext}'), username="r/memes") await webhook.send(
file=discord.File(
meme_image, filename=f"img.{ext}"
),
username="r/memes",
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
@ -315,25 +343,33 @@ class Meme(commands.Cog):
continue continue
(comic_title, comic_url) = comic (comic_title, comic_url) = comic
comic_title = discord.utils.escape_markdown(comic_title) comic_title = discord.utils.escape_markdown(comic_title)
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) comic_request = requests.get(
comic_url, stream=True, timeout=(5, 20), headers=headers
)
comic_request.raise_for_status() comic_request.raise_for_status()
comic_content: bytes = comic_request.raw.read() comic_content: bytes = comic_request.raw.read()
ext = comic_url.split(".")[-1]\ ext = comic_url.split(".")[-1].split("?")[0].split("&")[0]
.split("?")[0].split("&")[0]
async with ClientSession() as session: async with ClientSession() as session:
for chanid, _hook in self.THREADS.get('comic_explosm', {}).items(): for chanid, _hook in self.THREADS.get(
"comic_explosm", {}
).items():
comic_image: io.BytesIO = io.BytesIO(comic_content) comic_image: io.BytesIO = io.BytesIO(comic_content)
channel: int = chanid channel: int = chanid
(hook_uri, thread_id) = _hook (hook_uri, thread_id) = _hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
_channel: Any = self.bot.get_channel(channel) _channel: Any = self.bot.get_channel(channel)
if not _channel: if not _channel:
return return
thread = _channel.get_thread(thread_id) thread = _channel.get_thread(thread_id)
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), await webhook.send(
username="Cyanide & Happiness", thread=thread) f"**{comic_title}**",
file=discord.File(comic_image, filename=f"img.{ext}"),
username="Cyanide & Happiness",
thread=thread,
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
@ -343,26 +379,32 @@ class Meme(commands.Cog):
continue continue
(comic_title, comic_url) = comic (comic_title, comic_url) = comic
comic_title = discord.utils.escape_markdown(comic_title) comic_title = discord.utils.escape_markdown(comic_title)
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) comic_request = requests.get(
comic_url, stream=True, timeout=(5, 20), headers=headers
)
comic_request.raise_for_status() comic_request.raise_for_status()
comic_content = comic_request.raw.read() comic_content = comic_request.raw.read()
comic_image = io.BytesIO(comic_request.raw.read()) comic_image = io.BytesIO(comic_request.raw.read())
ext = comic_url.split(".")[-1]\ ext = comic_url.split(".")[-1].split("?")[0].split("&")[0]
.split("?")[0].split("&")[0]
async with ClientSession() as session: async with ClientSession() as session:
for chanid, _hook in self.THREADS.get('comic_xkcd', {}).items(): for chanid, _hook in self.THREADS.get("comic_xkcd", {}).items():
comic_image = io.BytesIO(comic_content) comic_image = io.BytesIO(comic_content)
channel = chanid channel = chanid
(hook_uri, thread_id) = _hook (hook_uri, thread_id) = _hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
_channel = self.bot.get_channel(channel) _channel = self.bot.get_channel(channel)
if not _channel: if not _channel:
return return
thread = _channel.get_thread(thread_id) thread = _channel.get_thread(thread_id)
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), await webhook.send(
username="xkcd", thread=thread) f"**{comic_title}**",
file=discord.File(comic_image, filename=f"img.{ext}"),
username="xkcd",
thread=thread,
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
@ -372,25 +414,31 @@ class Meme(commands.Cog):
continue continue
(comic_title, comic_url) = comic (comic_title, comic_url) = comic
comic_title = discord.utils.escape_markdown(comic_title) comic_title = discord.utils.escape_markdown(comic_title)
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) comic_request = requests.get(
comic_url, stream=True, timeout=(5, 20), headers=headers
)
comic_request.raise_for_status() comic_request.raise_for_status()
comic_content = comic_request.raw.read() comic_content = comic_request.raw.read()
ext = comic_url.split(".")[-1]\ ext = comic_url.split(".")[-1].split("?")[0].split("&")[0]
.split("?")[0].split("&")[0]
async with ClientSession() as session: async with ClientSession() as session:
for chanid, _hook in self.THREADS.get('comic_smbc', {}).items(): for chanid, _hook in self.THREADS.get("comic_smbc", {}).items():
comic_image = io.BytesIO(comic_content) comic_image = io.BytesIO(comic_content)
channel = chanid channel = chanid
(hook_uri, thread_id) = _hook (hook_uri, thread_id) = _hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
_channel = self.bot.get_channel(channel) _channel = self.bot.get_channel(channel)
if not _channel: if not _channel:
return return
thread = _channel.get_thread(thread_id) thread = _channel.get_thread(thread_id)
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), await webhook.send(
username="SMBC", thread=thread) f"**{comic_title}**",
file=discord.File(comic_image, filename=f"img.{ext}"),
username="SMBC",
thread=thread,
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
@ -401,30 +449,33 @@ class Meme(commands.Cog):
continue continue
(comic_title, comic_url) = comic (comic_title, comic_url) = comic
comic_title = discord.utils.escape_markdown(comic_title) comic_title = discord.utils.escape_markdown(comic_title)
comic_url = regex.sub(r'^http://ww\.', 'http://www.', comic_url = regex.sub(r"^http://ww\.", "http://www.", comic_url)
comic_url) comic_url = regex.sub(r"\.pmg$", ".png", comic_url)
comic_url = regex.sub(r'\.pmg$', '.png', comic_request = requests.get(
comic_url) comic_url, stream=True, timeout=(5, 20), headers=headers
comic_request = requests.get(comic_url, stream=True, )
timeout=(5, 20), headers=headers)
comic_request.raise_for_status() comic_request.raise_for_status()
comic_content = comic_request.raw.read() comic_content = comic_request.raw.read()
ext = comic_url.split(".")[-1]\ ext = comic_url.split(".")[-1].split("?")[0].split("&")[0]
.split("?")[0].split("&")[0]
async with ClientSession() as session: async with ClientSession() as session:
for chanid, _hook in self.THREADS.get('comic_qc', {}).items(): for chanid, _hook in self.THREADS.get("comic_qc", {}).items():
comic_image = io.BytesIO(comic_content) comic_image = io.BytesIO(comic_content)
channel = chanid channel = chanid
(hook_uri, thread_id) = _hook (hook_uri, thread_id) = _hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
_channel = self.bot.get_channel(channel) _channel = self.bot.get_channel(channel)
if not _channel: if not _channel:
return return
thread = _channel.get_thread(thread_id) thread = _channel.get_thread(thread_id)
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), await webhook.send(
username="Questionable Content", thread=thread) f"**{comic_title}**",
file=discord.File(comic_image, filename=f"img.{ext}"),
username="Questionable Content",
thread=thread,
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
traceback.print_exc() traceback.print_exc()
@ -435,25 +486,31 @@ class Meme(commands.Cog):
continue continue
(comic_title, comic_url) = comic (comic_title, comic_url) = comic
comic_title = discord.utils.escape_markdown(comic_title) comic_title = discord.utils.escape_markdown(comic_title)
comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) comic_request = requests.get(
comic_url, stream=True, timeout=(5, 20), headers=headers
)
comic_request.raise_for_status() comic_request.raise_for_status()
comic_content = comic_request.raw.read() comic_content = comic_request.raw.read()
ext = comic_url.split(".")[-1]\ ext = comic_url.split(".")[-1].split("?")[0].split("&")[0]
.split("?")[0].split("&")[0]
async with ClientSession() as session: async with ClientSession() as session:
for chanid, _hook in self.THREADS.get('comic_dino', {}).items(): for chanid, _hook in self.THREADS.get("comic_dino", {}).items():
comic_image = io.BytesIO(comic_content) comic_image = io.BytesIO(comic_content)
channel = chanid channel = chanid
(hook_uri, thread_id) = _hook (hook_uri, thread_id) = _hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
_channel = self.bot.get_channel(channel) _channel = self.bot.get_channel(channel)
if not _channel: if not _channel:
return return
thread = _channel.get_thread(thread_id) thread = _channel.get_thread(thread_id)
await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), await webhook.send(
username="Dinosaur Comics", thread=thread) f"**{comic_title}**",
file=discord.File(comic_image, filename=f"img.{ext}"),
username="Dinosaur Comics",
thread=thread,
)
await asyncio.sleep(2) await asyncio.sleep(2)
except: except:
pass pass
@ -462,15 +519,20 @@ class Meme(commands.Cog):
if not onion: if not onion:
continue continue
(onion_title, onion_description, onion_link, onion_video) = onion (onion_title, onion_description, onion_link, onion_video) = onion
onion_description = textwrap.wrap(text=onion_description, onion_description = textwrap.wrap(
width=860, max_lines=1)[0] text=onion_description, width=860, max_lines=1
)[0]
embed: discord.Embed = discord.Embed(title=onion_title) embed: discord.Embed = discord.Embed(title=onion_title)
embed.add_field(name="Content", value=f"{onion_description[0:960]}\n-# {onion_link}") embed.add_field(
name="Content",
value=f"{onion_description[0:960]}\n-# {onion_link}",
)
async with ClientSession() as session: async with ClientSession() as session:
for hook in self.NO_THREAD_WEBHOOKS.get('theonion', {}): for hook in self.NO_THREAD_WEBHOOKS.get("theonion", {}):
hook_uri = hook hook_uri = hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
await webhook.send(embed=embed, username="The Onion") await webhook.send(embed=embed, username="The Onion")
if onion_video: if onion_video:
await webhook.send(f"^ video: {onion_video}") await webhook.send(f"^ video: {onion_video}")
@ -483,16 +545,20 @@ class Meme(commands.Cog):
if not thn: if not thn:
continue continue
(thn_title, thn_description, thn_link, thn_pubdate, thn_video) = thn (thn_title, thn_description, thn_link, thn_pubdate, thn_video) = thn
thn_description = textwrap.wrap(text=thn_description, thn_description = textwrap.wrap(
width=860, max_lines=1)[0] text=thn_description, width=860, max_lines=1
)[0]
embed = discord.Embed(title=thn_title) embed = discord.Embed(title=thn_title)
embed.add_field(name="Content", value=f"{thn_description[0:960]}\n-# {thn_link}") embed.add_field(
name="Content", value=f"{thn_description[0:960]}\n-# {thn_link}"
)
embed.add_field(name="Published", value=thn_pubdate, inline=False) embed.add_field(name="Published", value=thn_pubdate, inline=False)
async with ClientSession() as session: async with ClientSession() as session:
for hook in self.NO_THREAD_WEBHOOKS.get('thn', {}): for hook in self.NO_THREAD_WEBHOOKS.get("thn", {}):
hook_uri = hook hook_uri = hook
webhook = discord.Webhook.from_url(hook_uri, webhook = discord.Webhook.from_url(
session=session) hook_uri, session=session
)
await webhook.send(embed=embed, username="The Hacker News") await webhook.send(embed=embed, username="The Hacker News")
if thn_video: if thn_video:
await webhook.send(f"^ video: {thn_video}") await webhook.send(f"^ video: {thn_video}")
@ -564,13 +630,17 @@ class Meme(commands.Cog):
return return
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
if message.channel.id == lb_chanid\ if (
and not message.author.id == self.bot.user.id: message.channel.id == lb_chanid
and not message.author.id == self.bot.user.id
):
"""Message to #memes-top-10 not by Havoc, delete it""" """Message to #memes-top-10 not by Havoc, delete it"""
await message.delete(reason=f"Messages to #{message.channel.name} are not allowed") await message.delete(
reason=f"Messages to #{message.channel.name} are not allowed"
)
removal_embed: discord.Embed = discord.Embed( removal_embed: discord.Embed = discord.Embed(
title="Message Deleted", 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." description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed.",
) )
await message.author.send(embed=removal_embed) await message.author.send(embed=removal_embed)
@ -598,12 +668,14 @@ class Meme(commands.Cog):
out_top: list[tuple[int, int]] = [] out_top: list[tuple[int, int]] = []
async with sqlite3.connect(self.stats_db_path, timeout=2) as db_conn: async with sqlite3.connect(self.stats_db_path, timeout=2) as db_conn:
db_conn.row_factory = sqlite3.Row db_conn.row_factory = sqlite3.Row
query: str = "SELECT discord_uid, count FROM memes WHERE count > 0 ORDER BY count DESC" query: str = (
"SELECT discord_uid, count FROM memes WHERE count > 0 ORDER BY count DESC"
)
async with db_conn.execute(query) as db_cursor: async with db_conn.execute(query) as db_cursor:
db_result = await db_cursor.fetchall() db_result = await db_cursor.fetchall()
for res in db_result: for res in db_result:
uid = res['discord_uid'] uid = res["discord_uid"]
count = res['count'] count = res["count"]
out_top.append((uid, count)) out_top.append((uid, count))
# Check for and remove missing members # Check for and remove missing members
guild_id: int = 1145182936002482196 guild_id: int = 1145182936002482196
@ -615,12 +687,12 @@ class Meme(commands.Cog):
member: Optional[discord.Member] = guild.get_member(uid) member: Optional[discord.Member] = guild.get_member(uid)
if not member: if not member:
out_top.pop(x) out_top.pop(x)
return out_top[0:(n+1)] return out_top[0 : (n + 1)]
except: except:
traceback.print_exc() traceback.print_exc()
return None return None
async def get_top_embed(self, n:int = 10) -> Optional[discord.Embed]: async def get_top_embed(self, n: int = 10) -> Optional[discord.Embed]:
""" """
Get Top Memes Embed Get Top Memes Embed
@ -643,11 +715,13 @@ class Meme(commands.Cog):
if not member: if not member:
continue continue
display_name: str = member.display_name display_name: str = member.display_name
top_formatted += f"{x+1}. **{discord.utils.escape_markdown(display_name)}**: *{count}*\n" top_formatted += (
f"{x+1}. **{discord.utils.escape_markdown(display_name)}**: *{count}*\n"
)
top_formatted = top_formatted.strip() top_formatted = top_formatted.strip()
embed: discord.Embed = discord.Embed(title=f"Top {n} Memes", embed: discord.Embed = discord.Embed(
description=top_formatted, title=f"Top {n} Memes", description=top_formatted, colour=0x25BD6B
colour=0x25bd6b) )
return embed return embed
@tasks.loop(seconds=30, reconnect=True) @tasks.loop(seconds=30, reconnect=True)
@ -661,8 +735,10 @@ class Meme(commands.Cog):
if not isinstance(channel, discord.TextChannel): if not isinstance(channel, discord.TextChannel):
return return
message_to_edit = await channel.fetch_message(message_id) message_to_edit = await channel.fetch_message(message_id)
await message_to_edit.edit(embed=top_embed, await message_to_edit.edit(
content="## This message will automatically update periodically.") embed=top_embed,
content="## This message will automatically update periodically.",
)
except: except:
traceback.print_exc() traceback.print_exc()
@ -671,17 +747,21 @@ class Meme(commands.Cog):
async def doembed(self, ctx) -> None: async def doembed(self, ctx) -> None:
"""Do Meme Embed""" """Do Meme Embed"""
meme_lb_chan_id: int = 1352373745108652145 meme_lb_chan_id: int = 1352373745108652145
meme_lb_chan: Union[discord.TextChannel, Any] = self.bot.get_channel(meme_lb_chan_id) meme_lb_chan: Union[discord.TextChannel, Any] = self.bot.get_channel(
meme_lb_chan_id
)
embed = await self.get_top_embed() embed = await self.get_top_embed()
if embed: if embed:
await meme_lb_chan.send(embed=embed) await meme_lb_chan.send(embed=embed)
else: else:
await ctx.respond("NO embed :(") await ctx.respond("NO embed :(")
def cog_unload(self) -> None: def cog_unload(self) -> None:
self.meme_stream_loop.cancel() self.meme_stream_loop.cancel()
self.explosm_loop.cancel() self.explosm_loop.cancel()
self.update_meme_lb.cancel() self.update_meme_lb.cancel()
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Meme(bot)) bot.add_cog(Meme(bot))

File diff suppressed because it is too large Load Diff

View File

@ -9,20 +9,24 @@ from discord.ext import bridge, commands
from disc_havoc import Havoc from disc_havoc import Havoc
import util import util
class Owner(commands.Cog): class Owner(commands.Cog):
"""Owner Cog for Havoc""" """Owner Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.former_roles_store: dict[int, list[discord.Role]] = {} self.former_roles_store: dict[int, list[discord.Role]] = {}
self._temperature: int = random.randrange(20, 30) self._temperature: int = random.randrange(20, 30)
@bridge.bridge_command(guild_ids=[1145182936002482196]) @bridge.bridge_command(guild_ids=[1145182936002482196])
async def temperature(self, ctx, temp: Optional[int|str] = None) -> None: async def temperature(self, ctx, temp: Optional[int | str] = None) -> None:
""" """
Set Temperature Set Temperature
""" """
if not temp: if not temp:
return await ctx.respond(f"The current temperature is: {self._temperature} °C") return await ctx.respond(
f"The current temperature is: {self._temperature} °C"
)
if not self.bot.is_owner(ctx.author): if not self.bot.is_owner(ctx.author):
return await ctx.respond("I am afraid I can't let you do that.") return await ctx.respond("I am afraid I can't let you do that.")
@ -35,15 +39,13 @@ class Owner(commands.Cog):
elif _temperature > 35: elif _temperature > 35:
return await ctx.respond("Too hot! (35°C maximum)") return await ctx.respond("Too hot! (35°C maximum)")
self._temperature = _temperature self._temperature = _temperature
return await ctx.respond(f"As per your request, I have adjusted the temperature to {_temperature} °C.") return await ctx.respond(
f"As per your request, I have adjusted the temperature to {_temperature} °C."
)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def editmsg(self, ctx, async def editmsg(self, ctx, msgid: str, *, newcontent: str) -> None:
msgid: str,
*,
newcontent: str
) -> None:
""" """
Edit a message previously sent by the bot Edit a message previously sent by the bot
""" """
@ -51,14 +53,14 @@ class Owner(commands.Cog):
try: try:
message: Optional[discord.Message] = self.bot.get_message(int(msgid)) message: Optional[discord.Message] = self.bot.get_message(int(msgid))
if not message: if not message:
await ctx.respond(f"**Failed:** Message {msgid} not found.", await ctx.respond(
ephemeral=True) f"**Failed:** Message {msgid} not found.", ephemeral=True
)
return None return None
await message.edit(content=newcontent) await message.edit(content=newcontent)
await ctx.respond("**Done!**", ephemeral=True) await ctx.respond("**Done!**", ephemeral=True)
except Exception as e: except Exception as e:
await ctx.respond(f"**Failed:** {str(e)}", await ctx.respond(f"**Failed:** {str(e)}", ephemeral=True)
ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
@ -70,41 +72,43 @@ class Owner(commands.Cog):
self.bot.load_exts(False) self.bot.load_exts(False)
await ctx.respond("Reloaded!", ephemeral=True) await ctx.respond("Reloaded!", ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def say(self, ctx, *, async def say(self, ctx, *, parameters: str) -> None:
parameters: str) -> None:
""" """
Make me say something in a channel Make me say something in a channel
""" """
_parameters: list[str] = parameters.split(" ") _parameters: list[str] = parameters.split(" ")
if not len(_parameters) > 1: if not len(_parameters) > 1:
return await ctx.respond("**Error**: Incorrect command usage; required: <chan> <msg>", ephemeral=True) return await ctx.respond(
"**Error**: Incorrect command usage; required: <chan> <msg>",
ephemeral=True,
)
channel: str = _parameters[0] channel: str = _parameters[0]
channel_mentions: list[int] = discord.utils.raw_channel_mentions(channel) channel_mentions: list[int] = discord.utils.raw_channel_mentions(channel)
if channel_mentions: if channel_mentions:
channel = str(channel_mentions[0]) channel = str(channel_mentions[0])
msg: str = " ".join(_parameters[1:]) msg: str = " ".join(_parameters[1:])
await util.discord_helpers.send_message(self.bot, channel=channel, await util.discord_helpers.send_message(self.bot, channel=channel, message=msg)
message=msg)
return await ctx.respond("**Done.**", ephemeral=True) return await ctx.respond("**Done.**", ephemeral=True)
@bridge.bridge_command() @bridge.bridge_command()
@commands.is_owner() @commands.is_owner()
async def chgstatus(self, ctx, *, async def chgstatus(self, ctx, *, status: Optional[str] = None) -> None:
status: Optional[str] = None) -> None:
""" """
Change bots status Change bots status
""" """
if not status: if not status:
return await ctx.respond("ERR: No status provided to change to!", return await ctx.respond(
ephemeral=True) "ERR: No status provided to change to!", ephemeral=True
)
await self.bot.change_presence(status=discord.Status.online, await self.bot.change_presence(
activity=discord.CustomActivity(name=status.strip())) status=discord.Status.online,
activity=discord.CustomActivity(name=status.strip()),
)
await ctx.respond("Done!", ephemeral=True) await ctx.respond("Done!", ephemeral=True)
@commands.message_command(name="Remove Messages Starting Here") @commands.message_command(name="Remove Messages Starting Here")
@ -120,10 +124,12 @@ class Owner(commands.Cog):
None None
""" """
try: try:
await ctx.channel.purge(after=message, await ctx.channel.purge(
after=message,
bulk=True, bulk=True,
limit=900000, limit=900000,
reason=f"Purge initiated by {ctx.author.display_name}") reason=f"Purge initiated by {ctx.author.display_name}",
)
await message.delete(reason=f"Purge initiated by {ctx.author.display_name}") await message.delete(reason=f"Purge initiated by {ctx.author.display_name}")
await ctx.respond("**Done!**") await ctx.respond("**Done!**")
# Wait 3 seconds, then delete interaction # Wait 3 seconds, then delete interaction
@ -149,7 +155,9 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
memes_channel: discord.TextChannel = ctx.guild.get_channel(1147229098544988261) memes_channel: discord.TextChannel = ctx.guild.get_channel(
1147229098544988261
)
message_content: str = message.content message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
@ -157,14 +165,17 @@ class Owner(commands.Cog):
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
if not _file: if not _file:
return # No file to move return # No file to move
await memes_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...*\n**{message_author}:** {message_content}", file=_file) await memes_channel.send(
f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...*\n**{message_author}:** {message_content}",
file=_file,
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except:
@ -186,7 +197,9 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
drugs_channel: discord.TextChannel = ctx.guild.get_channel(1172247451047034910) drugs_channel: discord.TextChannel = ctx.guild.get_channel(
1172247451047034910
)
message_content: str = message.content message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
@ -194,15 +207,18 @@ class Owner(commands.Cog):
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
if not _file: if not _file:
return # No file to move return # No file to move
await drugs_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\ await drugs_channel.send(
*\n**{message_author}:** {message_content}", file=_file) f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\
*\n**{message_author}:** {message_content}",
file=_file,
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except:
@ -224,7 +240,9 @@ class Owner(commands.Cog):
try: try:
if not isinstance(message.channel, discord.TextChannel): if not isinstance(message.channel, discord.TextChannel):
return return
funhouse_channel: discord.TextChannel = ctx.guild.get_channel(1213160512364478607) funhouse_channel: discord.TextChannel = ctx.guild.get_channel(
1213160512364478607
)
message_content: str = message.content message_content: str = message.content
message_author: str = message.author.display_name message_author: str = message.author.display_name
message_channel: str = message.channel.name message_channel: str = message.channel.name
@ -232,13 +250,15 @@ class Owner(commands.Cog):
if message.attachments: if message.attachments:
for item in message.attachments: for item in message.attachments:
if item.url and len(item.url) >= 20: if item.url and len(item.url) >= 20:
image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, image: io.BytesIO = io.BytesIO(
timeout=20).raw.read()) requests.get(item.url, stream=True, timeout=20).raw.read()
ext: str = item.url.split(".")[-1]\ )
.split("?")[0].split("&")[0] ext: str = item.url.split(".")[-1].split("?")[0].split("&")[0]
_file = discord.File(image, filename=f'img.{ext}') _file = discord.File(image, filename=f"img.{ext}")
await funhouse_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\ await funhouse_channel.send(
...*\n**{message_author}:** {message_content}") f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\
...*\n**{message_author}:** {message_content}"
)
await message.delete() await message.delete()
await ctx.respond("OK!", ephemeral=True) await ctx.respond("OK!", ephemeral=True)
except: except:
@ -265,10 +285,17 @@ class Owner(commands.Cog):
audit_reason: str = f"Einsperren von {ctx.user.display_name}" audit_reason: str = f"Einsperren von {ctx.user.display_name}"
member = ctx.guild.get_member(member.id) member = ctx.guild.get_member(member.id)
member_display: str = member.display_name member_display: str = member.display_name
einsperren_role: discord.Role = ctx.guild.get_role(1235415059300093973) if ctx.guild.id != 1145182936002482196\ einsperren_role: discord.Role = (
ctx.guild.get_role(1235415059300093973)
if ctx.guild.id != 1145182936002482196
else ctx.guild.get_role(1235406301614309386) else ctx.guild.get_role(1235406301614309386)
member_roles: list = [role for role in member.roles if not role.name == "@everyone"] )
member_role_names: list[str] = [str(role.name).lower() for role in member_roles] member_roles: list = [
role for role in member.roles if not role.name == "@everyone"
]
member_role_names: list[str] = [
str(role.name).lower() for role in member_roles
]
opers_chan: discord.TextChannel = ctx.guild.get_channel(1181416083287187546) opers_chan: discord.TextChannel = ctx.guild.get_channel(1181416083287187546)
if not "einsperren" in member_role_names: if not "einsperren" in member_role_names:
try: try:
@ -279,8 +306,12 @@ class Owner(commands.Cog):
pass # Safe to ignore pass # Safe to ignore
try: try:
await member.edit(roles=[einsperren_role], reason=audit_reason) await member.edit(roles=[einsperren_role], reason=audit_reason)
await ctx.respond(f"Gesendet {member_display} an einsperren.", ephemeral=True) await ctx.respond(
await opers_chan.send(f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren.") f"Gesendet {member_display} an einsperren.", ephemeral=True
)
await opers_chan.send(
f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren."
)
except: except:
traceback.print_exc() traceback.print_exc()
return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True) return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True)
@ -292,12 +323,17 @@ class Owner(commands.Cog):
former_roles: list = self.former_roles_store.get(member.id, [0]) former_roles: list = self.former_roles_store.get(member.id, [0])
await member.edit(roles=former_roles, reason=f"De-{audit_reason}") await member.edit(roles=former_roles, reason=f"De-{audit_reason}")
await ctx.respond(f"{member_display} wurde von der Einsperre befreit.", ephemeral=True) await ctx.respond(
await opers_chan.send(f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit.") f"{member_display} wurde von der Einsperre befreit.", ephemeral=True
)
await opers_chan.send(
f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit."
)
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"ERR: {str(e)}", ephemeral=True) return await ctx.respond(f"ERR: {str(e)}", ephemeral=True)
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Owner(bot)) bot.add_cog(Owner(bot))

View File

@ -6,20 +6,24 @@ from util.radio_util import get_now_playing, skip
import discord import discord
from disc_havoc import Havoc from disc_havoc import Havoc
class Radio(commands.Cog): class Radio(commands.Cog):
"""Radio Cog for Havoc""" """Radio Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.channels: dict[str, tuple] = { self.channels: dict[str, tuple] = {
'sfm': (1145182936002482196, 1221615558492029050), # Tuple: Guild Id, Chan Id "sfm": (
1145182936002482196,
1221615558492029050,
), # Tuple: Guild Id, Chan Id
} }
self.STREAM_URL: str = "https://stream.codey.lol/sfm.ogg" self.STREAM_URL: str = "https://stream.codey.lol/sfm.ogg"
self.LAST_NP_TRACK: Optional[str] = None self.LAST_NP_TRACK: Optional[str] = None
try: try:
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
except Exception as e: except Exception as e:
logging.debug("Failed to cancel radio_state_loop: %s", logging.debug("Failed to cancel radio_state_loop: %s", str(e))
str(e))
@commands.Cog.listener() @commands.Cog.listener()
async def on_ready(self) -> None: async def on_ready(self) -> None:
@ -28,12 +32,14 @@ class Radio(commands.Cog):
def is_radio_chan(): # type: ignore def is_radio_chan(): # type: ignore
"""Check if channel is radio chan""" """Check if channel is radio chan"""
def predicate(ctx): def predicate(ctx):
try: try:
return ctx.channel.id == 1221615558492029050 return ctx.channel.id == 1221615558492029050
except: except:
traceback.print_exc() traceback.print_exc()
return False return False
return commands.check(predicate) return commands.check(predicate)
@bridge.bridge_command() @bridge.bridge_command()
@ -49,7 +55,7 @@ class Radio(commands.Cog):
async def radio_init(self) -> None: async def radio_init(self) -> None:
"""Init Radio""" """Init Radio"""
try: try:
(radio_guild, radio_chan) = self.channels['sfm'] (radio_guild, radio_chan) = self.channels["sfm"]
guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild) guild: Optional[discord.Guild] = self.bot.get_guild(radio_guild)
if not guild: if not guild:
return return
@ -62,8 +68,7 @@ class Radio(commands.Cog):
try: try:
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
except Exception as e: except Exception as e:
logging.debug("Failed to cancel radio_state_loop: %s", logging.debug("Failed to cancel radio_state_loop: %s", str(e))
str(e))
self.radio_state_loop.start() self.radio_state_loop.start()
logging.info("radio_state_loop task started!") logging.info("radio_state_loop task started!")
except: except:
@ -77,7 +82,7 @@ class Radio(commands.Cog):
async def radio_state_loop(self) -> None: async def radio_state_loop(self) -> None:
"""Radio State Loop""" """Radio State Loop"""
try: try:
(radio_guild, radio_chan) = self.channels['sfm'] (radio_guild, radio_chan) = self.channels["sfm"]
try: try:
vc: discord.VoiceProtocol = self.bot.voice_clients[-1] vc: discord.VoiceProtocol = self.bot.voice_clients[-1]
except: except:
@ -97,16 +102,22 @@ class Radio(commands.Cog):
but they exist. but they exist.
""" """
logging.info("Detected VC not playing... playing!") logging.info("Detected VC not playing... playing!")
source: discord.FFmpegAudio = discord.FFmpegOpusAudio(self.STREAM_URL, source: discord.FFmpegAudio = discord.FFmpegOpusAudio(
before_options="-timeout 3000000") self.STREAM_URL, before_options="-timeout 3000000"
vc.play(source, # type: ignore )
after=lambda e: logging.info("Error: %s", e) if e\ vc.play(
else None) source, # type: ignore
after=lambda e: logging.info("Error: %s", e) if e else None,
)
# Get Now Playing # Get Now Playing
np_track: Optional[str] = await get_now_playing() np_track: Optional[str] = await get_now_playing()
if np_track and not self.LAST_NP_TRACK == np_track: if np_track and not self.LAST_NP_TRACK == np_track:
self.LAST_NP_TRACK = np_track self.LAST_NP_TRACK = np_track
await self.bot.change_presence(activity=discord.Activity(type=discord.ActivityType.listening, name=np_track)) await self.bot.change_presence(
activity=discord.Activity(
type=discord.ActivityType.listening, name=np_track
)
)
except: except:
traceback.print_exc() traceback.print_exc()
@ -120,11 +131,11 @@ class Radio(commands.Cog):
await skip() await skip()
return await ctx.respond("OK", ephemeral=True) return await ctx.respond("OK", ephemeral=True)
def cog_unload(self) -> None: def cog_unload(self) -> None:
"""Run on Cog Unload""" """Run on Cog Unload"""
self.radio_state_loop.cancel() self.radio_state_loop.cancel()
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Radio(bot)) bot.add_cog(Radio(bot))

View File

@ -11,18 +11,23 @@ from disc_havoc import Havoc
BOT_CHANIDS = [] BOT_CHANIDS = []
class Sing(commands.Cog): class Sing(commands.Cog):
"""Sing Cog for Havoc""" """Sing Cog for Havoc"""
def __init__(self, bot: Havoc) -> None: def __init__(self, bot: Havoc) -> None:
self.bot: Havoc = bot self.bot: Havoc = bot
self.utility = Utility() self.utility = Utility()
global BOT_CHANIDS global BOT_CHANIDS
BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit
self.control_strip_regex: Pattern = regex.compile(r"\x0f|\x1f|\035|\002|\u2064|\x02|(\x03([0-9]{1,2}))|(\x03|\003)(?:\d{1,2}(?:,\d{1,2})?)?", self.control_strip_regex: Pattern = regex.compile(
regex.UNICODE) r"\x0f|\x1f|\035|\002|\u2064|\x02|(\x03([0-9]{1,2}))|(\x03|\003)(?:\d{1,2}(?:,\d{1,2})?)?",
regex.UNICODE,
)
def is_spamchan(): # type: ignore def is_spamchan(): # type: ignore
"""Check if channel is spam chan""" """Check if channel is spam chan"""
def predicate(ctx): def predicate(ctx):
try: try:
if not ctx.channel.id in BOT_CHANIDS: if not ctx.channel.id in BOT_CHANIDS:
@ -31,18 +36,19 @@ class Sing(commands.Cog):
except: except:
traceback.print_exc() traceback.print_exc()
return False return False
return commands.check(predicate) return commands.check(predicate)
@bridge.bridge_command(aliases=['sing']) @bridge.bridge_command(aliases=["sing"])
async def s(self, ctx, *, async def s(self, ctx, *, song: Optional[str] = None) -> None:
song: Optional[str] = None) -> None:
""" """
Search for lyrics, format is artist : song. Also reads activity. Search for lyrics, format is artist : song. Also reads activity.
""" """
try: try:
with ctx.channel.typing(): with ctx.channel.typing():
interaction: bool = isinstance(ctx, interaction: bool = isinstance(
discord.ext.bridge.BridgeApplicationContext) ctx, discord.ext.bridge.BridgeApplicationContext
)
activity: Optional[discord.Activity] = None activity: Optional[discord.Activity] = None
if not song: if not song:
if not ctx.author.activities: if not ctx.author.activities:
@ -53,10 +59,14 @@ class Sing(commands.Cog):
activity = _activity activity = _activity
if not activity: if not activity:
return await ctx.respond("**Error**: No song specified, no activity found to read.") return await ctx.respond(
"**Error**: No song specified, no activity found to read."
)
if interaction: if interaction:
await ctx.respond("*Searching...*") # Must respond to interactions within 3 seconds, per Discord await ctx.respond(
"*Searching...*"
) # Must respond to interactions within 3 seconds, per Discord
parsed = self.utility.parse_song_input(song, activity) parsed = self.utility.parse_song_input(song, activity)
@ -64,8 +74,9 @@ class Sing(commands.Cog):
(search_artist, search_song, search_subsearch) = parsed (search_artist, search_song, search_subsearch) = parsed
# await ctx.respond(f"So, {search_song} by {search_artist}? Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging # await ctx.respond(f"So, {search_song} by {search_artist}? Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song, search_result: Optional[list] = await self.utility.lyric_search(
search_subsearch) search_artist, search_song, search_subsearch
)
if not search_result: if not search_result:
await ctx.respond("ERR: No search result.") await ctx.respond("ERR: No search result.")
@ -78,27 +89,44 @@ class Sing(commands.Cog):
if not isinstance(search_result[0], tuple): if not isinstance(search_result[0], tuple):
return # Invalid data type return # Invalid data type
( (
search_result_artist, search_result_song, search_result_src, search_result_artist,
search_result_confidence, search_result_time_taken search_result_song,
) = search_result[0] # First index is a tuple search_result_src,
search_result_wrapped: list[str] = search_result[1] # Second index is the wrapped lyrics search_result_confidence,
search_result_wrapped_short: list[str] = search_result[2] # Third is short wrapped lyrics search_result_time_taken,
) = 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: if not ctx.channel.id in BOT_CHANIDS:
short_lyrics = " ".join(search_result_wrapped_short) # Replace with shortened lyrics for non spamchans short_lyrics = " ".join(
short_lyrics = regex.sub(r'\p{Vert_Space}', ' / ', short_lyrics.strip()) search_result_wrapped_short
return await ctx.respond(f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}") ) # Replace with shortened lyrics for non spamchans
short_lyrics = regex.sub(
r"\p{Vert_Space}", " / ", short_lyrics.strip()
)
return await ctx.respond(
f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}"
)
c: int = 0 c: int = 0
out_messages: list = [] out_messages: list = []
footer: str = "" # Placeholder footer: str = "" # Placeholder
for section in search_result_wrapped: for section in search_result_wrapped:
c+=1 c += 1
if c == len(search_result_wrapped): if c == len(search_result_wrapped):
footer = f"`Found on: {search_result_src}`" footer = f"`Found on: {search_result_src}`"
# if ctx.guild.id == 1145182936002482196: # if ctx.guild.id == 1145182936002482196:
# section = section.upper() # section = section.upper()
section = regex.sub(r'\p{Vert_Space}', ' / ', section.strip()) section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" msg: str = (
f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
)
if c > 1: if c > 1:
msg = "\n".join(msg.split("\n")[1:]) msg = "\n".join(msg.split("\n")[1:])
out_messages.append(msg.strip()) out_messages.append(msg.strip())
@ -122,58 +150,89 @@ class Sing(commands.Cog):
try: try:
PODY_ID: int = 1172340700663255091 PODY_ID: int = 1172340700663255091
IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS
member_display = ctx.interaction.guild.get_member(member.id)\ member_display = ctx.interaction.guild.get_member(member.id).display_name
.display_name if (
if not(ctx.interaction.guild.get_member(member.id).activities)\ not (ctx.interaction.guild.get_member(member.id).activities)
and not member.id == PODY_ID: and not member.id == PODY_ID
return await ctx.respond(f"No activity detected to read for {member_display}.", ephemeral=True) ):
member_id: int = member.id #if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody! return await ctx.respond(
f"No activity detected to read for {member_display}.",
ephemeral=True,
)
member_id: int = (
member.id
) # if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody!
activity: Optional[discord.Activity] = None activity: Optional[discord.Activity] = None
if IS_SPAMCHAN: if IS_SPAMCHAN:
await ctx.respond(f"***Reading activity of {member_display}...***") await ctx.respond(f"***Reading activity of {member_display}...***")
for _activity in ctx.interaction.guild.get_member(member_id).activities: for _activity in ctx.interaction.guild.get_member(member_id).activities:
if _activity.type == discord.ActivityType.listening: if _activity.type == discord.ActivityType.listening:
activity = _activity activity = _activity
parsed: Union[tuple, bool] = self.utility.parse_song_input(song=None, parsed: Union[tuple, bool] = self.utility.parse_song_input(
activity=activity) song=None, activity=activity
)
if not parsed: if not parsed:
return await ctx.respond(f"Could not parse activity of {member_display}.", ephemeral=True) return await ctx.respond(
f"Could not parse activity of {member_display}.", ephemeral=True
)
if isinstance(parsed, tuple): if isinstance(parsed, tuple):
(search_artist, search_song, search_subsearch) = parsed (search_artist, search_song, search_subsearch) = parsed
await ctx.respond("*Searching...*") # Must respond to interactions within 3 seconds, per Discord await ctx.respond(
search_result: Optional[list] = await self.utility.lyric_search(search_artist, search_song, "*Searching...*"
search_subsearch) ) # Must respond to interactions within 3 seconds, per Discord
search_result: Optional[list] = await self.utility.lyric_search(
search_artist, search_song, search_subsearch
)
if not search_result: if not search_result:
await ctx.respond("ERR: No search result") await ctx.respond("ERR: No search result")
return return
if len(search_result) == 1 and isinstance(search_result[0][0], str):
return await ctx.send(
"ERR: No search result"
) # Error message from API
if len(search_result) == 1 and\ (
isinstance(search_result[0][0], str): search_result_artist,
return await ctx.send("ERR: No search result") # Error message from API search_result_song,
search_result_src,
(search_result_artist, search_result_song, search_result_src, search_result_confidence,
search_result_confidence, search_result_time_taken) = search_result[0] # First index is a tuple search_result_time_taken,
search_result_wrapped: list = search_result[1] # Second index is the wrapped lyrics ) = search_result[
search_result_wrapped_short: list[str] = search_result[2] # Third index is shortened lyrics 0
] # First index is a tuple
search_result_wrapped: list = search_result[
1
] # Second index is the wrapped lyrics
search_result_wrapped_short: list[str] = search_result[
2
] # Third index is shortened lyrics
if not IS_SPAMCHAN: if not IS_SPAMCHAN:
short_lyrics = " ".join(search_result_wrapped_short) # Replace with shortened lyrics for non spamchans short_lyrics = " ".join(
short_lyrics = regex.sub(r'\p{Vert_Space}', ' / ', short_lyrics.strip()) search_result_wrapped_short
return await ctx.respond(f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}") ) # Replace with shortened lyrics for non spamchans
short_lyrics = regex.sub(
r"\p{Vert_Space}", " / ", short_lyrics.strip()
)
return await ctx.respond(
f"**{search_result_song}** by **{search_result_artist}**\n-# {short_lyrics}"
)
out_messages: list = [] out_messages: list = []
footer: str = "" footer: str = ""
c: int = 0 c: int = 0
for section in search_result_wrapped: for section in search_result_wrapped:
c+=1 c += 1
if c == len(search_result_wrapped): if c == len(search_result_wrapped):
footer = f"`Found on: {search_result_src}`" footer = f"`Found on: {search_result_src}`"
# if ctx.guild.id == 1145182936002482196: # if ctx.guild.id == 1145182936002482196:
# section = section.upper() # section = section.upper()
section = regex.sub(r'\p{Vert_Space}', ' / ', section.strip()) section = regex.sub(r"\p{Vert_Space}", " / ", section.strip())
msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" msg: str = (
f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}"
)
if c > 1: if c > 1:
msg = "\n".join(msg.split("\n")[1:]) msg = "\n".join(msg.split("\n")[1:])
out_messages.append(msg.strip()) out_messages.append(msg.strip())
@ -183,6 +242,7 @@ class Sing(commands.Cog):
traceback.print_exc() traceback.print_exc()
return await ctx.respond(f"ERR: {str(e)}") return await ctx.respond(f"ERR: {str(e)}")
def setup(bot) -> None: def setup(bot) -> None:
"""Run on Cog Load""" """Run on Cog Load"""
bot.add_cog(Sing(bot)) bot.add_cog(Sing(bot))

View File

@ -1,15 +1,22 @@
""" """
AI AI
""" """
class AIException(Exception): class AIException(Exception):
"""AI Exception (generic)""" """AI Exception (generic)"""
pass pass
""" """
LoveHate LoveHate
""" """
class LoveHateException(Exception): class LoveHateException(Exception):
"""Love Hate Exception (generic)""" """Love Hate Exception (generic)"""
pass pass
@ -17,6 +24,8 @@ class LoveHateException(Exception):
Misc Misc
""" """
class MiscException(Exception): class MiscException(Exception):
"""Misc Exception (generic)""" """Misc Exception (generic)"""
pass pass

View File

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

View File

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

View File

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

View File

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

View File

@ -13,24 +13,23 @@ Jesus Meme Generator
(requires Catbox uploader) (requires Catbox uploader)
""" """
class JesusMemeGenerator():
class JesusMemeGenerator:
def __init__(self) -> None: def __init__(self) -> None:
self.MEMEAPIURL = "https://apimeme.com/meme?meme=" self.MEMEAPIURL = "https://apimeme.com/meme?meme="
self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"), self.MEMESTORAGEDIR = os.path.join(os.path.expanduser("~"), "memes")
"memes")
self.top_line_regex: Pattern = regex.compile( self.top_line_regex: Pattern = regex.compile(
r'[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]' r"[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]"
) )
self.bottom_line_regex: Pattern = regex.compile( self.bottom_line_regex: Pattern = regex.compile(
r'[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]' r"[^\p{Letter}\p{Number}\p{Punctuation}\p{Horiz_Space}\p{Currency_Symbol}]"
) )
self.url_regex_1: Pattern = regex.compile( self.url_regex_1: Pattern = regex.compile(r"\p{Horiz_Space}")
r'\p{Horiz_Space}') self.url_regex_2: Pattern = regex.compile(r"#")
self.url_regex_2: Pattern = regex.compile(
r'#')
async def create_meme(self, top_line: str, bottom_line: str, async def create_meme(
meme="Jesus-Talking-To-Cool-Dude") -> Optional[str]: self, top_line: str, bottom_line: str, meme="Jesus-Talking-To-Cool-Dude"
) -> Optional[str]:
""" """
Create Meme Create Meme
@ -46,28 +45,32 @@ class JesusMemeGenerator():
try: try:
if not top_line or not bottom_line: if not top_line or not bottom_line:
return None return None
top_line = self.top_line_regex.sub('', top_line = self.top_line_regex.sub("", top_line.strip())
top_line.strip()) bottom_line = self.bottom_line_regex.sub("", bottom_line.strip())
bottom_line = self.bottom_line_regex.sub('',
bottom_line.strip())
out_fname: Optional[str] = None out_fname: Optional[str] = None
if len(top_line) < 1 or len(bottom_line) < 1: if len(top_line) < 1 or len(bottom_line) < 1:
return None return None
formed_url: str = f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}" formed_url: str = (
formed_url = self.url_regex_1.sub('+', self.url_regex_2.sub('%23', formed_url.strip())) f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}"
)
formed_url = self.url_regex_1.sub(
"+", self.url_regex_2.sub("%23", formed_url.strip())
)
timeout = aiohttp.ClientTimeout(total=15) timeout = aiohttp.ClientTimeout(total=15)
async with aiohttp.ClientSession(timeout=timeout) as session: async with aiohttp.ClientSession(timeout=timeout) as session:
async with session.get(formed_url) as response: async with session.get(formed_url) as response:
UUID = f"{random.getrandbits(8)}-{random.getrandbits(8)}" UUID = f"{random.getrandbits(8)}-{random.getrandbits(8)}"
out_fname = f"{UUID}.jpg" out_fname = f"{UUID}.jpg"
with open(f"{self.MEMESTORAGEDIR}/{out_fname}", 'wb') as f: with open(f"{self.MEMESTORAGEDIR}/{out_fname}", "wb") as f:
f.write(await response.read()) f.write(await response.read())
if not out_fname: if not out_fname:
uploader = CatboxAsync() uploader = CatboxAsync()
meme_link: Optional[str] = await uploader.upload(f"{self.MEMESTORAGEDIR}/{out_fname}") meme_link: Optional[str] = await uploader.upload(
f"{self.MEMESTORAGEDIR}/{out_fname}"
)
if not meme_link: if not meme_link:
logging.info("Meme upload failed!") logging.info("Meme upload failed!")
return None return None

View File

@ -13,12 +13,13 @@ import os
litterbox_api_url: str = "https://litterbox.catbox.moe/resources/internals/api.php" litterbox_api_url: str = "https://litterbox.catbox.moe/resources/internals/api.php"
http_headers: dict[str, str] = { http_headers: dict[str, str] = {
'accept': '*/*', "accept": "*/*",
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53', "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.64 Safari/537.36 Edg/101.0.1210.53",
'Accept-Language': 'en-US,en;q=0.9,it;q=0.8,es;q=0.7', "Accept-Language": "en-US,en;q=0.9,it;q=0.8,es;q=0.7",
'referer': 'https://www.google.com/', "referer": "https://www.google.com/",
'cookie': 'DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0' "cookie": "DSID=AAO-7r4OSkS76zbHUkiOpnI0kk-X19BLDFF53G8gbnd21VZV2iehu-w_2v14cxvRvrkd_NjIdBWX7wUiQ66f-D8kOkTKD1BhLVlqrFAaqDP3LodRK2I0NfrObmhV9HsedGE7-mQeJpwJifSxdchqf524IMh9piBflGqP0Lg0_xjGmLKEQ0F4Na6THgC06VhtUG5infEdqMQ9otlJENe3PmOQTC_UeTH5DnENYwWC8KXs-M4fWmDADmG414V0_X0TfjrYu01nDH2Dcf3TIOFbRDb993g8nOCswLMi92LwjoqhYnFdf1jzgK0",
} }
class LitterboxAsync: class LitterboxAsync:
def __init__(self) -> None: def __init__(self) -> None:
@ -35,12 +36,12 @@ class LitterboxAsync:
str str
""" """
if not fileExt: if not fileExt:
fileExt = 'png' fileExt = "png"
return f"{random.getrandbits(32)}.{fileExt}" return f"{random.getrandbits(32)}.{fileExt}"
async def upload(self, async def upload(
file: Union[str, bytes, BufferedReader], self, file: Union[str, bytes, BufferedReader], time="1h"
time='1h') -> Optional[str]: ) -> Optional[str]:
""" """
Upload File to Litterbox Upload File to Litterbox
@ -60,12 +61,12 @@ class LitterboxAsync:
if isinstance(file, BufferedReader): if isinstance(file, BufferedReader):
file = file.read() file = file.read()
fileExt: str = 'png' fileExt: str = "png"
if isinstance(file, str): if isinstance(file, str):
if file.find(".") > 0: if file.find(".") > 0:
fileExt = "".join(file.split(".")[-1:]) fileExt = "".join(file.split(".")[-1:])
file = open(file, 'rb').read() file = open(file, "rb").read()
with magic.Magic(flags=magic.MAGIC_MIME) as m: with magic.Magic(flags=magic.MAGIC_MIME) as m:
if isinstance(file, BufferedReader): if isinstance(file, BufferedReader):
@ -74,23 +75,23 @@ class LitterboxAsync:
content_type = str(m.id_filename(file)) content_type = str(m.id_filename(file))
post_data: FormData = FormData() post_data: FormData = FormData()
post_data.add_field(name="reqtype", post_data.add_field(name="reqtype", value="fileupload")
value="fileupload") post_data.add_field(name="userhash", value="")
post_data.add_field(name="userhash", post_data.add_field(name="time", value=time)
value="")
post_data.add_field(name="time",
value=time)
post_data.add_field(name="fileToUpload", post_data.add_field(
name="fileToUpload",
value=file, value=file,
filename=self.generateRandomFileName(fileExt), filename=self.generateRandomFileName(fileExt),
content_type=content_type content_type=content_type,
) )
async with ClientSession() as session: async with ClientSession() as session:
async with await session.post(self.api_path, async with await session.post(
self.api_path,
headers=self.headers, headers=self.headers,
data=post_data, data=post_data,
timeout=ClientTimeout(connect=5, sock_read=70)) as request: timeout=ClientTimeout(connect=5, sock_read=70),
) as request:
request.raise_for_status() request.raise_for_status()
return await request.text() return await request.text()
except: except:

View File

@ -4,14 +4,18 @@ from typing import LiteralString, Optional, Union
import aiosqlite as sqlite3 import aiosqlite as sqlite3
from constructors import LoveHateException from constructors import LoveHateException
class DB: class DB:
"""LoveHate DB Utility Class""" """LoveHate DB Utility Class"""
def __init__(self, bot) -> None:
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, def __init__(self, bot) -> None:
hates: bool = False) -> Union[list[tuple], bool]: self.db_path: str | LiteralString = os.path.join(
"/usr/local/share", "sqlite_dbs", "lovehate.db"
)
async def get_wholovehates(
self, thing: str, loves: bool = False, hates: bool = False
) -> Union[list[tuple], bool]:
""" """
Get a list of users who have professed their love OR hatred for <thing> Get a list of users who have professed their love OR hatred for <thing>
@ -36,7 +40,10 @@ class DB:
elif not hates and not loves: elif not hates and not loves:
raise LoveHateException("Neither loves nor hates were requested") raise LoveHateException("Neither loves nor hates were requested")
params = (thing, flag,) params = (
thing,
flag,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(query, params) as db_cursor: async with await db_conn.execute(query, params) as db_cursor:
result: list[tuple] = await db_cursor.fetchall() result: list[tuple] = await db_cursor.fetchall()
@ -45,8 +52,13 @@ class DB:
return False return False
return result return result
async def get_lovehates(self, loves: bool = False, hates: bool = False, async def get_lovehates(
user: Optional[str] = None, thing: Optional[str] = None) -> Union[list[tuple], bool]: self,
loves: bool = False,
hates: bool = False,
user: Optional[str] = None,
thing: Optional[str] = None,
) -> Union[list[tuple], bool]:
""" """
Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates
@ -64,7 +76,9 @@ class DB:
params: tuple = tuple() params: tuple = tuple()
if not user and not thing: if not user and not thing:
raise LoveHateException("Neither a <user> or <thing> was specified to query against") raise LoveHateException(
"Neither a <user> or <thing> was specified to query against"
)
flag: Optional[int] = None flag: Optional[int] = None
@ -79,10 +93,16 @@ class DB:
if user: if user:
query = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?" query = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?"
params = (user, flag,) params = (
user,
flag,
)
elif thing: elif thing:
query = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?" query = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?"
params = (thing, flag,) params = (
thing,
flag,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(query, params) as db_cursor: async with await db_conn.execute(query, params) as db_cursor:
@ -91,7 +111,6 @@ class DB:
return False return False
return result return result
async def check_existence(self, user: str, thing: str) -> Optional[int]: async def check_existence(self, user: str, thing: str) -> Optional[int]:
""" """
Determine whether a user is opinionated on a <thing> Determine whether a user is opinionated on a <thing>
@ -103,10 +122,16 @@ class DB:
Optional[int] Optional[int]
""" """
params = (user, thing,) params = (
user,
thing,
)
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute("SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?", params) as db_cursor: async with await db_conn.execute(
"SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?",
params,
) as db_cursor:
result = await db_cursor.fetchone() result = await db_cursor.fetchone()
if not result: if not result:
return None return None
@ -125,20 +150,29 @@ class DB:
str str
""" """
if not flag in range(-1, 2): if not flag in range(-1, 2):
raise LoveHateException(f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)") raise LoveHateException(
f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)"
)
db_query: str = "" db_query: str = ""
params: tuple = (user, thing,) params: tuple = (
user,
thing,
)
already_opinionated: Optional[int] = await self.check_existence(user, thing) already_opinionated: Optional[int] = await self.check_existence(user, thing)
if already_opinionated: if already_opinionated:
if flag == 0: if flag == 0:
db_query = "DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?" db_query = (
"DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?"
)
else: else:
loves_or_hates: str = "loves" loves_or_hates: str = "loves"
if already_opinionated == -1: if already_opinionated == -1:
loves_or_hates = "hates" loves_or_hates = "hates"
raise LoveHateException(f"But {user} already {loves_or_hates} {thing}...") raise LoveHateException(
f"But {user} already {loves_or_hates} {thing}..."
)
else: else:
match flag: match flag:
case -1: case -1:
@ -148,12 +182,13 @@ class DB:
case _: case _:
raise LoveHateException("Unknown error, default case matched") raise LoveHateException("Unknown error, default case matched")
async with sqlite3.connect(self.db_path, timeout=2) as db_conn: async with sqlite3.connect(self.db_path, timeout=2) as db_conn:
async with await db_conn.execute(db_query, params) as db_cursor: async with await db_conn.execute(db_query, params) as db_cursor:
await db_conn.commit() await db_conn.commit()
if db_cursor.rowcount != 1: if db_cursor.rowcount != 1:
raise LoveHateException(f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query") raise LoveHateException(
f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query"
)
match flag: match flag:
case -1: case -1:
return f"We're done here, {user} hates {thing}." return f"We're done here, {user} hates {thing}."
@ -162,4 +197,6 @@ class DB:
case 1: case 1:
return f"We're done here, {user} loves {thing}." return f"We're done here, {user} loves {thing}."
case _: case _:
raise LoveHateException("Unknown error, default case matched [2]") raise LoveHateException(
"Unknown error, default case matched [2]"
)

View File

@ -11,62 +11,117 @@ from aiohttp import ClientSession, ClientTimeout
from bohancompliment import ComplimentGenerator from bohancompliment import ComplimentGenerator
from discord import Embed from discord import Embed
class Util: class Util:
"""Misc Utility""" """Misc Utility"""
def __init__(self) -> None: def __init__(self) -> None:
self.URL_URBANDICTIONARY: str = "http://api.urbandictionary.com/v0/define" self.URL_URBANDICTIONARY: str = "http://api.urbandictionary.com/v0/define"
self.URL_INSULTAPI: str = "https://insult.mattbas.org/api/insult" self.URL_INSULTAPI: str = "https://insult.mattbas.org/api/insult"
self.COMPLIMENT_GENERATOR = ComplimentGenerator() self.COMPLIMENT_GENERATOR = ComplimentGenerator()
self.dbs: dict[str, str|LiteralString] = { self.dbs: dict[str, str | LiteralString] = {
'whisky': os.path.join("/usr/local/share", "whisky": os.path.join("/usr/local/share", "sqlite_dbs", "whiskey.db"),
"sqlite_dbs", "whiskey.db"), "drinks": os.path.join("/usr/local/share", "sqlite_dbs", "cocktails.db"),
'drinks': os.path.join("/usr/local/share", "strains": os.path.join("/usr/local/share", "sqlite_dbs", "strains.db"),
"sqlite_dbs", "cocktails.db"), "qajoke": os.path.join("/usr/local/share", "sqlite_dbs", "qajoke.db"),
'strains': os.path.join("/usr/local/share", "rjokes": os.path.join("/usr/local/share", "sqlite_dbs", "rjokes.db"),
"sqlite_dbs", "strains.db"), "randmsg": os.path.join("/usr/local/share", "sqlite_dbs", "randmsg.db"),
'qajoke': os.path.join("/usr/local/share", "stats": os.path.join("/usr/local/share", "sqlite_dbs", "havoc_stats.db"),
"sqlite_dbs", "qajoke.db"), "cookies": os.path.join("/usr/local/share", "sqlite_dbs", "cookies.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', self.COFFEES: list = [
'a cup of Turkish coffee', 'a cup of Moka', 'an espresso', "a cup of french-pressed coffee",
'a cup of Nescafe coffee', "a cup of cold brew",
'an iced coffee', 'a Frappé', 'a freddo cappuccino', "a cup of flash brew",
'a cup of Chock full o\'Nuts', 'a cup of Folgers', 'a cup of Lavazza', "a cup of Turkish coffee",
'a cup of Maxwell House', 'a cup of Moccona', 'a cup of Mr. Brown Coffee', "a cup of Moka",
'a cup of affogato al caffè', "an espresso",
'a cup of Caffè Medici', 'a cup of Café Touba', "a cup of Nescafe coffee",
'a double-double', 'an indian filter coffee', 'a cup of pocillo', "an iced coffee",
'a cup of caffè americano', 'a cup of caffè lungo', 'a latte', 'a manilo', "a Frappé",
'a flat white', 'a cup of café cubano', 'a cup of caffè crema', "a freddo cappuccino",
'a cup of cafe zorro', 'an espresso roberto', 'an espresso romano', "a cup of Chock full o'Nuts",
'an espresso sara', 'a guillermo', 'a ristretto', 'a cup of melya', "a cup of Folgers",
'a cup of caffè marocchino', 'a cup of café miel', 'a cup of café de olla', "a cup of Lavazza",
'a Mazagran', 'a Palazzo', 'an ice shot', 'a macchiato', "a cup of Maxwell House",
'a cortado', 'a red eye', 'a cappuccino', "a cup of Moccona",
'a mocha', 'a café au lait', 'a bicerin', "a cup of Mr. Brown Coffee",
'a caffè corretto', 'a ca phe trung', 'a café bombón', 'a Vienna coffee', "a cup of affogato al caffè",
'a flat black', 'a lungo', 'a doppio', 'a ristretto bianco', 'a piccolo latte', "a cup of Caffè Medici",
'a gibraltar', 'a breve', 'a café con leche', 'a su café', 'a café del tiempo', "a cup of Café Touba",
'a java chip frappuccino', 'a pumpkin spice latte', 'a caramel macchiato', "a double-double",
'a white chocolate mocha', 'a hazelnut coffee', 'a toffee nut latte', "an indian filter coffee",
'a peppermint mocha', 'a cinnamon dolce latte', 'a coconut milk latte', "a cup of pocillo",
'an almond milk cappuccino', 'an oat milk latte', 'a caramel frappuccino', "a cup of caffè americano",
'a chocolate frappuccino', 'a butter pecan coffee', 'a maple pecan latte', "a cup of caffè lungo",
'a sea salt caramel mocha', 'a nitro cold brew', 'a pumpkin cold brew', "a latte",
'a honey almond flat white', 'a sweet cream cold brew', 'a matcha latte', "a manilo",
'a golden latte', 'a turmeric latte', 'a beetroot latte', 'a kopi luwak'] "a flat white",
"a cup of café cubano",
"a cup of caffè crema",
"a cup of cafe zorro",
"an espresso roberto",
"an espresso romano",
"an espresso sara",
"a guillermo",
"a ristretto",
"a cup of melya",
"a cup of caffè marocchino",
"a cup of café miel",
"a cup of café de olla",
"a Mazagran",
"a Palazzo",
"an ice shot",
"a macchiato",
"a cortado",
"a red eye",
"a cappuccino",
"a mocha",
"a café au lait",
"a bicerin",
"a caffè corretto",
"a ca phe trung",
"a café bombón",
"a Vienna coffee",
"a flat black",
"a lungo",
"a doppio",
"a ristretto bianco",
"a piccolo latte",
"a gibraltar",
"a breve",
"a café con leche",
"a su café",
"a café del tiempo",
"a java chip frappuccino",
"a pumpkin spice latte",
"a caramel macchiato",
"a white chocolate mocha",
"a hazelnut coffee",
"a toffee nut latte",
"a peppermint mocha",
"a cinnamon dolce latte",
"a coconut milk latte",
"an almond milk cappuccino",
"an oat milk latte",
"a caramel frappuccino",
"a chocolate frappuccino",
"a butter pecan coffee",
"a maple pecan latte",
"a sea salt caramel mocha",
"a nitro cold brew",
"a pumpkin cold brew",
"a honey almond flat white",
"a sweet cream cold brew",
"a matcha latte",
"a golden latte",
"a turmeric latte",
"a beetroot latte",
"a kopi luwak",
]
self.LAST_5_COFFEES: list = [] self.LAST_5_COFFEES: list = []
def tdTuple(self, td: datetime.timedelta) -> tuple:
def tdTuple(self, td:datetime.timedelta) -> tuple:
""" """
Create TimeDelta Tuple Create TimeDelta Tuple
@ -76,11 +131,13 @@ class Util:
tuple tuple
""" """
def _t(t, n): def _t(t, n):
if t < n: if t < n:
return (t, 0) return (t, 0)
v = t//n v = t // n
return (t - (v * n), v) return (t - (v * n), v)
(s, h) = _t(td.seconds, 3600) (s, h) = _t(td.seconds, 3600)
(s, m) = _t(s, 60) (s, m) = _t(s, 60)
(mics, mils) = _t(td.microseconds, 1000) (mics, mils) = _t(td.microseconds, 1000)
@ -96,16 +153,17 @@ class Util:
Optional[dict] Optional[dict]
""" """
stats_db: str|LiteralString = self.dbs.get('stats', '') stats_db: str | LiteralString = self.dbs.get("stats", "")
if not stats_db: if not stats_db:
return None return None
async with sqlite3.connect(stats_db, async with sqlite3.connect(stats_db, timeout=3) as db_conn:
timeout=3) as db_conn:
db_conn.row_factory = sqlite3.Row db_conn.row_factory = sqlite3.Row
query: str = "SELECT ? FROM stats LIMIT 1" query: str = "SELECT ? FROM stats LIMIT 1"
if not counter: if not counter:
query = "SELECT * FROM stats LIMIT 1" query = "SELECT * FROM stats LIMIT 1"
async with await db_conn.execute(query, (counter,) if counter else None) as db_cursor: async with await db_conn.execute(
query, (counter,) if counter else None
) as db_cursor:
result = await db_cursor.fetchone() result = await db_cursor.fetchone()
return result return result
@ -122,11 +180,11 @@ class Util:
return None return None
embed: Embed = Embed(title="Stats") embed: Embed = Embed(title="Stats")
counter_message: str = "" counter_message: str = ""
counters_sorted: dict = dict(sorted(counters.items(), counters_sorted: dict = dict(
key=lambda item: item[1], reverse=True)) sorted(counters.items(), key=lambda item: item[1], reverse=True)
)
for counter, value in counters_sorted.items(): for counter, value in counters_sorted.items():
counter = regex.sub(r'_', ' ', counter = regex.sub(r"_", " ", counter.strip()).title()
counter.strip()).title()
counter_message += f"- {value} {counter}\n" counter_message += f"- {value} {counter}\n"
embed.description = counter_message.strip() embed.description = counter_message.strip()
return embed return embed
@ -141,14 +199,17 @@ class Util:
bool bool
""" """
stats_db: str|LiteralString = self.dbs.get('stats', '') stats_db: str | LiteralString = self.dbs.get("stats", "")
if not stats_db: if not stats_db:
return False return False
async with sqlite3.connect(stats_db, async with sqlite3.connect(stats_db, timeout=3) as db_conn:
timeout=3) as db_conn: async with await db_conn.execute(
async with await db_conn.execute(f"UPDATE stats SET {counter} = {counter} + 1") as db_cursor: f"UPDATE stats SET {counter} = {counter} + 1"
) as db_cursor:
if db_cursor.rowcount < 0: if db_cursor.rowcount < 0:
logging.critical("[karma::increment_counter] Fail! %s", db_cursor.rowcount) logging.critical(
"[karma::increment_counter] Fail! %s", db_cursor.rowcount
)
return False return False
await db_conn.commit() await db_conn.commit()
return True return True
@ -165,23 +226,30 @@ class Util:
""" """
try: try:
async with ClientSession() as session: async with ClientSession() as session:
async with await session.get(self.URL_URBANDICTIONARY, async with await session.get(
self.URL_URBANDICTIONARY,
params={ params={
"term": term, "term": term,
}, },
headers = { headers={
'content-type': 'application/json; charset=utf-8', "content-type": "application/json; charset=utf-8",
}, timeout=ClientTimeout(connect=5, sock_read=5)) as request: },
logging.debug("UD returned: %s", timeout=ClientTimeout(connect=5, sock_read=5),
await request.text()) ) as request:
logging.debug("UD returned: %s", await request.text())
data: dict = await request.json() data: dict = await request.json()
if "list" in data: if "list" in data:
definitions: list[dict] = data["list"] definitions: list[dict] = data["list"]
if definitions: if definitions:
definition: dict = definitions[0] definition: dict = definitions[0]
definition_word: str = definition.get("word", "N/A") definition_word: str = definition.get("word", "N/A")
definition_text: str = regex.sub(r'(\r|\n|\r\n)', ' ', definition["definition"].strip()) definition_text: str = regex.sub(
return (definition_word, definition_text) # Tuple: Returned word, returned definition r"(\r|\n|\r\n)", " ", definition["definition"].strip()
)
return (
definition_word,
definition_text,
) # Tuple: Returned word, returned definition
else: else:
return (term, "Not found!") return (term, "Not found!")
else: else:
@ -201,13 +269,13 @@ class Util:
""" """
async with ClientSession() as session: async with ClientSession() as session:
async with await session.get(f"{self.URL_INSULTAPI}?who={recipient}") as request: async with await session.get(
f"{self.URL_INSULTAPI}?who={recipient}"
) as request:
request.raise_for_status() request.raise_for_status()
return await request.text() return await request.text()
async def get_compliment(self, subject: str, language: Optional[str] = "en") -> str:
async def get_compliment(self, subject: str,
language: Optional[str] = 'en') -> str:
""" """
Get Compliment Get Compliment
@ -228,26 +296,35 @@ class Util:
Optional[tuple] Optional[tuple]
""" """
whisky_db: str|LiteralString = self.dbs.get('whisky', '') whisky_db: str | LiteralString = self.dbs.get("whisky", "")
if not whisky_db: if not whisky_db:
return None return None
async with sqlite3.connect(database=whisky_db, async with sqlite3.connect(database=whisky_db, timeout=2) as db_conn:
timeout=2) as db_conn: db_query: str = (
db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1" "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1"
)
async with await db_conn.execute(db_query) as db_cursor: async with await db_conn.execute(db_query) as db_cursor:
db_result: Optional[Union[sqlite3.Row, tuple]] = await db_cursor.fetchone() db_result: Optional[Union[sqlite3.Row, tuple]] = (
await db_cursor.fetchone()
)
if not db_result: if not db_result:
return None return None
(name, category, description) = db_result (name, category, description) = db_result
name = regex.sub(r'(^\p{White_Space}|\r|\n)', '', name = regex.sub(
regex.sub(r'\p{White_Space}{2,}', ' ', r"(^\p{White_Space}|\r|\n)",
name.strip())) "",
category = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r"\p{White_Space}{2,}", " ", name.strip()),
regex.sub(r'\p{White_Space}{2,}', ' ', )
category.strip())) category = regex.sub(
description = regex.sub(r'(^\p{White_Space}|\r|\n)', '', r"(^\p{White_Space}|\r|\n)",
regex.sub(r'\p{White_Space}{2,}', ' ', "",
description.strip())) regex.sub(r"\p{White_Space}{2,}", " ", category.strip()),
)
description = regex.sub(
r"(^\p{White_Space}|\r|\n)",
"",
regex.sub(r"\p{White_Space}{2,}", " ", description.strip()),
)
return (name, category, description) return (name, category, description)
async def get_drink(self) -> Optional[tuple]: async def get_drink(self) -> Optional[tuple]:
@ -258,19 +335,28 @@ class Util:
Optional[tuple] Optional[tuple]
""" """
drinks_db: str|LiteralString = self.dbs.get('drinks', '') drinks_db: str | LiteralString = self.dbs.get("drinks", "")
if not drinks_db: if not drinks_db:
return None return None
async with sqlite3.connect(database=drinks_db, async with sqlite3.connect(database=drinks_db, timeout=2) as db_conn:
timeout=2) as db_conn: db_query: str = (
db_query: str = "SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1" "SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1"
)
async with await db_conn.execute(db_query) as db_cursor: async with await db_conn.execute(db_query) as db_cursor:
db_result: tuple = await db_cursor.fetchone() db_result: tuple = await db_cursor.fetchone()
(name, ingredients) = db_result (name, ingredients) = db_result
name = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', name.strip())) name = regex.sub(
ingredients = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', ingredients.strip())) r"(^\p{White_Space}|\r|\n)",
ingredients = regex.sub(r'\*', '\u2731', ingredients.strip()) "",
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) return (name, ingredients)
async def get_strain(self, strain: Optional[str] = None) -> Optional[tuple]: async def get_strain(self, strain: Optional[str] = None) -> Optional[tuple]:
@ -283,16 +369,19 @@ class Util:
Optional[tuple] Optional[tuple]
""" """
strains_db: str|LiteralString = self.dbs.get('strains', '') strains_db: str | LiteralString = self.dbs.get("strains", "")
if not strains_db: if not strains_db:
return None return None
async with sqlite3.connect(database=strains_db, async with sqlite3.connect(database=strains_db, timeout=2) as db_conn:
timeout=2) as db_conn:
db_params: Optional[tuple] = None db_params: Optional[tuple] = None
if not strain: if not strain:
db_query: str = "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1" db_query: str = (
"SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1"
)
else: else:
db_query = "SELECT name, description FROM strains_w_desc WHERE name LIKE ?" db_query = (
"SELECT name, description FROM strains_w_desc WHERE name LIKE ?"
)
db_params = (f"%{strain.strip()}%",) db_params = (f"%{strain.strip()}%",)
async with await db_conn.execute(db_query, db_params) as db_cursor: async with await db_conn.execute(db_query, db_params) as db_cursor:
db_result: Optional[tuple] = await db_cursor.fetchone() db_result: Optional[tuple] = await db_cursor.fetchone()
@ -306,12 +395,13 @@ class Util:
Optional[tuple] Optional[tuple]
""" """
qajoke_db: str|LiteralString = self.dbs.get('qajoke', '') qajoke_db: str | LiteralString = self.dbs.get("qajoke", "")
if not qajoke_db: if not qajoke_db:
return None return None
async with sqlite3.connect(database=qajoke_db, async with sqlite3.connect(database=qajoke_db, timeout=2) as db_conn:
timeout=2) as db_conn: db_query: str = (
db_query: str = "SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1" "SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1"
)
async with await db_conn.execute(db_query) as cursor: async with await db_conn.execute(db_query) as cursor:
(question, answer) = await cursor.fetchone() (question, answer) = await cursor.fetchone()
return (question, answer) return (question, answer)
@ -325,17 +415,18 @@ class Util:
Optional[tuple] Optional[tuple]
""" """
rjokes_db: str|LiteralString = self.dbs.get('rjokes', '') rjokes_db: str | LiteralString = self.dbs.get("rjokes", "")
if not rjokes_db: if not rjokes_db:
return None return None
async with sqlite3.connect(database=rjokes_db, timeout=2) as db_conn: async with sqlite3.connect(database=rjokes_db, timeout=2) as db_conn:
db_query: str = "SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'" db_query: str = (
"SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'"
)
async with await db_conn.execute(db_query) as cursor: async with await db_conn.execute(db_query) as cursor:
(title, body, score) = await cursor.fetchone() (title, body, score) = await cursor.fetchone()
return (title, body, score) return (title, body, score)
return None return None
async def get_random_fact(self) -> str: async def get_random_fact(self) -> str:
""" """
Get Random Fact Get Random Fact
@ -346,21 +437,25 @@ class Util:
""" """
try: try:
facts_api_url: str = "https://uselessfacts.jsph.pl/api/v2/facts/random" facts_api_url: str = "https://uselessfacts.jsph.pl/api/v2/facts/random"
facts_backup_url: str = "https://cnichols1734.pythonanywhere.com/facts/random" facts_backup_url: str = (
"https://cnichols1734.pythonanywhere.com/facts/random"
)
async with ClientSession() as client: async with ClientSession() as client:
try: try:
async with await client.get(facts_api_url, async with await client.get(
timeout=ClientTimeout(connect=5, sock_read=5)) as request: facts_api_url, timeout=ClientTimeout(connect=5, sock_read=5)
) as request:
_json: dict = await request.json() _json: dict = await request.json()
fact: str = _json.get('text', None) fact: str = _json.get("text", None)
if not fact: if not fact:
raise BaseException("RandFact Src 1 Failed") raise BaseException("RandFact Src 1 Failed")
return fact return fact
except: except:
async with await client.get(facts_backup_url, async with await client.get(
timeout=ClientTimeout(connect=5, sock_read=5)) as request: facts_backup_url, timeout=ClientTimeout(connect=5, sock_read=5)
) as request:
_json = await request.json() _json = await request.json()
fact = _json.get('fact', None) fact = _json.get("fact", None)
return fact return fact
except Exception as e: except Exception as e:
traceback.print_exc() traceback.print_exc()
@ -374,23 +469,18 @@ class Util:
Optional[dict] Optional[dict]
""" """
cookies_db = self.dbs.get('cookies', '') cookies_db = self.dbs.get("cookies", "")
if not cookies_db: if not cookies_db:
return None return None
async with sqlite3.connect(cookies_db, async with sqlite3.connect(cookies_db, timeout=2) as db_conn:
timeout=2) as db_conn: db_query: str = (
db_query: str = "SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1" "SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1"
)
async with await db_conn.execute(db_query) as db_cursor: async with await db_conn.execute(db_query) as db_cursor:
(name, origin, image_url) = await db_cursor.fetchone() (name, origin, image_url) = await db_cursor.fetchone()
return { return {"name": name, "origin": origin, "image_url": image_url}
'name': name,
'origin': origin,
'image_url': image_url
}
def get_coffee(self, recipient_allergic: Optional[bool] = False) -> Optional[str]:
def get_coffee(self,
recipient_allergic: Optional[bool] = False) -> Optional[str]:
""" """
Get Coffee Get Coffee
@ -403,8 +493,11 @@ class Util:
""" """
try: try:
randomCoffee: str = random.choice(self.COFFEES) randomCoffee: str = random.choice(self.COFFEES)
if self.LAST_5_COFFEES and randomCoffee in self.LAST_5_COFFEES\ if (
or (recipient_allergic and "nut" in randomCoffee.lower()): self.LAST_5_COFFEES
and randomCoffee in self.LAST_5_COFFEES
or (recipient_allergic and "nut" in randomCoffee.lower())
):
return self.get_coffee() # Recurse return self.get_coffee() # Recurse
if len(self.LAST_5_COFFEES) >= 5: 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.pop() # Store no more than 5 of the last served coffees
@ -429,7 +522,7 @@ class Util:
day=25, day=25,
tzinfo=pytz.UTC, tzinfo=pytz.UTC,
) )
td: datetime.timedelta = (xmas - today) td: datetime.timedelta = xmas - today
days, hours, minutes, seconds, us, ms = self.tdTuple(td) days, hours, minutes, seconds, us, ms = self.tdTuple(td)
return (days, hours, minutes, seconds, ms, us) return (days, hours, minutes, seconds, ms, us)
@ -442,11 +535,10 @@ class Util:
Optional[str] Optional[str]
""" """
randmsg_db: str|LiteralString = self.dbs.get('randmsg', '') randmsg_db: str | LiteralString = self.dbs.get("randmsg", "")
if not randmsg_db: if not randmsg_db:
return None return None
async with sqlite3.connect(database=randmsg_db, async with sqlite3.connect(database=randmsg_db, timeout=2) as db_conn:
timeout=2) as db_conn:
db_query: str = "SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1" db_query: str = "SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1"
async with await db_conn.execute(db_query) as db_cursor: async with await db_conn.execute(db_query) as db_cursor:
(result,) = await db_cursor.fetchone() (result,) = await db_cursor.fetchone()

View File

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

View File

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