initial push
This commit is contained in:
		
							
								
								
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  |  | ||||||
|  |  | ||||||
|  | # Discord-Havoc Rewrite (Pycord) | ||||||
							
								
								
									
										49
									
								
								api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								api.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  |  | ||||||
|  | import importlib | ||||||
|  | from typing import Optional | ||||||
|  | from fastapi import FastAPI, HTTPException | ||||||
|  | from pydantic import BaseModel | ||||||
|  | import util | ||||||
|  |  | ||||||
|  | class ValidSendMsgRequest(BaseModel): | ||||||
|  |     """ | ||||||
|  |     - **guild**: optional, guild id in case multiple channels match (normally first result would be used) | ||||||
|  |     - **channel**: channel to target | ||||||
|  |     - **message**: message to send | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     guild: Optional[int] = None | ||||||
|  |     channel: str | ||||||
|  |     message: str | ||||||
|  |  | ||||||
|  | class API: | ||||||
|  |     """API [FastAPI Instance] for Havoc""" | ||||||
|  |     def __init__(self, discord_bot): | ||||||
|  |         api_app = FastAPI(title="Havoc API") | ||||||
|  |         self.bot = discord_bot | ||||||
|  |         self.api_app = api_app | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         @api_app.get("/{any:path}") | ||||||
|  |         def block_get(): | ||||||
|  |             raise HTTPException(status_code=403, detail="Invalid request") | ||||||
|  |          | ||||||
|  |         @api_app.post("/send_msg") | ||||||
|  |         async def send_msg_handler(data: ValidSendMsgRequest): | ||||||
|  |             await util.discord_helpers.send_message( | ||||||
|  |                 bot=self.bot, | ||||||
|  |                 guild=data.guild, | ||||||
|  |                 channel=data.channel, | ||||||
|  |                 message=data.message, | ||||||
|  |             ) | ||||||
|  |             return { | ||||||
|  |                 'result': "presumed_success", | ||||||
|  |             } | ||||||
|  |      | ||||||
|  |  | ||||||
|  | def __init__(): | ||||||
|  |     import util # pylint: disable=redefined-outer-name, reimported, import-outside-toplevel | ||||||
|  |     importlib.reload(util) | ||||||
|  |  | ||||||
|  | __init__() | ||||||
							
								
								
									
										308
									
								
								cogs/karma.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										308
									
								
								cogs/karma.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,308 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=broad-exception-caught, bare-except, invalid-name | ||||||
|  |  | ||||||
|  | import sys | ||||||
|  | from os import path | ||||||
|  | sys.path.append( path.dirname( path.dirname( path.abspath(__file__) ) ) ) | ||||||
|  | import constants | ||||||
|  | import traceback | ||||||
|  | import time | ||||||
|  | import importlib | ||||||
|  | import logging | ||||||
|  | import discord | ||||||
|  | import regex | ||||||
|  | from typing import Pattern | ||||||
|  | from aiohttp import ClientSession, ClientTimeout | ||||||
|  | from discord.ext import bridge, commands, tasks | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Util: | ||||||
|  |     """Karma Utility""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         self.bot = bot | ||||||
|  |         self.api_key: str = constants.PRV_API_KEY | ||||||
|  |         self.karma_endpoints_base_url: str = "https://api.codey.lol/karma/" | ||||||
|  |         self.karma_retrieval_url: str = f"{self.karma_endpoints_base_url}get" | ||||||
|  |         self.karma_update_url: str = f"{self.karma_endpoints_base_url}modify" | ||||||
|  |         self.karma_top_10_url: str = f"{self.karma_endpoints_base_url}top"                 | ||||||
|  |         self.timers: dict = {} # discord uid : timestamp, used for rate limiting | ||||||
|  |         self.karma_cooldown: int = 15 # 15 seconds between karma updates         | ||||||
|  |  | ||||||
|  |     async def get_karma(self, keyword: str) -> int|str: | ||||||
|  |         """ | ||||||
|  |         Get Karma for Keyword | ||||||
|  |         Args: | ||||||
|  |             keyword (str) | ||||||
|  |         Returns: | ||||||
|  |             int|str | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             async with ClientSession() as session: | ||||||
|  |                 async with await session.post(self.karma_retrieval_url, | ||||||
|  |                                         json={'keyword': keyword}, | ||||||
|  |                                         headers={ | ||||||
|  |                                             'content-type': 'application/json; charset=utf-8', | ||||||
|  |                                             'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}', | ||||||
|  |                                             }, timeout=ClientTimeout(connect=3, sock_read=5)) as request: | ||||||
|  |                     resp = await request.json() | ||||||
|  |                     return resp.get('count') | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return f"Failed-- {type(e).__name__}: {str(e)}" | ||||||
|  |              | ||||||
|  |     async def get_top(self, n: int = 10) -> dict: | ||||||
|  |         """ | ||||||
|  |         Get top (n=10) Karma | ||||||
|  |         Args: | ||||||
|  |             n (int): Number of top results to return, default 10 | ||||||
|  |         Returns: | ||||||
|  |             dict | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             async with ClientSession() as session: | ||||||
|  |                 async with await session.post(self.karma_top_10_url, | ||||||
|  |                                         json = { | ||||||
|  |                                             'n': n, | ||||||
|  |                                         }, | ||||||
|  |                                         headers={ | ||||||
|  |                                             'content-type': 'application/json; charset=utf-8', | ||||||
|  |                                             'X-Authd-With': f'Bearer {constants.KARMA_API_KEY}' | ||||||
|  |                                         }, timeout=ClientTimeout(connect=3, sock_read=5)) as request: | ||||||
|  |                     resp: dict = await request.json() | ||||||
|  |                     return resp | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     async def get_top_embed(self, n:int = 10) -> discord.Embed: | ||||||
|  |         """ | ||||||
|  |         Get Top Karma Embed | ||||||
|  |         Args: | ||||||
|  |             n (int): Number of top results to return, default 10 | ||||||
|  |         Returns: | ||||||
|  |             discord.Embed | ||||||
|  |         """ | ||||||
|  |         top: dict = await self.get_top(n) | ||||||
|  |         top_formatted: str = "" | ||||||
|  |         for x, item in enumerate(top): | ||||||
|  |             top_formatted += f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n" | ||||||
|  |         top_formatted: str = top_formatted.strip() | ||||||
|  |         embed: discord.Embed = discord.Embed(title=f"Top {n} Karma", | ||||||
|  |                             description=top_formatted, | ||||||
|  |                             colour=0xff00ff) | ||||||
|  |         return embed | ||||||
|  |  | ||||||
|  |     async def update_karma(self, display: str, _id: int, keyword: str, flag: int) -> bool: | ||||||
|  |         """ | ||||||
|  |         Update Karma for Keyword | ||||||
|  |         Args: | ||||||
|  |             display (str): Display name of the user who requested the update | ||||||
|  |             _id (int): Discord UID of the user who requested the update | ||||||
|  |             keyword (str): Keyword to update | ||||||
|  |             flag (int) | ||||||
|  |             """ | ||||||
|  |         if not flag in [0, 1]: | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         reqObj: dict = { | ||||||
|  |             'granter': f"Discord: {display} ({_id})", | ||||||
|  |             'keyword': keyword, | ||||||
|  |             'flag': flag, | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             async with ClientSession() as session: | ||||||
|  |                 async with await session.post(self.karma_update_url, | ||||||
|  |                                         json=reqObj, | ||||||
|  |                                         headers={ | ||||||
|  |                                             'content-type': 'application/json; charset=utf-8', | ||||||
|  |                                             'X-Authd-With': f'Bearer {self.api_key}', | ||||||
|  |                                         }, | ||||||
|  |                                         timeout=ClientTimeout(connect=3, sock_read=5)) as request: | ||||||
|  |                     result = await request.json() | ||||||
|  |                     return result.get('success', False) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return False | ||||||
|  |  | ||||||
|  |          | ||||||
|  |     async def check_cooldown(self, user_id: int) -> bool: | ||||||
|  |         """ | ||||||
|  |         Check if member has met cooldown period prior to adjusting karma | ||||||
|  |         Args: | ||||||
|  |             user_id (int): The Discord UID to check | ||||||
|  |         Returns: | ||||||
|  |             bool | ||||||
|  |         """ | ||||||
|  |         if not user_id in self.timers: | ||||||
|  |             return True | ||||||
|  |         now = int(time.time()) | ||||||
|  |         if (now - self.timers[user_id]) < self.karma_cooldown: | ||||||
|  |             return False | ||||||
|  |         return True | ||||||
|  |              | ||||||
|  |      | ||||||
|  |  | ||||||
|  | class Karma(commands.Cog): | ||||||
|  |     """Karma Cog for Havoc""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         importlib.reload(constants) | ||||||
|  |         self.bot = bot | ||||||
|  |         self.util = Util(self.bot) | ||||||
|  |         # self.karma_regex = regex.compile(r'(\w+)(\+\+|\-\-)') | ||||||
|  |         self.karma_regex: Pattern = regex.compile(r'(\b\w+(?:\s+\w+)*)(\+\+($|\s)|\-\-($|\s))') | ||||||
|  |         self.mention_regex: Pattern = regex.compile(r'(<@([0-9]{17,20})>)(\+\+|\-\-)') | ||||||
|  |         self.mention_regex_no_flag: Pattern = regex.compile(r'(<@([0-9]{17,20})>+)') | ||||||
|  |         self.karma_chanid: int = 1307065684785893406 | ||||||
|  |         self.karma_msgid: int = 1325442184572567686 | ||||||
|  |  | ||||||
|  |         # asyncio.get_event_loop().create_task(self.bot.get_channel(self.karma_chanid).send(".")) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             self.update_karma_chan.start() | ||||||
|  |         except Exception as e: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @tasks.loop(seconds=30, reconnect=True) | ||||||
|  |     async def update_karma_chan(self): | ||||||
|  |         """Update the Karma Chan Leaderboard""" | ||||||
|  |         try: | ||||||
|  |             top_embed = await self.util.get_top_embed(n=25) | ||||||
|  |             channel = self.bot.get_channel(self.karma_chanid) | ||||||
|  |             message_to_edit = await channel.fetch_message(self.karma_msgid) | ||||||
|  |             await message_to_edit.edit(embed=top_embed, | ||||||
|  |                                     content="## This message will automatically update periodically.") | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @commands.Cog.listener() | ||||||
|  |     async def on_message(self, message: discord.Message): | ||||||
|  |         """ | ||||||
|  |         Message hook, to monitor for ++/-- | ||||||
|  |         Also monitors for messages to #karma to autodelete, only Havoc may post in #karma! | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         if message.channel.id == self.karma_chanid and not message.author.id == self.bot.user.id: | ||||||
|  |             """Message to #karma not by Havoc, delete it""" | ||||||
|  |             await message.delete(reason="Messages to #karma are not allowed") | ||||||
|  |             removal_embed: discord.Embed = discord.Embed( | ||||||
|  |                 title="Message Deleted", | ||||||
|  |                 description=f"Your message to **#{message.channel.name}** has been automatically deleted.\n**Reason**: Messages to this channel by users is not allowed." | ||||||
|  |             ) | ||||||
|  |             return await message.author.send(embed=removal_embed) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |         if message.author.id == self.bot.user.id: # Bots own message | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         if not message.guild.id in [1145182936002482196, 1228740575235149855]: # Not a valid guild for cmd | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         message_content: str = message.content.strip() | ||||||
|  |         mentions: list = regex.findall(self.mention_regex, message_content) | ||||||
|  |          | ||||||
|  |         for mention in mentions: | ||||||
|  |             try: | ||||||
|  |                 logging.debug("Mention: %s", mention) | ||||||
|  |                 mentioned_uid: int = int(mention[1]) | ||||||
|  |                 friendly_flag: int = int(mention[2]) | ||||||
|  |                 guild: discord.Guild = self.bot.get_guild(message.guild.id) | ||||||
|  |                 guild_member: discord.Member = guild.get_member(mentioned_uid) | ||||||
|  |                 display: str = guild_member.display_name | ||||||
|  |                 message_content: str = message_content.replace(mention[0], display) | ||||||
|  |                 logging.debug("New message: %s", message_content) | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |  | ||||||
|  |         message_content: str = discord.utils.escape_markdown(message_content) | ||||||
|  |  | ||||||
|  |         karma_regex: list[str] = regex.findall(self.karma_regex, message_content.strip()) | ||||||
|  |         if not karma_regex:  # Not a request to adjust karma | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         flooding: bool = not await self.util.check_cooldown(message.author.id) | ||||||
|  |         exempt_uids: list[int] = [1172340700663255091, 992437729927376996]  | ||||||
|  |         if flooding and not message.author.id in exempt_uids: | ||||||
|  |             return await message.add_reaction(emoji="❗") | ||||||
|  |          | ||||||
|  |         processed_keywords_lc: list[str] = [] | ||||||
|  |  | ||||||
|  |         logging.debug("Matched: %s", karma_regex) | ||||||
|  |          | ||||||
|  |         for matched_keyword in karma_regex: | ||||||
|  |             if len(matched_keyword) == 4: | ||||||
|  |                 (keyword, friendly_flag, _, __) = matched_keyword | ||||||
|  |             else: | ||||||
|  |                 (keyword, friendly_flag) = matched_keyword  | ||||||
|  |             now: int = int(time.time()) | ||||||
|  |  | ||||||
|  |             flag: int = None | ||||||
|  |             match friendly_flag: | ||||||
|  |                 case "++": | ||||||
|  |                     flag: int = 0 | ||||||
|  |                 case "--": | ||||||
|  |                     flag: int = 1 | ||||||
|  |                 case _: | ||||||
|  |                     logging.info("Unknown flag %s", flag) | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |             if keyword.lower() in processed_keywords_lc: | ||||||
|  |                 continue | ||||||
|  |              | ||||||
|  |             processed_keywords_lc.append(keyword.lower()) | ||||||
|  |  | ||||||
|  |             self.util.timers[message.author.id] = now | ||||||
|  |  | ||||||
|  |             updated: bool = await self.util.update_karma(message.author.display_name, | ||||||
|  |                                                          message.author.id, keyword, flag) | ||||||
|  |             if updated: | ||||||
|  |                 return await message.add_reaction(emoji="👍") | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def karma(self, ctx, *, keyword: str | None = None) -> None: | ||||||
|  |         """With no arguments, top 10 karma is provided; a keyword can also be provided to lookup.""" | ||||||
|  |         try: | ||||||
|  |             if not keyword: | ||||||
|  |                 top_10_embed: discord.Embed = await self.util.get_top_embed() | ||||||
|  |                 return await ctx.respond(embed=top_10_embed) | ||||||
|  |              | ||||||
|  |             keyword: str = discord.utils.escape_markdown(keyword) | ||||||
|  |              | ||||||
|  |             mentions: list[str] = regex.findall(self.mention_regex_no_flag, keyword) | ||||||
|  |  | ||||||
|  |             for mention in mentions: | ||||||
|  |                 try: | ||||||
|  |                     mentioned_uid = int(mention[1]) | ||||||
|  |                     guild = self.bot.get_guild(ctx.guild.id) | ||||||
|  |                     guild_member = guild.get_member(mentioned_uid) | ||||||
|  |                     display = guild_member.display_name | ||||||
|  |                     keyword = keyword.replace(mention[0], display) | ||||||
|  |                 except: | ||||||
|  |                     traceback.print_exc() | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |             score: int = await self.util.get_karma(keyword) | ||||||
|  |             description: str = f"**{keyword}** has a karma of *{score}*" | ||||||
|  |             if isinstance(score, dict) and score.get('err'): | ||||||
|  |                 description: str = f"*{score.get('errorText')}*" | ||||||
|  |             embed: discord.Embed = discord.Embed(title=f"Karma for {keyword}", | ||||||
|  |                                                  description=description) | ||||||
|  |             return await ctx.respond(embed=embed) | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {str(e)}") | ||||||
|  |             traceback.print_exc() | ||||||
|  |      | ||||||
|  |     def cog_unload(self) -> None: | ||||||
|  |         try: | ||||||
|  |             self.update_karma_chan.cancel() | ||||||
|  |         except: | ||||||
|  |             """Safe to ignore""" | ||||||
|  |             pass | ||||||
|  |          | ||||||
|  |                               | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Karma(bot)) | ||||||
							
								
								
									
										250
									
								
								cogs/lovehate.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								cogs/lovehate.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,250 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=broad-exception-caught | ||||||
|  |  | ||||||
|  | import traceback | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | from typing import Any, Optional | ||||||
|  | import discord | ||||||
|  | import aiosqlite as sqlite3 | ||||||
|  | from discord.ext import bridge, commands | ||||||
|  | from util.lovehate_db import DB | ||||||
|  | from constructors import LoveHateException | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LoveHate(commands.Cog): | ||||||
|  |     """LoveHate Cog for Havoc""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         self.bot: discord.Bot = bot | ||||||
|  |         self.db = DB(self.bot) | ||||||
|  |  | ||||||
|  |     def join_with_and(self, items: list) -> str: | ||||||
|  |         """ | ||||||
|  |         Join list with and added before last item | ||||||
|  |         Args: | ||||||
|  |             items (list) | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         if len(items) > 1: | ||||||
|  |             return ', '.join(items[:-1]) + ' and ' + items[-1] | ||||||
|  |         return items[0] if items else ''         | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def loves(self, ctx, user: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         If keyword isn't provided, returns the things YOU love; specify a user to find what THEY love. | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             user (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not user: | ||||||
|  |                 loves: list[tuple] = await self.db.get_lovehates(user=ctx.author.display_name, loves=True) | ||||||
|  |                 if not loves: | ||||||
|  |                     return await ctx.respond("You don't seem to love anything...") | ||||||
|  |                  | ||||||
|  |                 out_loves: list = [] | ||||||
|  |                 for love in loves: | ||||||
|  |                     (love,) = love | ||||||
|  |                     out_loves.append(love) | ||||||
|  |      | ||||||
|  |                 out_loves_str: str = self.join_with_and(out_loves) | ||||||
|  |                 return await ctx.respond(f"{ctx.author.mention} loves {out_loves_str}") | ||||||
|  |  | ||||||
|  |             loves: list[tuple] = await self.db.get_lovehates(user=user.strip(), loves=True) | ||||||
|  |             if not loves: | ||||||
|  |                 return await ctx.respond(f"{user} doesn't seem to love anything...")             | ||||||
|  |                | ||||||
|  |             out_loves_str: str = self.join_with_and(out_loves) | ||||||
|  |             return await ctx.respond(f"{user} loves {out_loves_str}") | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"Error: {str(e)}") | ||||||
|  |          | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def wholoves(self, ctx, *, thing: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         Check who loves <thing> | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             thing (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not thing: | ||||||
|  |                 thing: str = ctx.author.display_name | ||||||
|  |             if discord.utils.raw_mentions(thing): | ||||||
|  |                 # There are mentions | ||||||
|  |                 thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention | ||||||
|  |                 thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name | ||||||
|  |              | ||||||
|  |             who_loves: list[tuple] = await self.db.get_wholovehates(thing=thing, | ||||||
|  |                                                     loves=True) | ||||||
|  |             if not who_loves: | ||||||
|  |                 return await ctx.respond(f"I couldn't find anyone who loves {thing}...") | ||||||
|  |              | ||||||
|  |             out_wholoves: list = [] | ||||||
|  |             for lover in who_loves: | ||||||
|  |                 (lover,) = lover | ||||||
|  |                 out_wholoves.append(str(lover)) | ||||||
|  |  | ||||||
|  |             optional_s: str = "s" if len(out_wholoves) == 1 else "" | ||||||
|  |              | ||||||
|  |             out_wholoves_str: str = self.join_with_and(out_wholoves) | ||||||
|  |  | ||||||
|  |             return await ctx.respond(f"{out_wholoves_str} love{optional_s} {thing}") | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"Error: {str(e)}") | ||||||
|  |      | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def whohates(self, ctx, *, thing: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         Check who hates <thing> | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             thing (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not thing: | ||||||
|  |                 thing: str = ctx.author.display_name | ||||||
|  |             if discord.utils.raw_mentions(thing): | ||||||
|  |                 # There are mentions | ||||||
|  |                 thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention | ||||||
|  |                 thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name         | ||||||
|  |  | ||||||
|  |             who_hates: list[tuple] = await self.db.get_wholovehates(thing=thing, | ||||||
|  |                                                     hates=True) | ||||||
|  |             if not who_hates: | ||||||
|  |                 return await ctx.respond(f"I couldn't find anyone who hates {thing}...") | ||||||
|  |              | ||||||
|  |             out_whohates: list = [] | ||||||
|  |             for hater in who_hates: | ||||||
|  |                 (hater,) = hater | ||||||
|  |                 out_whohates.append(str(hater)) | ||||||
|  |  | ||||||
|  |             optional_s: str = "s" if len(out_whohates) == 1 else "" | ||||||
|  |              | ||||||
|  |             out_whohates_str: str = self.join_with_and(out_whohates) | ||||||
|  |  | ||||||
|  |             return await ctx.respond(f"{out_whohates_str} hate{optional_s} {thing}") | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"Error: {str(e)}") | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def dontcare(self, ctx, thing: str) -> None: | ||||||
|  |         """ | ||||||
|  |         Make me forget your opinion on <thing> | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             thing (str) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             stop_caring: str = await self.db.update(ctx.author.display_name, | ||||||
|  |                                                thing, 0) | ||||||
|  |             return await ctx.respond(stop_caring)             | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {str(e)}") | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     async def hates(self, ctx, user: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         If keyword isn't provided, returns the things YOU hate; specify a user to find what THEY hate. | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             user (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not user: | ||||||
|  |                 hates: list[tuple] = await self.db.get_lovehates(user=ctx.author.display_name, | ||||||
|  |                                                                  hates=True) | ||||||
|  |                 if not hates: | ||||||
|  |                     return await ctx.respond("You don't seem to hate anything...") | ||||||
|  |                  | ||||||
|  |                 out_hates: list = [] | ||||||
|  |                 for hated_thing in hates: | ||||||
|  |                     (hated_thing,) = hated_thing | ||||||
|  |                     out_hates.append(str(hated_thing)) | ||||||
|  |                  | ||||||
|  |                 out_hates_str: str = self.join_with_and(out_hates) | ||||||
|  |                  | ||||||
|  |                 return await ctx.respond(f"{ctx.author.mention} hates {out_hates_str}") | ||||||
|  |  | ||||||
|  |             hates: list[tuple] = await self.db.get_lovehates(user=user.strip(), hates=True) | ||||||
|  |             if not hates: | ||||||
|  |                 return await ctx.respond(f"{user} doesn't seem to hate anything...")             | ||||||
|  |              | ||||||
|  |             out_hates_str: str = self.join_with_and(hates) | ||||||
|  |  | ||||||
|  |             return await ctx.respond(f"{user} hates {out_hates_str}") | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {str(e)}") | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['sarcastichate']) | ||||||
|  |     async def love(self, ctx, *, thing: str) -> None: | ||||||
|  |         """ | ||||||
|  |         Love <thing> | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             thing (str) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if discord.utils.raw_mentions(thing): | ||||||
|  |                 # There are mentions | ||||||
|  |                 thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention | ||||||
|  |                 thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name | ||||||
|  |  | ||||||
|  |             love: str = await self.db.update(ctx.author.display_name, | ||||||
|  |                                                thing, 1) | ||||||
|  |             return await ctx.respond(love)             | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {str(e)}") | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['sarcasticlove']) | ||||||
|  |     async def hate(self, ctx, *, thing: str) -> None: | ||||||
|  |         """ | ||||||
|  |         Hate <thing> | ||||||
|  |         Args: | ||||||
|  |             ctx (Any) | ||||||
|  |             thing (str) | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if discord.utils.raw_mentions(thing): | ||||||
|  |                 # There are mentions | ||||||
|  |                 thing_id: int = discord.utils.raw_mentions(thing)[0] # First mention | ||||||
|  |                 thing: str = self.bot.get_guild(ctx.guild.id).get_member(thing_id).display_name | ||||||
|  |             hate: str = await self.db.update(ctx.author.display_name, | ||||||
|  |                                                thing, -1) | ||||||
|  |             return await ctx.respond(hate)             | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {str(e)}") | ||||||
|  |             traceback.print_exc() | ||||||
|  |      | ||||||
|  |     def cog_unload(self) -> None: | ||||||
|  |         # not needed currently | ||||||
|  |         pass | ||||||
|  |          | ||||||
|  |                               | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(LoveHate(bot)) | ||||||
							
								
								
									
										458
									
								
								cogs/meme.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										458
									
								
								cogs/meme.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,458 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import traceback | ||||||
|  | import json | ||||||
|  | import io | ||||||
|  | import asyncio | ||||||
|  | import random | ||||||
|  | from typing import LiteralString, Optional | ||||||
|  | import logging | ||||||
|  | import textwrap | ||||||
|  | import regex | ||||||
|  | import requests | ||||||
|  | import discord | ||||||
|  | from aiohttp import ClientSession | ||||||
|  | from discord.ext import bridge, commands, tasks | ||||||
|  | from jesusmemes import JesusMemeGenerator | ||||||
|  | import scrapers.reddit_scrape as memeg | ||||||
|  | import scrapers.explosm_scrape as explosmg | ||||||
|  | import scrapers.xkcd_scrape as xkcdg | ||||||
|  | import scrapers.smbc_scrape as smbcg | ||||||
|  | import scrapers.qc_scrape as qcg | ||||||
|  | import scrapers.dinosaur_scrape as dinog | ||||||
|  | import scrapers.onion_scrape as oniong | ||||||
|  | import scrapers.thn_scrape as thng | ||||||
|  | import constants | ||||||
|  | # pylint: disable=global-statement, bare-except, invalid-name, line-too-long | ||||||
|  |  | ||||||
|  | meme_choices = [] | ||||||
|  | BOT_CHANIDS = [] | ||||||
|  |  | ||||||
|  | class Helper: | ||||||
|  |     """Meme Helper""" | ||||||
|  |     def load_meme_choices(self) -> None: | ||||||
|  |         """Load Available Meme Templates from JSON File""" | ||||||
|  |         global meme_choices | ||||||
|  |          | ||||||
|  |         memes_file: str|LiteralString = os.path.join(os.path.dirname(__file__), "memes.json") | ||||||
|  |         with open(memes_file, 'r', encoding='utf-8') as f: | ||||||
|  |             meme_choices = json.loads(f.read()) | ||||||
|  |  | ||||||
|  | class MemeView(discord.ui.View): | ||||||
|  |     """Meme Selection discord.ui.View""" | ||||||
|  |     helper = Helper() | ||||||
|  |     helper.load_meme_choices() | ||||||
|  |     @discord.ui.select( | ||||||
|  |         placeholder = "Choose a Meme!", | ||||||
|  |         min_values = 1, | ||||||
|  |         max_values = 1, | ||||||
|  |         options = [ | ||||||
|  |             discord.SelectOption( | ||||||
|  |                 label=meme_label | ||||||
|  |             ) for meme_label in meme_choices[0:24] | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  |     async def select_callback(self, select: discord.ui.Select, | ||||||
|  |                               interaction: discord.Interaction) -> None: | ||||||
|  |         """Meme Selection Callback""" | ||||||
|  |         modal: discord.ui.Modal = MemeModal(meme=select.values[0], title="Meme Selected") | ||||||
|  |         await interaction.response.send_modal(modal) | ||||||
|  |  | ||||||
|  | class MemeModal(discord.ui.Modal): | ||||||
|  |     """Meme Creation discord.ui.Modal""" | ||||||
|  |     def __init__(self, *args, meme: Optional[str] = None, **kwargs) -> None: | ||||||
|  |         super().__init__(*args, **kwargs) | ||||||
|  |         self.selected_meme: str = meme | ||||||
|  |         self.meme_generator = JesusMemeGenerator() | ||||||
|  |         self.TEXT_LIMIT: int = 80 | ||||||
|  |          | ||||||
|  |         self.add_item(discord.ui.InputText(label="Top Text", | ||||||
|  |                                            style=discord.InputTextStyle.singleline)) | ||||||
|  |         self.add_item(discord.ui.InputText(label="Bottom Text", | ||||||
|  |                                            style=discord.InputTextStyle.singleline)) | ||||||
|  |          | ||||||
|  |     async def callback(self, interaction: discord.Interaction) -> None: | ||||||
|  |         selected_meme: str = self.selected_meme | ||||||
|  |         meme_top_line: str = self.children[0].value.strip() | ||||||
|  |         meme_bottom_line: str = self.children[1].value.strip() | ||||||
|  |         if len(meme_top_line) > self.TEXT_LIMIT or len(meme_bottom_line) > self.TEXT_LIMIT: | ||||||
|  |             await interaction.response.send_message("ERR: Text is limited to 80 characters for each the top and bottom lines.") | ||||||
|  |             return | ||||||
|  |          | ||||||
|  |         meme_link: str = await self.meme_generator.create_meme(top_line=meme_top_line, bottom_line=meme_bottom_line, meme=selected_meme) | ||||||
|  |              | ||||||
|  |         embed: discord.Embed = discord.Embed(title="Generated Meme") | ||||||
|  |         embed.set_image(url=meme_link) | ||||||
|  |         embed.add_field(name="Meme", value=self.selected_meme, inline=True) | ||||||
|  |         await interaction.response.send_message(embeds=[embed]) | ||||||
|  |         return | ||||||
|  |  | ||||||
|  | class Meme(commands.Cog): | ||||||
|  |     """Meme Cog for Havoc""" | ||||||
|  |  | ||||||
|  |     def __init__(self, bot) -> None: | ||||||
|  |         self.bot: discord.Bot = bot | ||||||
|  |         self.meme_choices: list = [] | ||||||
|  |         self.meme_counter: int = 0 | ||||||
|  |         self.THREADS: dict[dict[list]] = { | ||||||
|  |             # Format: Guild1: [ChanId : [Webhook, ThreadId], Guild2: [ChanId : [Webhook, ThreadId] | ||||||
|  |             'comic_explosm': { | ||||||
|  |                 1298729744216359055: [constants.EXPLOSM_WEBHOOK, 1299165855493390367], | ||||||
|  |                 1306414795049926676: [constants.EXPLOSM_WEBHOOK2, 1306416492304138364], | ||||||
|  |             }, | ||||||
|  |             'comic_xkcd': { | ||||||
|  |                 1298729744216359055: [constants.XKCD_WEBHOOK, 1299165928755433483],  | ||||||
|  |                 1306414795049926676: [constants.XKCD_WEBHOOK2, 1306416681991798854], | ||||||
|  |             }, | ||||||
|  |             'comic_smbc': { | ||||||
|  |                 1298729744216359055: [constants.SMBC_WEBHOOK, 1299166071038808104], | ||||||
|  |                 1306414795049926676: [constants.SMBC_WEBHOOK2, 1306416842511745024], | ||||||
|  |             }, | ||||||
|  |             'comic_qc': { | ||||||
|  |                 1298729744216359055: [constants.QC_WEBHOOK, 1299392115364593674],  | ||||||
|  |                 1306414795049926676: [constants.QC_WEBHOOK2, 1306417084774744114], | ||||||
|  |             }, | ||||||
|  |             'comic_dino': { | ||||||
|  |                 1298729744216359055: [constants.DINO_WEBHOOK, 1299771918886506557],  | ||||||
|  |                 1306414795049926676: [constants.DINO_WEBHOOK2, 1306417286713704548], | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         self.NO_THREAD_WEBHOOKS: dict[list] = { | ||||||
|  |             'theonion': [constants.ONION_WEBHOOK, constants.ONION_WEBHOOK2], | ||||||
|  |             'thn': [constants.THN_WEBHOOK], | ||||||
|  |             'memes': [constants.MEME_WEBHOOK1, constants.MEME_WEBHOOK2], | ||||||
|  |         } | ||||||
|  |          | ||||||
|  |         global BOT_CHANIDS | ||||||
|  |         BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit | ||||||
|  |  | ||||||
|  |         self.meme_stream_loop.start() | ||||||
|  |         self.explosm_loop.start() | ||||||
|  |  | ||||||
|  |     def is_spamchan() -> bool: # pylint: disable=no-method-argument | ||||||
|  |         """Check if channel is spamchan""" | ||||||
|  |         def predicate(ctx): | ||||||
|  |             try: | ||||||
|  |                 if not ctx.channel.id in BOT_CHANIDS: | ||||||
|  |                     logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS) | ||||||
|  |                 return ctx.channel.id in BOT_CHANIDS | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |                 return False | ||||||
|  |         return commands.check(predicate) | ||||||
|  |      | ||||||
|  |      | ||||||
|  |      | ||||||
|  |     async def do_autos(self, only_comics: Optional[bool] = False) -> None: | ||||||
|  |         """ | ||||||
|  |         Run Auto Posters | ||||||
|  |         Args: | ||||||
|  |             only_comics (Optional[bool]): default False | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             meme_grabber = memeg.MemeGrabber() | ||||||
|  |             explosm_grabber = explosmg.ExplosmGrabber() | ||||||
|  |             xkcd_grabber = xkcdg.XKCDGrabber() | ||||||
|  |             smbc_grabber = smbcg.SMBCGrabber() | ||||||
|  |             qc_grabber = qcg.QCGrabber() | ||||||
|  |             dino_grabber = dinog.DinosaurGrabber() | ||||||
|  |             onion_grabber = oniong.OnionGrabber() | ||||||
|  |             thn_grabber = thng.THNGrabber() | ||||||
|  |             explosm_comics = xkcd_comics = smbc_comics = qc_comics = dino_comics\ | ||||||
|  |                 = onions = thns = [] | ||||||
|  |             memes: list[tuple] = await meme_grabber.get() | ||||||
|  |             try: | ||||||
|  |                 try: | ||||||
|  |                     explosm_comics: list[tuple] = await explosm_grabber.get() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     xkcd_comics: list[tuple] = await xkcd_grabber.get() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     smbc_comics: list[tuple] = await smbc_grabber.get() | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     qc_comics: list[tuple] = await qc_grabber.get() | ||||||
|  |                     print(f"QC: {qc_comics}") | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     dino_comics: list[tuple] = await dino_grabber.get() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     logging.debug("Dino failed: %s", str(e)) | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     onions: list[tuple] = await onion_grabber.get() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     logging.debug("Onion failed: %s", str(e)) | ||||||
|  |                     pass | ||||||
|  |                 try: | ||||||
|  |                     thns: list[tuple] = await thn_grabber.get() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     logging.debug("THNs failed: %s", str(e)) | ||||||
|  |                     pass | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |             agents: list[str] = constants.HTTP_UA_LIST | ||||||
|  |             headers: dict = { | ||||||
|  |                 'User-Agent': random.choice(agents) | ||||||
|  |             } | ||||||
|  |             if not only_comics: | ||||||
|  |                 try: | ||||||
|  |                     for meme in memes: | ||||||
|  |                         (meme_id, meme_title, meme_url) = meme # pylint: disable=unused-variable | ||||||
|  |                         request = requests.get(meme_url, stream=True, timeout=(5, 30), headers=headers) | ||||||
|  |                         if not request.status_code == 200: | ||||||
|  |                             continue | ||||||
|  |                         meme_content: bytes = request.raw.read() | ||||||
|  |                         for meme_hook in self.NO_THREAD_WEBHOOKS.get('memes'): | ||||||
|  |                             meme_image: io.BytesIO = io.BytesIO(meme_content) | ||||||
|  |                             ext: str = meme_url.split(".")[-1]\ | ||||||
|  |                                 .split("?")[0].split("&")[0] | ||||||
|  |                             async with ClientSession() as session: | ||||||
|  |                                 webhook: discord.Webhook = discord.Webhook.from_url(meme_hook, | ||||||
|  |                                                                 session=session) | ||||||
|  |                                 await webhook.send(file=discord.File(meme_image, | ||||||
|  |                                                                     filename=f'img.{ext}'), username="r/memes") | ||||||
|  |                         await asyncio.sleep(2) | ||||||
|  |                 except: | ||||||
|  |                     pass | ||||||
|  |             try: | ||||||
|  |                 for comic in explosm_comics: | ||||||
|  |                     (comic_title, comic_url) = comic | ||||||
|  |                     comic_title: str = discord.utils.escape_markdown(comic_title) | ||||||
|  |                     comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) | ||||||
|  |                     comic_request.raise_for_status() | ||||||
|  |                     comic_content: bytes = comic_request.raw.read() | ||||||
|  |                     ext: str = comic_url.split(".")[-1]\ | ||||||
|  |                         .split("?")[0].split("&")[0] | ||||||
|  |  | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for chanid, _hook in self.THREADS.get('comic_explosm').items(): | ||||||
|  |                             comic_image: io.BytesIO = io.BytesIO(comic_content) | ||||||
|  |                             channel: int = chanid | ||||||
|  |                             hook_uri: str = _hook[0] | ||||||
|  |                             thread_id: int = _hook[1] | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                                         session=session) | ||||||
|  |                             thread: discord.Thread = self.bot.get_channel(channel)\ | ||||||
|  |                                 .get_thread(thread_id) | ||||||
|  |                             await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), | ||||||
|  |                                                     username="Cyanide & Happiness", thread=thread) | ||||||
|  |                         await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             try: | ||||||
|  |                 for comic in xkcd_comics: | ||||||
|  |                     (comic_title, comic_url) = comic | ||||||
|  |                     comic_title: str = discord.utils.escape_markdown(comic_title)                 | ||||||
|  |                     comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) | ||||||
|  |                     comic_request.raise_for_status() | ||||||
|  |                     comic_content: bytes = comic_request.raw.read() | ||||||
|  |                     comic_image: io.BytesIO = io.BytesIO(comic_request.raw.read()) | ||||||
|  |                     ext: str = comic_url.split(".")[-1]\ | ||||||
|  |                         .split("?")[0].split("&")[0] | ||||||
|  |  | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for chanid, _hook in self.THREADS.get('comic_xkcd').items(): | ||||||
|  |                             comic_image: io.BytesIO = io.BytesIO(comic_content) | ||||||
|  |                             channel: int = chanid | ||||||
|  |                             hook_uri: str = _hook[0] | ||||||
|  |                             thread_id: int = _hook[1] | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                                         session=session) | ||||||
|  |                             thread: discord.Thread = self.bot.get_channel(channel)\ | ||||||
|  |                                 .get_thread(thread_id) | ||||||
|  |                             await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), | ||||||
|  |                                                     username="xkcd", thread=thread) | ||||||
|  |                         await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 pass  | ||||||
|  |             try: | ||||||
|  |                 for comic in smbc_comics: | ||||||
|  |                     (comic_title, comic_url) = comic | ||||||
|  |                     comic_title: str = discord.utils.escape_markdown(comic_title)                 | ||||||
|  |                     comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) | ||||||
|  |                     comic_request.raise_for_status() | ||||||
|  |                     comic_content: bytes = comic_request.raw.read() | ||||||
|  |                     ext: str = comic_url.split(".")[-1]\ | ||||||
|  |                         .split("?")[0].split("&")[0] | ||||||
|  |  | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for chanid, _hook in self.THREADS.get('comic_smbc').items(): | ||||||
|  |                             comic_image: io.BytesIO = io.BytesIO(comic_content) | ||||||
|  |                             channel: int = chanid | ||||||
|  |                             hook_uri: str = _hook[0] | ||||||
|  |                             thread_id: int = _hook[1] | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                                         session=session) | ||||||
|  |                             thread = self.bot.get_channel(channel)\ | ||||||
|  |                                 .get_thread(thread_id) | ||||||
|  |                             await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), | ||||||
|  |                                                     username="SMBC", thread=thread) | ||||||
|  |                         await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 pass  | ||||||
|  |             try: | ||||||
|  |                 for comic in qc_comics: | ||||||
|  |                     logging.debug("Trying QC...") | ||||||
|  |                     (comic_title, comic_url) = comic | ||||||
|  |                     comic_title: str = discord.utils.escape_markdown(comic_title)                 | ||||||
|  |                     comic_url: str = regex.sub(r'^http://ww\.', 'http://www.', | ||||||
|  |                                                comic_url) | ||||||
|  |                     comic_url: str = regex.sub(r'\.pmg$', '.png', | ||||||
|  |                                                comic_url) | ||||||
|  |                     comic_request = requests.get(comic_url, stream=True, | ||||||
|  |                                                  timeout=(5, 20), headers=headers) | ||||||
|  |                     comic_request.raise_for_status() | ||||||
|  |                     comic_content: bytes = comic_request.raw.read() | ||||||
|  |                     ext: str = comic_url.split(".")[-1]\ | ||||||
|  |                         .split("?")[0].split("&")[0] | ||||||
|  |                          | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for chanid, _hook in self.THREADS.get('comic_qc').items(): | ||||||
|  |                             comic_image: io.BytesIO = io.BytesIO(comic_content) | ||||||
|  |                             channel: int = chanid | ||||||
|  |                             hook_uri: str = _hook[0] | ||||||
|  |                             thread_id: int = _hook[1] | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                                             session=session) | ||||||
|  |                             thread: discord.Thread = self.bot.get_channel(channel)\ | ||||||
|  |                                 .get_thread(thread_id) | ||||||
|  |                             await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), | ||||||
|  |                                                         username="Questionable Content", thread=thread) | ||||||
|  |                     await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |                 pass                                                                  | ||||||
|  |             try: | ||||||
|  |                 for comic in dino_comics: | ||||||
|  |                     (comic_title, comic_url) = comic | ||||||
|  |                     comic_title: str = discord.utils.escape_markdown(comic_title)                 | ||||||
|  |                     comic_request = requests.get(comic_url, stream=True, timeout=(5, 20), headers=headers) | ||||||
|  |                     comic_request.raise_for_status() | ||||||
|  |                     comic_content: bytes = comic_request.raw.read() | ||||||
|  |                     ext = comic_url.split(".")[-1]\ | ||||||
|  |                         .split("?")[0].split("&")[0] | ||||||
|  |  | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for chanid, _hook in self.THREADS.get('comic_dino').items(): | ||||||
|  |                             comic_image: io.BytesIO = io.BytesIO(comic_content) | ||||||
|  |                             channel: int = chanid | ||||||
|  |                             hook_uri: str = _hook[0] | ||||||
|  |                             thread_id: int = _hook[1]                         | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                                             session=session) | ||||||
|  |                             thread: discord.Thread = self.bot.get_channel(channel)\ | ||||||
|  |                                 .get_thread(thread_id) | ||||||
|  |                             await webhook.send(f"**{comic_title}**", file=discord.File(comic_image, filename=f'img.{ext}'), | ||||||
|  |                                                         username="Dinosaur Comics", thread=thread) | ||||||
|  |                         await asyncio.sleep(2)                                                                      | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             try: | ||||||
|  |                 for onion in onions: | ||||||
|  |                     (onion_title, onion_description, onion_link, onion_video) = onion | ||||||
|  |                     onion_description: list[str] = textwrap.wrap(text=onion_description,  | ||||||
|  |                                                         width=860, max_lines=1)[0] | ||||||
|  |                     embed: discord.Embed = discord.Embed(title=onion_title) | ||||||
|  |                     embed.add_field(name="Content", value=f"{onion_description[0:960]}\n-# {onion_link}") | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for hook in self.NO_THREAD_WEBHOOKS.get('theonion'): | ||||||
|  |                             hook_uri: str = hook | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                 session=session) | ||||||
|  |                             await webhook.send(embed=embed, username="The Onion") | ||||||
|  |                             if onion_video: | ||||||
|  |                                 await webhook.send(f"^ video: {onion_video}") | ||||||
|  |                         await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |             try: | ||||||
|  |                 for thn in thns: | ||||||
|  |                     logging.debug("Trying thn...") | ||||||
|  |                     (thn_title, thn_description, thn_link, thn_pubdate, thn_video) = thn | ||||||
|  |                     thn_description: list[str] = textwrap.wrap(text=thn_description,  | ||||||
|  |                                                     width=860, max_lines=1)[0] | ||||||
|  |                     embed: discord.Embed = discord.Embed(title=thn_title) | ||||||
|  |                     embed.add_field(name="Content", value=f"{thn_description[0:960]}\n-# {thn_link}") | ||||||
|  |                     embed.add_field(name="Published", value=thn_pubdate, inline=False) | ||||||
|  |                     async with ClientSession() as session: | ||||||
|  |                         for hook in self.NO_THREAD_WEBHOOKS.get('thn'): | ||||||
|  |                             hook_uri: str = hook | ||||||
|  |                             webhook: discord.Webhook = discord.Webhook.from_url(hook_uri, | ||||||
|  |                                 session=session) | ||||||
|  |                             await webhook.send(embed=embed, username="The Hacker News") | ||||||
|  |                             if thn_video: | ||||||
|  |                                 await webhook.send(f"^ video: {thn_video}") | ||||||
|  |                     await asyncio.sleep(2) | ||||||
|  |             except: | ||||||
|  |                 pass | ||||||
|  |         except: | ||||||
|  |             # await self.bot.get_channel(self.MEMESTREAM_CHANID).send(f"FUCK, MY MEEMER!  YOU DENTED MY MEEMER!") | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     @tasks.loop(hours=12.0) | ||||||
|  |     async def meme_stream_loop(self) -> None: | ||||||
|  |         """Meme Stream Loop (r/memes)""" | ||||||
|  |         try: | ||||||
|  |             await asyncio.sleep(10) # Try to ensure we are ready first | ||||||
|  |             self.meme_counter += 1 | ||||||
|  |             if self.meme_counter == 1: | ||||||
|  |                 return await self.do_autos(only_comics=True) # Skip first iteration! | ||||||
|  |              | ||||||
|  |             await self.do_autos() | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc()      | ||||||
|  |  | ||||||
|  |     @tasks.loop(hours=0.5) | ||||||
|  |     async def explosm_loop(self) -> None: | ||||||
|  |         """Comic Loop""" | ||||||
|  |         try: | ||||||
|  |             await asyncio.sleep(10) # Try to ensure we are ready first | ||||||
|  |             await self.do_autos(only_comics=True) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc()                       | ||||||
|  |              | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     @is_spamchan() # pylint: disable=too-many-function-args | ||||||
|  |     async def meme(self, ctx) -> None: | ||||||
|  |         """Create Meme""" | ||||||
|  |         await ctx.respond(view=MemeView()) | ||||||
|  |          | ||||||
|  |     @bridge.bridge_command(hidden=True) | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def domemestream(self, ctx) -> None: | ||||||
|  |         """Run Meme Stream Auto Post""" | ||||||
|  |         try: | ||||||
|  |             await ctx.respond("Trying!", ephemeral=True) | ||||||
|  |             await self.do_autos() | ||||||
|  |         except: | ||||||
|  |             await ctx.respond("Fuck! :(", ephemeral=True) | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(hidden=True) | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def doexplosm(self, ctx) -> None: | ||||||
|  |         """Run Comic Auto Posters""" | ||||||
|  |         try: | ||||||
|  |             await ctx.respond("Trying!", ephemeral=True) | ||||||
|  |             await self.do_autos(only_comics=True) | ||||||
|  |         except: | ||||||
|  |             await ctx.respond("Fuck! :(", ephemeral=True) | ||||||
|  |             traceback.print_exc() | ||||||
|  |      | ||||||
|  |     def cog_unload(self) -> None: | ||||||
|  |         self.meme_stream_loop.cancel() | ||||||
|  |         self.explosm_loop.cancel() | ||||||
|  |          | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Meme(bot)) | ||||||
							
								
								
									
										14
									
								
								cogs/memes.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								cogs/memes.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | [ | ||||||
|  |     "10-Guy", | ||||||
|  |     "1990s-First-World-Problems", | ||||||
|  |     "Bill-Nye-The-Science-Guy", | ||||||
|  |     "Dont-You-Squidward", | ||||||
|  |     "Grumpy-Cat", | ||||||
|  |     "I-Was-Told-There-Would-Be", | ||||||
|  |     "I-Know-Fuck-Me-Right", | ||||||
|  |     "Putin", | ||||||
|  |     "Michael-Jackson-Popcorn", | ||||||
|  |     "Peter-Griffin-News", | ||||||
|  |     "Shut-Up-And-Take-My-Money-Fry", | ||||||
|  |     "Relaxed-Office-Guy" | ||||||
|  | ] | ||||||
							
								
								
									
										1253
									
								
								cogs/misc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1253
									
								
								cogs/misc.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										388
									
								
								cogs/misc_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										388
									
								
								cogs/misc_util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,388 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import logging | ||||||
|  | import traceback | ||||||
|  | import random | ||||||
|  | import datetime | ||||||
|  | import pytz | ||||||
|  | from typing import Optional, LiteralString | ||||||
|  | import regex | ||||||
|  | import aiosqlite as sqlite3 | ||||||
|  | from aiohttp import ClientSession, ClientTimeout | ||||||
|  | from bohancompliment import ComplimentGenerator | ||||||
|  | from discord import Embed | ||||||
|  |  | ||||||
|  | class Util: | ||||||
|  |     """Misc Utility""" | ||||||
|  |     def __init__(self) -> None: | ||||||
|  |         self.URL_URBANDICTIONARY: str = "http://api.urbandictionary.com/v0/define" | ||||||
|  |         self.URL_INSULTAPI: str = "https://insult.mattbas.org/api/insult" | ||||||
|  |         self.COMPLIMENT_GENERATOR = ComplimentGenerator() | ||||||
|  |         self.dbs: dict[str|LiteralString] = { | ||||||
|  |             'whisky': os.path.join("/usr/local/share", | ||||||
|  |                                    "sqlite_dbs", "whiskey.db"), | ||||||
|  |             'drinks': os.path.join("/usr/local/share", | ||||||
|  |                                    "sqlite_dbs", "cocktails.db"), | ||||||
|  |             'strains': os.path.join("/usr/local/share", | ||||||
|  |                                     "sqlite_dbs", "strains.db"), | ||||||
|  |             'qajoke': os.path.join("/usr/local/share", | ||||||
|  |                                    "sqlite_dbs", "qajoke.db"), | ||||||
|  |             'rjokes': os.path.join("/usr/local/share", | ||||||
|  |                                    "sqlite_dbs", "rjokes.db"), | ||||||
|  |             'randmsg': os.path.join("/usr/local/share", | ||||||
|  |                                     "sqlite_dbs", "randmsg.db"), | ||||||
|  |             'stats': os.path.join("/usr/local/share", | ||||||
|  |                                   "sqlite_dbs", "havoc_stats.db"), | ||||||
|  |             'cookies': os.path.join("/usr/local/share", | ||||||
|  |                                     "sqlite_dbs", "cookies.db"), | ||||||
|  |         } | ||||||
|  |         self.COFFEES: list = ['a cup of french-pressed coffee', 'a cup of cold brew', 'a cup of flash brew', | ||||||
|  |                         'a cup of Turkish coffee', 'a cup of Moka', 'an espresso', | ||||||
|  |                         'a cup of Nescafe coffee', | ||||||
|  |                         'an iced coffee', 'a Frappé', 'a freddo cappuccino', | ||||||
|  |                         'a cup of Chock full o\'Nuts', 'a cup of Folgers', 'a cup of Lavazza', | ||||||
|  |                         'a cup of Maxwell House', 'a cup of Moccona', 'a cup of Mr. Brown Coffee', | ||||||
|  |                         'a cup of affogato al caffè', | ||||||
|  |                         'a cup of Caffè Medici', 'a cup of Café Touba', | ||||||
|  |                         'a double-double', 'an indian filter coffee', 'a cup of pocillo', | ||||||
|  |                         'a cup of caffè americano', 'a cup of caffè lungo', 'a latte', 'a manilo', | ||||||
|  |                         'a flat white', 'a cup of café cubano', 'a cup of caffè crema', | ||||||
|  |                         'a cup of cafe zorro', 'an espresso roberto', 'an espresso romano', | ||||||
|  |                         'an espresso sara', 'a guillermo', 'a ristretto', 'a cup of melya', | ||||||
|  |                         'a cup of caffè marocchino', 'a cup of café miel', 'a cup of café de olla', | ||||||
|  |                         'a Mazagran', 'a Palazzo', 'an ice shot']     | ||||||
|  |         self.LAST_5_COFFEES: list = [] | ||||||
|  |          | ||||||
|  |  | ||||||
|  |     def tdTuple(self, td:datetime.timedelta) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Create TimeDelta Tuple | ||||||
|  |         Args: | ||||||
|  |             td (datetime.timedelta) | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         def _t(t, n): | ||||||
|  |             if t < n:  | ||||||
|  |                 return (t, 0) | ||||||
|  |             v = t//n | ||||||
|  |             return (t -  (v * n), v) | ||||||
|  |         (s, h) = _t(td.seconds, 3600) | ||||||
|  |         (s, m) = _t(s, 60)     | ||||||
|  |         (mics, mils) = _t(td.microseconds, 1000) | ||||||
|  |         return (td.days, h, m, s, mics, mils) | ||||||
|  |  | ||||||
|  |     def sqlite_dict_factory(self, cursor: sqlite3.Cursor, row: sqlite3.Row) -> dict: | ||||||
|  |         """ | ||||||
|  |         SQLite Dict Factory for Rows Returned | ||||||
|  |         Args: | ||||||
|  |             cursor (sqlite3.Row) | ||||||
|  |             row (sqlite3.Row) | ||||||
|  |         Returns: | ||||||
|  |             dict | ||||||
|  |         """ | ||||||
|  |         fields = [column[0] for column in cursor.description] | ||||||
|  |         return { key: value for key, value in zip(fields, row) } | ||||||
|  |      | ||||||
|  |  | ||||||
|  |     async def get_counter(self, counter: Optional[str] = None) -> dict: | ||||||
|  |         """ | ||||||
|  |         Get Counter | ||||||
|  |         Args: | ||||||
|  |             counter (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             dict | ||||||
|  |         """ | ||||||
|  |         async with sqlite3.connect(self.dbs.get('stats'), | ||||||
|  |                                          timeout=3) as db_conn: | ||||||
|  |             db_conn.row_factory = self.sqlite_dict_factory | ||||||
|  |             query: str = "SELECT ? FROM stats LIMIT 1" | ||||||
|  |             if not counter: | ||||||
|  |                 query: str = "SELECT * FROM stats LIMIT 1" | ||||||
|  |             async with await db_conn.execute(query, (counter,) if counter else None) as db_cursor: | ||||||
|  |                 result: dict = await db_cursor.fetchone() | ||||||
|  |                 return result | ||||||
|  |              | ||||||
|  |     async def get_stats_embed(self) -> Embed: | ||||||
|  |         """ | ||||||
|  |         Get Stats Embed | ||||||
|  |         Returns: | ||||||
|  |             Embed | ||||||
|  |         """ | ||||||
|  |         counters: dict = await self.get_counter() | ||||||
|  |         embed: Embed = Embed(title="Stats") | ||||||
|  |         counter_message: str = "" | ||||||
|  |         counters_sorted: dict = dict(sorted(counters.items(),  | ||||||
|  |                           key=lambda item: item[1], reverse=True)) | ||||||
|  |         for counter, value in counters_sorted.items(): | ||||||
|  |             counter: str = regex.sub(r'_', ' ', | ||||||
|  |                                      counter.strip()).title() | ||||||
|  |             counter_message += f"- {value} {counter}\n" | ||||||
|  |         embed.description = counter_message.strip() | ||||||
|  |         return embed | ||||||
|  |              | ||||||
|  |     async def increment_counter(self, counter: str) -> bool: | ||||||
|  |         """ | ||||||
|  |         Increment Counter | ||||||
|  |         Args: | ||||||
|  |             counter (str) | ||||||
|  |         Returns: | ||||||
|  |             bool | ||||||
|  |         """ | ||||||
|  |         async with sqlite3.connect(self.dbs.get('stats'), | ||||||
|  |                                          timeout=3) as db_conn: | ||||||
|  |             async with await db_conn.execute(f"UPDATE stats SET {counter} = {counter} + 1") as db_cursor: | ||||||
|  |                 if db_cursor.rowcount < 0: | ||||||
|  |                     logging.critical("[karma::increment_counter] Fail! %s", db_cursor.rowcount) | ||||||
|  |                     return False | ||||||
|  |                 await db_conn.commit() | ||||||
|  |                 return True | ||||||
|  |  | ||||||
|  |     async def get_ud_def(self, term: Optional[str] = None) -> tuple[str, str]: | ||||||
|  |         """ | ||||||
|  |         Get Definition from UD | ||||||
|  |         Args: | ||||||
|  |             term (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             tuple[str, str] | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             async with ClientSession() as session: | ||||||
|  |                 async with await session.get(self.URL_URBANDICTIONARY, | ||||||
|  |                                              params={ | ||||||
|  |                                                  "term": term, | ||||||
|  |                                                  }, | ||||||
|  |                                              headers = { | ||||||
|  |                                                  'content-type': 'application/json; charset=utf-8', | ||||||
|  |                                                  }, timeout=ClientTimeout(connect=5, sock_read=5)) as request: | ||||||
|  |                     logging.debug("UD returned: %s", | ||||||
|  |                                   await request.text()) | ||||||
|  |                     data: dict = await request.json() | ||||||
|  |                     if "list" in data: | ||||||
|  |                         definitions: list[dict] = data["list"] | ||||||
|  |                         if definitions: | ||||||
|  |                             definition: dict = definitions[0] | ||||||
|  |                             definition_word: str = definition.get("word", "N/A") | ||||||
|  |                             definition_text: str = regex.sub(r'(\r|\n|\r\n)', ' ', definition["definition"].strip()) | ||||||
|  |                             return (definition_word, definition_text)  # Tuple: Returned word, returned definition | ||||||
|  |                         else: | ||||||
|  |                             return (term, "Not found!") | ||||||
|  |                     else: | ||||||
|  |                         return (term, "Error retrieving data from Urban Dictionary") | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return (term, f"ERR: {str(e)}") | ||||||
|  |  | ||||||
|  |     async def get_insult(self, recipient: str) -> str: | ||||||
|  |         """ | ||||||
|  |         Get Insult | ||||||
|  |         Args: | ||||||
|  |             recipient (str) | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         async with ClientSession() as session: | ||||||
|  |             async with await session.get(f"{self.URL_INSULTAPI}?who={recipient}") as request: | ||||||
|  |                 request.raise_for_status() | ||||||
|  |                 return await request.text() | ||||||
|  |              | ||||||
|  |  | ||||||
|  |     async def get_compliment(self, subject: str, | ||||||
|  |                              language: Optional[str] = None) -> str: | ||||||
|  |         """ | ||||||
|  |         Get Compliment | ||||||
|  |         Args: | ||||||
|  |             subject (str) | ||||||
|  |             language (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         if not language: | ||||||
|  |             return self.COMPLIMENT_GENERATOR.compliment(subject) | ||||||
|  |         return self.COMPLIMENT_GENERATOR.compliment_in_language(subject, language) | ||||||
|  |  | ||||||
|  |     async def get_whisky(self) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Get Whisky | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         whisky_db: str|LiteralString = self.dbs.get('whisky') | ||||||
|  |         db_conn = await sqlite3.connect(database=whisky_db, timeout=2) | ||||||
|  |         db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1" | ||||||
|  |         db_cursor: sqlite3.Cursor = await db_conn.execute(db_query) | ||||||
|  |         db_result: tuple = await db_cursor.fetchone() | ||||||
|  |          | ||||||
|  |         (name, category, description) = db_result | ||||||
|  |         name: str = regex.sub(r'(^\p{White_Space}|\r|\n)', '', | ||||||
|  |                          regex.sub(r'\p{White_Space}{2,}', ' ', | ||||||
|  |                                    name.strip())) | ||||||
|  |         category: str = regex.sub(r'(^\p{White_Space}|\r|\n)', '', | ||||||
|  |                              regex.sub(r'\p{White_Space}{2,}', ' ', | ||||||
|  |                                        category.strip())) | ||||||
|  |         description: str = regex.sub(r'(^\p{White_Space}|\r|\n)', '', | ||||||
|  |                                 regex.sub(r'\p{White_Space}{2,}', ' ', | ||||||
|  |                                           description.strip())) | ||||||
|  |         return (name, category, description) | ||||||
|  |          | ||||||
|  |     async def get_drink(self) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Get Drink | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         drinks_db: str|LiteralString = self.dbs.get('drinks') | ||||||
|  |         db_conn = await sqlite3.connect(database=drinks_db, timeout=2) | ||||||
|  |         db_query: str = "SELECT name, ingredients FROM cocktails ORDER BY random() LIMIT 1" | ||||||
|  |         db_cursor: sqlite3.Cursor = await db_conn.execute(db_query) | ||||||
|  |         db_result: tuple = await db_cursor.fetchone() | ||||||
|  |          | ||||||
|  |         (name, ingredients) = db_result | ||||||
|  |         name = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', name.strip())) | ||||||
|  |         ingredients = regex.sub(r'(^\p{White_Space}|\r|\n)', '', regex.sub(r'\p{White_Space}{2,}', ' ', ingredients.strip())) | ||||||
|  |         ingredients = regex.sub(r'\*', '\u2731', ingredients.strip()) | ||||||
|  |          | ||||||
|  |         return (name, ingredients) | ||||||
|  |          | ||||||
|  |     async def get_strain(self, strain: Optional[str] = None) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Get Strain | ||||||
|  |         Args: | ||||||
|  |             strain (Optional[str]) | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         strains_db: str|LiteralString = self.dbs.get('strains') | ||||||
|  |         db_conn = await sqlite3.connect(database=strains_db, timeout=2) | ||||||
|  |         db_params: Optional[tuple] = None | ||||||
|  |         if not strain: | ||||||
|  |             db_query: str = "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1" | ||||||
|  |         else: | ||||||
|  |             db_query: str = "SELECT name, description FROM strains_w_desc WHERE name LIKE ?" | ||||||
|  |             db_params: tuple = (f"%{strain.strip()}%",)  | ||||||
|  |              | ||||||
|  |         db_cursor: sqlite3.Cursor = await db_conn.execute(db_query, db_params) | ||||||
|  |         db_result: tuple = await db_cursor.fetchone() | ||||||
|  |          | ||||||
|  |         return db_result | ||||||
|  |          | ||||||
|  |     async def get_qajoke(self) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Get QA Joke | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         qajoke_db: str|LiteralString = self.dbs.get('qajoke') | ||||||
|  |         async with sqlite3.connect(database=qajoke_db, timeout=2) as db: | ||||||
|  |             async with await db.execute('SELECT question, answer FROM jokes ORDER BY RANDOM() LIMIT 1') as cursor: | ||||||
|  |                 (question, answer) = await cursor.fetchone() | ||||||
|  |                 return (question, answer) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     async def get_rjoke(self) -> tuple: | ||||||
|  |         """ | ||||||
|  |         Get r/joke Joke | ||||||
|  |         Returns: | ||||||
|  |             tuple | ||||||
|  |         """ | ||||||
|  |         rjokes_db: str|LiteralString = self.dbs.get('rjokes') | ||||||
|  |         async with sqlite3.connect(database=rjokes_db, timeout=2) as db: | ||||||
|  |             async with await db.execute('SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1') as cursor: | ||||||
|  |                 (title, body, score) = await cursor.fetchone() | ||||||
|  |                 return (title, body, score) | ||||||
|  |         return None         | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     async def get_random_fact(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Get Random Fact | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             facts_api_url: str = "https://uselessfacts.jsph.pl/api/v2/facts/random" | ||||||
|  |             facts_backup_url: str = "https://cnichols1734.pythonanywhere.com/facts/random" | ||||||
|  |             async with ClientSession() as client: | ||||||
|  |                 try: | ||||||
|  |                     async with await client.get(facts_api_url, | ||||||
|  |                                                 timeout=ClientTimeout(connect=5, sock_read=5)) as request: | ||||||
|  |                         json: dict = await request.json() | ||||||
|  |                         fact: str = json.get('text') | ||||||
|  |                         if not fact: | ||||||
|  |                             raise BaseException("RandFact Src 1 Failed") | ||||||
|  |                         return fact | ||||||
|  |                 except: | ||||||
|  |                     async with await client.get(facts_backup_url, | ||||||
|  |                                                 timeout=ClientTimeout(connect=5, sock_read=5)) as request: | ||||||
|  |                         json: dict = await request.json() | ||||||
|  |                         fact: str = json.get('fact') | ||||||
|  |                         return fact | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return f"Failed to get a random fact :( [{str(e)}]" | ||||||
|  |          | ||||||
|  |     async def get_cookie(self) -> dict: | ||||||
|  |         """ | ||||||
|  |         Get Cookie | ||||||
|  |         Returns: | ||||||
|  |             dict | ||||||
|  |         """ | ||||||
|  |         async with sqlite3.connect(self.dbs.get('cookies'), timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute("SELECT name, origin, image_url FROM cookies ORDER BY RANDOM() LIMIT 1") as db_cursor: | ||||||
|  |                 (name, origin, image_url) = await db_cursor.fetchone() | ||||||
|  |                 return { | ||||||
|  |                     'name': name, | ||||||
|  |                     'origin': origin, | ||||||
|  |                     'image_url': image_url | ||||||
|  |                 } | ||||||
|  |                  | ||||||
|  |  | ||||||
|  |     def get_coffee(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Get Coffee | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             randomCoffee: str = random.choice(self.COFFEES) | ||||||
|  |             if self.LAST_5_COFFEES and randomCoffee in self.LAST_5_COFFEES: | ||||||
|  |                 return self.get_coffee() # Recurse | ||||||
|  |             if len(self.LAST_5_COFFEES) >= 5: | ||||||
|  |                 self.LAST_5_COFFEES.pop() # Store no more than 5 of the last served coffees | ||||||
|  |             self.LAST_5_COFFEES.append(randomCoffee) | ||||||
|  |             return randomCoffee | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |     def get_days_to_xmas(self) -> tuple[int|float]: | ||||||
|  |         """ | ||||||
|  |         Get # of Days until Xmas | ||||||
|  |         Returns: | ||||||
|  |             tuple[int|float] | ||||||
|  |         """ | ||||||
|  |         today: datetime = datetime.datetime.now(tz=pytz.UTC) | ||||||
|  |         xmas: datetime = datetime.datetime( | ||||||
|  |             year=today.year, | ||||||
|  |             month=12, | ||||||
|  |             day=25, | ||||||
|  |             tzinfo=pytz.UTC, | ||||||
|  |         ) | ||||||
|  |         td: datetime.timedelta = (xmas - today) # pylint: disable=superfluous-parens | ||||||
|  |         days, hours, minutes, seconds, us, ms = self.tdTuple(td) | ||||||
|  |          | ||||||
|  |         return (days, hours, minutes, seconds, ms, us) | ||||||
|  |      | ||||||
|  |     async def get_randmsg(self) -> str: | ||||||
|  |         """ | ||||||
|  |         Get Random Message from randmsg.db | ||||||
|  |         Returns: | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         randmsg_db = self.dbs.get('randmsg') | ||||||
|  |         async with sqlite3.connect(database=randmsg_db, | ||||||
|  |                                          timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute("SELECT msg FROM msgs ORDER BY RANDOM() LIMIT 1") as db_cursor: | ||||||
|  |                 (result,) = await db_cursor.fetchone() | ||||||
|  |                 return result | ||||||
							
								
								
									
										285
									
								
								cogs/owner.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										285
									
								
								cogs/owner.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,285 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=bare-except, broad-exception-caught | ||||||
|  |  | ||||||
|  | import io | ||||||
|  | import random | ||||||
|  | import asyncio | ||||||
|  | import traceback | ||||||
|  | from typing import Optional | ||||||
|  | import discord | ||||||
|  | import requests | ||||||
|  | from discord.ext import bridge, commands | ||||||
|  | import util | ||||||
|  |  | ||||||
|  | class Owner(commands.Cog): | ||||||
|  |     """Owner Cog for Havoc""" | ||||||
|  |     def __init__(self, bot) -> None: | ||||||
|  |         self.bot: discord.Bot = bot | ||||||
|  |         self.former_roles_store: dict = {} | ||||||
|  |         self._temperature: int = random.randrange(20, 30) | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(guild_ids=[1145182936002482196]) | ||||||
|  |     async def temperature(self, ctx, temp: Optional[int|str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         Set Temperature | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             temperature (Optional[int|str]): New temperature | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         if not temp: | ||||||
|  |             return await ctx.respond(f"The current temperature is: {self._temperature} °C") | ||||||
|  |          | ||||||
|  |         if not self.bot.is_owner(ctx.author): | ||||||
|  |             return await ctx.respond("I am afraid I can't let you do that.") | ||||||
|  |         try: | ||||||
|  |             _temperature: int = int(temp) | ||||||
|  |         except: | ||||||
|  |             return await ctx.respond("Invalid input")                 | ||||||
|  |         if _temperature < -15: | ||||||
|  |             return await ctx.respond("Too cold! (-15°C minimum)") | ||||||
|  |         elif _temperature > 35: | ||||||
|  |             return await ctx.respond("Too hot! (35°C maximum)") | ||||||
|  |         self._temperature: int = _temperature | ||||||
|  |         return await ctx.respond(f"As per your request, I have adjusted the temperature to {_temperature} °C.") | ||||||
|  |          | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def reload(self, ctx) -> None: | ||||||
|  |         """ | ||||||
|  |         Reload Cogs | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         self.bot.load_exts(False) | ||||||
|  |         await ctx.respond("Reloaded!", ephemeral=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def say(self, ctx, *, | ||||||
|  |                   parameters: str) -> None: | ||||||
|  |         """ | ||||||
|  |         Make me say something in a channel | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             parameters (str): Channel <space> Message | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         parameters: list[str] = parameters.split(" ") | ||||||
|  |          | ||||||
|  |         if not len(parameters) > 1: | ||||||
|  |             return await ctx.respond("**Error**: Incorrect command usage; required: <chan> <msg>", ephemeral=True) | ||||||
|  |          | ||||||
|  |         channel: str = parameters[0] | ||||||
|  |         channel_mentions: list[str] = discord.utils.raw_channel_mentions(channel) | ||||||
|  |         if channel_mentions: | ||||||
|  |             channel: str = str(channel_mentions[0]) | ||||||
|  |         msg: str = " ".join(parameters[1:]) | ||||||
|  |         sent = await util.discord_helpers.send_message(self.bot, channel=channel, | ||||||
|  |                                                        message=msg) | ||||||
|  |         if not sent: | ||||||
|  |             return await ctx.respond("**Failed.**", ephemeral=True) | ||||||
|  |         return await ctx.respond("**Done.**", ephemeral=True) | ||||||
|  |          | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def chgstatus(self, ctx, *, | ||||||
|  |                         status: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         Change bots status | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             status (Optional[str]): The new status to set | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         if not status: | ||||||
|  |             return await ctx.respond("ERR: No status provided to change to!", | ||||||
|  |                                      ephemeral=True) | ||||||
|  |          | ||||||
|  |         await self.bot.change_presence(status=discord.Status.online, | ||||||
|  |                                        activity=discord.CustomActivity(name=status.strip())) | ||||||
|  |         await ctx.respond("Done!", ephemeral=True) | ||||||
|  |          | ||||||
|  |     @commands.message_command(name="Remove Messages Starting Here") | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def purge(self, ctx, message: discord.Message) -> None: | ||||||
|  |         """ | ||||||
|  |         Purge Messages | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             message (discord.Message): Discord message | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             await ctx.channel.purge(after=message, | ||||||
|  |                                     bulk=True, | ||||||
|  |                                     limit=900000, | ||||||
|  |                                     reason=f"Purge initiated by {ctx.author.display_name}") | ||||||
|  |             await message.delete(reason=f"Purge initiated by {ctx.author.display_name}") | ||||||
|  |             await ctx.respond("**Done!**") | ||||||
|  |             # Wait 3 seconds, then delete interaction | ||||||
|  |             await asyncio.sleep(3) | ||||||
|  |             interaction = await ctx.interaction.original_response() | ||||||
|  |             await interaction.delete() | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"**ERR: {str(e)}**") | ||||||
|  |              | ||||||
|  |     @commands.message_command(name="Move to Memes") | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def movememe(self, ctx, message: discord.Message) -> None: | ||||||
|  |         """ | ||||||
|  |         Move to Memes | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             message (discord.Message): Discord message | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             memes_channel: discord.TextChannel = ctx.guild.get_channel(1147229098544988261) | ||||||
|  |             message_content: str  = message.content | ||||||
|  |             message_author: str = message.author.display_name | ||||||
|  |             message_channel: str = message.channel.name | ||||||
|  |             _file: Optional[discord.File] = None | ||||||
|  |             if message.attachments: | ||||||
|  |                 for item in message.attachments: | ||||||
|  |                     if item.url and len(item.url) >= 20: | ||||||
|  |                         image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, | ||||||
|  |                                                                     timeout=20).raw.read()) | ||||||
|  |                         ext: str = item.url.split(".")[-1]\ | ||||||
|  |                             .split("?")[0].split("&")[0] | ||||||
|  |                         _file: discord.File = discord.File(image, filename=f'img.{ext}') | ||||||
|  |             await memes_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...*\n**{message_author}:** {message_content}", file=_file)             | ||||||
|  |             await message.delete() | ||||||
|  |             await ctx.respond("OK!", ephemeral=True) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond("Failed! :(", ephemeral=True) | ||||||
|  |                        | ||||||
|  |     @commands.message_command(name="Move to Drugs") | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def movedrugs(self, ctx, message: discord.Message) -> None: | ||||||
|  |         """ | ||||||
|  |         Move to Drugs | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             message (discord.Message): Discord message | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             drugs_channel: discord.TextChannel = ctx.guild.get_channel(1172247451047034910) | ||||||
|  |             message_content: str = message.content | ||||||
|  |             message_author: str = message.author.display_name | ||||||
|  |             message_channel: str = message.channel.name | ||||||
|  |             _file: Optional[discord.File] = None | ||||||
|  |             if message.attachments: | ||||||
|  |                 for item in message.attachments: | ||||||
|  |                     if item.url and len(item.url) >= 20: | ||||||
|  |                         image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, | ||||||
|  |                                                                     timeout=20).raw.read()) | ||||||
|  |                         ext: str = item.url.split(".")[-1]\ | ||||||
|  |                             .split("?")[0].split("&")[0] | ||||||
|  |                         _file: discord.File = discord.File(image, filename=f'img.{ext}') | ||||||
|  |             await drugs_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})...\ | ||||||
|  |                 *\n**{message_author}:** {message_content}", file=_file) | ||||||
|  |             await message.delete() | ||||||
|  |             await ctx.respond("OK!", ephemeral=True) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond("Failed! :(", ephemeral=True) | ||||||
|  |              | ||||||
|  |     @commands.message_command(name="Move to fun-house") | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def movefunhouse(self, ctx, message: discord.Message) -> None: | ||||||
|  |         """ | ||||||
|  |         Move to fun-house | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             message (discord.Message): Discord message | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             funhouse_channel: discord.TextChannel = ctx.guild.get_channel(1213160512364478607) | ||||||
|  |             message_content: str = message.content | ||||||
|  |             message_author: str = message.author.display_name | ||||||
|  |             message_channel: str = message.channel.name | ||||||
|  |             _file: Optional[discord.File] = None | ||||||
|  |             if message.attachments: | ||||||
|  |                 for item in message.attachments: | ||||||
|  |                     if item.url and len(item.url) >= 20: | ||||||
|  |                         image: io.BytesIO = io.BytesIO(requests.get(item.url, stream=True, | ||||||
|  |                                                                     timeout=20).raw.read()) | ||||||
|  |                         ext: str = item.url.split(".")[-1]\ | ||||||
|  |                             .split("?")[0].split("&")[0] | ||||||
|  |                         _file: discord.File = discord.File(image, filename=f'img.{ext}') | ||||||
|  |             await funhouse_channel.send(f"*Performing bureaucratic duties (this didn't belong in #{message_channel})\ | ||||||
|  |                 ...*\n**{message_author}:** {message_content}")             | ||||||
|  |             await message.delete() | ||||||
|  |             await ctx.respond("OK!", ephemeral=True) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond("Failed! :(", ephemeral=True) | ||||||
|  |              | ||||||
|  |     @commands.user_command(name="Einsperren!", guild_ids=[145182936002482196]) | ||||||
|  |     @commands.is_owner() | ||||||
|  |     async def einsperren(self, ctx, member: discord.Member) -> None: | ||||||
|  |         """ | ||||||
|  |         Einsperren! | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             member (discord.Member): Discord member | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not ctx.guild.id == 1145182936002482196: | ||||||
|  |                 return # Not home server! | ||||||
|  |             audit_reason: str = f"Einsperren von {ctx.user.display_name}" | ||||||
|  |             member: discord.Member = ctx.guild.get_member(member.id) | ||||||
|  |             member_display: str = member.display_name | ||||||
|  |             einsperren_role: discord.Role = ctx.guild.get_role(1235415059300093973) if ctx.guild.id != 1145182936002482196\ | ||||||
|  |                 else ctx.guild.get_role(1235406301614309386) | ||||||
|  |             member_roles: list[discord.Role] = [role for role in member.roles if not role.name == "@everyone"] | ||||||
|  |             member_role_names: list[str] = [str(role.name).lower() for role in member_roles] | ||||||
|  |             opers_chan: discord.TextChannel = ctx.guild.get_channel(1181416083287187546) | ||||||
|  |             if not "einsperren" in member_role_names: | ||||||
|  |                 try: | ||||||
|  |                     if member.id in self.former_roles_store: | ||||||
|  |                         self.former_roles_store.pop(member.id) | ||||||
|  |                     self.former_roles_store[member.id] = member.roles | ||||||
|  |                 except: | ||||||
|  |                     pass # Safe to ignore | ||||||
|  |                 try: | ||||||
|  |                     await member.edit(roles=[einsperren_role], reason=audit_reason) | ||||||
|  |                     await ctx.respond(f"Gesendet {member_display} an einsperren.", ephemeral=True) | ||||||
|  |                     await opers_chan.send(f"@everyone: {ctx.user.display_name} gesendet {member_display} an einsperren.") | ||||||
|  |                 except: | ||||||
|  |                     traceback.print_exc() | ||||||
|  |                     return await ctx.respond("GOTTVERDAMMT!!", ephemeral=True) | ||||||
|  |                 self.former_roles_store[member.id] = member.roles | ||||||
|  |                  | ||||||
|  |             if not member.id in self.former_roles_store: | ||||||
|  |                 await member.edit(roles=[])  # No roles | ||||||
|  |             else: | ||||||
|  |                 former_roles: list[discord.Role] = self.former_roles_store.get(member.id) | ||||||
|  |                 await member.edit(roles=former_roles, reason=f"De-{audit_reason}") | ||||||
|  |              | ||||||
|  |             await ctx.respond(f"{member_display} wurde von der Einsperre befreit.", ephemeral=True) | ||||||
|  |             await opers_chan.send(f"{member_display} wurde von {ctx.user.display_name} aus der Einsperre befreit.") | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"ERR: {str(e)}", ephemeral=True) | ||||||
|  |  | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Owner(bot)) | ||||||
							
								
								
									
										320
									
								
								cogs/quote.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								cogs/quote.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,320 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=bare-except, broad-exception-caught | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Quote cog for Havoc | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import traceback | ||||||
|  | import time | ||||||
|  | import os | ||||||
|  | import datetime | ||||||
|  | import asyncio | ||||||
|  | import discord | ||||||
|  | import aiosqlite as sqlite3 | ||||||
|  | from discord.ext import bridge, commands | ||||||
|  |  | ||||||
|  | class DB: | ||||||
|  |     """DB Utility for Quote Cog""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         self.bot = bot | ||||||
|  |         self.db_path = os.path.join("/", "usr", "local", "share", | ||||||
|  |                                     "sqlite_dbs", "quotes.db") | ||||||
|  |         self.hp_chanid = 1157529874936909934 | ||||||
|  |          | ||||||
|  |  | ||||||
|  |     async def get_quote_count(self): | ||||||
|  |         """Get Quote Count""" | ||||||
|  |         async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute("SELECT COUNT (*) FROM quotes") as db_cursor: | ||||||
|  |                 result = await db_cursor.fetchone() | ||||||
|  |                 return result[-1] | ||||||
|  |              | ||||||
|  |     async def remove_quote(self, quote_id: int): | ||||||
|  |         """Remove Quote from DB""" | ||||||
|  |         try: | ||||||
|  |             async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |                 async with await db_conn.execute("DELETE FROM quotes WHERE id = ?", (quote_id,)) as _: | ||||||
|  |                     await db_conn.commit() | ||||||
|  |                     return True | ||||||
|  |         except: | ||||||
|  |             await self.bot.get_channel(self.hp_chanid).send(traceback.format_exc()) | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |     async def add_quote(self, message_id: int, channel_id: int, | ||||||
|  |                         quoted_member_id: int, | ||||||
|  |                         message_time: int, | ||||||
|  |                         quoter_friendly: str, | ||||||
|  |                         quoted_friendly: str, | ||||||
|  |                         channel_friendly: str, | ||||||
|  |                         message_content: str, | ||||||
|  |                         ): | ||||||
|  |         """Add Quote to DB""" | ||||||
|  |         params = ( | ||||||
|  |             quoter_friendly, | ||||||
|  |             int(time.time()), | ||||||
|  |             quoted_friendly, | ||||||
|  |             quoted_member_id, | ||||||
|  |             channel_friendly, | ||||||
|  |             channel_id, | ||||||
|  |             message_id, | ||||||
|  |             message_time, | ||||||
|  |             quoter_friendly, | ||||||
|  |             message_content, | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |                 # pylint: disable=line-too-long | ||||||
|  |                 db_conn.row_factory = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)]) | ||||||
|  |                 async with await db_conn.execute("INSERT INTO quotes (added_by, added_at, quoted_user_display, quoted_user_memberid, quoted_channel_display, quoted_channel_id, quoted_message_id, quoted_message_time, added_by_friendly, quoted_message) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", | ||||||
|  |                                            params) as _: # pylint: enable=line-too-long | ||||||
|  |                     await db_conn.commit() | ||||||
|  |                     return True | ||||||
|  |         except: | ||||||
|  |             return traceback.format_exc() | ||||||
|  |          | ||||||
|  |     async def fetch_quote(self, random: bool = False, quoteid: int = None, added_by: str = None, quoted_user: str = None, content: str = None): | ||||||
|  |         """Fetch Quote from DB""" | ||||||
|  |         try: | ||||||
|  |             query_head = "SELECT id, added_by_friendly, added_at, quoted_user_display, quoted_channel_display, quoted_message_time, quoted_message FROM quotes" | ||||||
|  |             query = "" | ||||||
|  |             params = None | ||||||
|  |  | ||||||
|  |             if random: | ||||||
|  |                 query = f"{query_head} ORDER BY RANDOM() LIMIT 1" | ||||||
|  |             elif quoteid: | ||||||
|  |                 query = f"{query_head} WHERE id = ? LIMIT 1" | ||||||
|  |                 params = (quoteid,) | ||||||
|  |             elif added_by: | ||||||
|  |                 query = f"{query_head} WHERE added_by_friendly LIKE ? ORDER BY RANDOM() LIMIT 5" | ||||||
|  |                 params = (f"%{added_by}%",) | ||||||
|  |             elif quoted_user: | ||||||
|  |                 query = f"{query_head} WHERE quoted_user_display LIKE ? ORDER BY RANDOM() LIMIT 5" | ||||||
|  |                 params = (f"%{quoted_user}%",) | ||||||
|  |             elif content: | ||||||
|  |                 query = f"{query_head} WHERE quoted_message LIKE ? ORDER BY RANDOM() LIMIT 5" | ||||||
|  |                 params = (f"%{content}%",) | ||||||
|  |  | ||||||
|  |             async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |                 db_conn.row_factory = lambda c, r: dict([(col[0], r[idx]) for idx, col in enumerate(c.description)]) | ||||||
|  |                 async with await db_conn.execute(query, params) as db_cursor: | ||||||
|  |                     results = await db_cursor.fetchall() | ||||||
|  |                     if not results: | ||||||
|  |                         return { | ||||||
|  |                             'err': 'No results for query', | ||||||
|  |                         } | ||||||
|  |                     if random or quoteid: | ||||||
|  |                         chosen = results[-1] | ||||||
|  |                         return { | ||||||
|  |                             str(k): v for k,v in chosen.items() | ||||||
|  |                         } | ||||||
|  |                     else: | ||||||
|  |                         return [ | ||||||
|  |                             { str(k): v for k,v in _.items() } | ||||||
|  |                             for _ in results | ||||||
|  |                         ] | ||||||
|  |         except: | ||||||
|  |             return traceback.format_exc() | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Quote(commands.Cog): | ||||||
|  |     """Quote Cog for Havoc""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         self.bot = bot | ||||||
|  |         self.db = DB(self.bot) | ||||||
|  |  | ||||||
|  |     def is_homeserver(): # pylint: disable=no-method-argument | ||||||
|  |         """Check if channel/interaction is within homeserver""" | ||||||
|  |         def predicate(ctx): | ||||||
|  |             try: | ||||||
|  |                 return ctx.guild.id == 1145182936002482196 | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |                 return False | ||||||
|  |         return commands.check(predicate) | ||||||
|  |  | ||||||
|  |     @commands.message_command(name="Add Quote") | ||||||
|  |     async def add_quote(self, ctx, message: discord.Message): | ||||||
|  |         """Add A Quote""" | ||||||
|  |         hp_chanid = 1157529874936909934 | ||||||
|  |         try: | ||||||
|  |             if message.author.bot: | ||||||
|  |                 return await ctx.respond("Quotes are for real users, not bots.", ephemeral=True) | ||||||
|  |             quoter_friendly = ctx.author.display_name | ||||||
|  |             quoted_message_id = message.id             | ||||||
|  |             quoted_channel_friendly = f'#{ctx.channel.name}' | ||||||
|  |             quoted_channel_id = ctx.channel.id | ||||||
|  |             message_content = message.content | ||||||
|  |             message_author_friendly = message.author.display_name | ||||||
|  |             message_author_id = message.author.id | ||||||
|  |             message_time = int(message.created_at.timestamp()) | ||||||
|  |             message_escaped = discord.utils.escape_mentions(discord.utils.escape_markdown(message_content)).strip() | ||||||
|  |  | ||||||
|  |             if len(message_escaped) < 3: | ||||||
|  |                 return await ctx.respond("**Error**: Message (text content) is not long enough to quote.", ephemeral=True) | ||||||
|  |              | ||||||
|  |             if len(message_escaped) > 512: | ||||||
|  |                 return await ctx.respond("**Error**: Message (text content) is too long to quote.", ephemeral=True) | ||||||
|  |  | ||||||
|  |             result = await self.db.add_quote(message_id=quoted_message_id, | ||||||
|  |                                     channel_id=quoted_channel_id, | ||||||
|  |                                     quoted_member_id=message_author_id, | ||||||
|  |                                     message_time=message_time, | ||||||
|  |                                     quoter_friendly=quoter_friendly, | ||||||
|  |                                     quoted_friendly=message_author_friendly, | ||||||
|  |                                     channel_friendly=quoted_channel_friendly, | ||||||
|  |                                     message_content=message_content) | ||||||
|  |             if not result: | ||||||
|  |                 return await ctx.respond("Failed!", ephemeral=True) | ||||||
|  |             else: | ||||||
|  |                 return await ctx.respond("OK!", ephemeral=True) | ||||||
|  |         except: | ||||||
|  |             await self.bot.get_channel(hp_chanid).send(traceback.format_exc()) | ||||||
|  |  | ||||||
|  |      | ||||||
|  |     @bridge.bridge_command(aliases=['rand']) | ||||||
|  |     @is_homeserver() # pylint: disable=too-many-function-args | ||||||
|  |     async def randquote(self, ctx): | ||||||
|  |         """Get a random quote""" | ||||||
|  |         try: | ||||||
|  |             random_quote = await self.db.fetch_quote(random=True) | ||||||
|  |             if random_quote.get('err'): | ||||||
|  |                 return await ctx.respond("Failed to get a quote") | ||||||
|  |              | ||||||
|  |             quote_id = random_quote.get('id') | ||||||
|  |             quoted_friendly = random_quote.get('quoted_user_display', 'Unknown') | ||||||
|  |             adder_friendly = random_quote.get('added_by_friendly', 'Unknown') | ||||||
|  |             message_time = datetime.datetime.fromtimestamp(random_quote.get('quoted_message_time')) | ||||||
|  |             message_channel = random_quote.get('quoted_channel_display') | ||||||
|  |             quote_added_at = datetime.datetime.fromtimestamp(random_quote.get('added_at')) | ||||||
|  |             quote_content = random_quote.get('quoted_message') | ||||||
|  |  | ||||||
|  |             embed = discord.Embed( | ||||||
|  |                 colour=discord.Colour.orange(), | ||||||
|  |                 title=f"Quote #{quote_id}", | ||||||
|  |             ) | ||||||
|  |             embed.description = f"**{quoted_friendly}:** {quote_content}" | ||||||
|  |             embed.add_field(name="Original Message Time", value=message_time) | ||||||
|  |             embed.add_field(name="Channel", value=message_channel) | ||||||
|  |             embed.add_field(name="Quote ID", value=quote_id) | ||||||
|  |             embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}") | ||||||
|  |  | ||||||
|  |             return await ctx.respond(embed=embed) | ||||||
|  |         except: | ||||||
|  |             error = await ctx.respond(traceback.format_exc()) | ||||||
|  |             await asyncio.sleep(10) | ||||||
|  |             await error.delete() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['qg']) | ||||||
|  |     @is_homeserver() # pylint: disable=too-many-function-args | ||||||
|  |     async def quoteget(self, ctx, quoteid): | ||||||
|  |         """Get a specific quote by ID""" | ||||||
|  |         try: | ||||||
|  |             if not str(quoteid).strip().isnumeric(): | ||||||
|  |                 return await ctx.respond("**Error**: Quote ID must be numeric.") | ||||||
|  |             fetched_quote = await self.db.fetch_quote(quoteid=quoteid) | ||||||
|  |             if fetched_quote.get('err'): | ||||||
|  |                 return await ctx.respond("**Error**: Quote not found") | ||||||
|  |              | ||||||
|  |             quote_id = fetched_quote.get('id') | ||||||
|  |             quoted_friendly = fetched_quote.get('quoted_user_display', 'Unknown') | ||||||
|  |             adder_friendly = fetched_quote.get('added_by_friendly', 'Unknown') | ||||||
|  |             message_time = datetime.datetime.fromtimestamp(fetched_quote.get('quoted_message_time')) | ||||||
|  |             message_channel = fetched_quote.get('quoted_channel_display') | ||||||
|  |             quote_added_at = datetime.datetime.fromtimestamp(fetched_quote.get('added_at')) | ||||||
|  |             quote_content = fetched_quote.get('quoted_message') | ||||||
|  |  | ||||||
|  |             embed = discord.Embed( | ||||||
|  |                 colour=discord.Colour.orange(), | ||||||
|  |                 title=f"Quote #{quote_id}", | ||||||
|  |             ) | ||||||
|  |             embed.description = f"**{quoted_friendly}:** {quote_content}" | ||||||
|  |             embed.add_field(name="Original Message Time", value=message_time) | ||||||
|  |             embed.add_field(name="Channel", value=message_channel) | ||||||
|  |             embed.add_field(name="Quote ID", value=quote_id) | ||||||
|  |             embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}") | ||||||
|  |  | ||||||
|  |             return await ctx.respond(embed=embed) | ||||||
|  |         except: | ||||||
|  |             error = await ctx.respond(traceback.format_exc()) | ||||||
|  |             await asyncio.sleep(10) | ||||||
|  |             await error.delete() | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['qs']) | ||||||
|  |     @is_homeserver() # pylint: disable=too-many-function-args | ||||||
|  |     async def quotesearch(self, ctx, *, content: str): | ||||||
|  |         """Search for a quote (by content)""" | ||||||
|  |         try: | ||||||
|  |             found_quotes = await self.db.fetch_quote(content=content) | ||||||
|  |             if isinstance(found_quotes, dict) and found_quotes.get('err'): | ||||||
|  |                 return await ctx.respond(f"Quote search failed: {found_quotes.get('err')}") | ||||||
|  |              | ||||||
|  |             embeds = [] | ||||||
|  |  | ||||||
|  |             for quote in found_quotes: | ||||||
|  |                 quote_id = quote.get('id') | ||||||
|  |                 quoted_friendly = quote.get('quoted_user_display', 'Unknown') | ||||||
|  |                 adder_friendly = quote.get('added_by_friendly', 'Unknown') | ||||||
|  |                 message_time = datetime.datetime.fromtimestamp(quote.get('quoted_message_time')) | ||||||
|  |                 message_channel = quote.get('quoted_channel_display') | ||||||
|  |                 quote_added_at = datetime.datetime.fromtimestamp(quote.get('added_at')) | ||||||
|  |                 quote_content = quote.get('quoted_message') | ||||||
|  |  | ||||||
|  |                 # await ctx.respond(f"**{quoted_friendly}**: {quote}")ed_friendly = quote.get('quoted_user_display', 'Unknown') | ||||||
|  |                 adder_friendly = quote.get('added_by_friendly', 'Unknown') | ||||||
|  |                 message_time = datetime.datetime.fromtimestamp(quote.get('quoted_message_time')) | ||||||
|  |                 message_channel = quote.get('quoted_channel_display') | ||||||
|  |                 quote_added_at = datetime.datetime.fromtimestamp(quote.get('added_at')) | ||||||
|  |                 quote = quote.get('quoted_message') | ||||||
|  |  | ||||||
|  |                 # await ctx.respond(f"**{quoted_friendly}**: {quote}") | ||||||
|  |                 embed = discord.Embed( | ||||||
|  |                     colour=discord.Colour.orange(), | ||||||
|  |                     title=f"Quote #{quote_id}", | ||||||
|  |                 ) | ||||||
|  |                 embed.description = f"**{quoted_friendly}:** {quote_content}" | ||||||
|  |                 embed.add_field(name="Original Message Time", value=message_time) | ||||||
|  |                 embed.add_field(name="Channel", value=message_channel) | ||||||
|  |                 embed.add_field(name="Quote ID", value=quote_id) | ||||||
|  |                 embed.footer = discord.EmbedFooter(text=f"Added by {adder_friendly} {quote_added_at}") | ||||||
|  |                 embeds.append(embed) | ||||||
|  |  | ||||||
|  |             return await ctx.respond(embeds=embeds) | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {type(e).__name__} - {str(e)}") | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['nq']) | ||||||
|  |     @is_homeserver() # pylint: disable=too-many-function-args | ||||||
|  |     async def nquotes(self, ctx): | ||||||
|  |         """Get # of quotes stored""" | ||||||
|  |         try: | ||||||
|  |             quote_count = await self.db.get_quote_count() | ||||||
|  |             if not quote_count: | ||||||
|  |                 return await ctx.respond("**Error**: No quotes found!") | ||||||
|  |             return await ctx.respond(f"I currently have **{quote_count}** quotes stored.") | ||||||
|  |  | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {type(e).__name__} - {str(e)}") | ||||||
|  |  | ||||||
|  |     @bridge.bridge_command(aliases=['qr']) | ||||||
|  |     @commands.is_owner() | ||||||
|  |     @is_homeserver() # pylint: disable=too-many-function-args | ||||||
|  |     async def quoteremove(self, ctx, quoteid): | ||||||
|  |         """Remove a quote (by id) | ||||||
|  |         Owner only""" | ||||||
|  |         try: | ||||||
|  |             if not str(quoteid).strip().isnumeric(): | ||||||
|  |                 return await ctx.respond("**Error**: Quote ID must be numeric.") | ||||||
|  |             quoteid = int(quoteid) | ||||||
|  |             remove_quote = await self.db.remove_quote(quoteid)  | ||||||
|  |             if not remove_quote: | ||||||
|  |                 return await ctx.respond("**Error**: Failed!", ephemeral=True) | ||||||
|  |             return await ctx.respond("Removed!", ephemeral=True) | ||||||
|  |  | ||||||
|  |         except Exception as e: | ||||||
|  |             await ctx.respond(f"Error: {type(e).__name__} - {str(e)}") | ||||||
|  |  | ||||||
|  | def setup(bot): | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Quote(bot)) | ||||||
							
								
								
									
										108
									
								
								cogs/radio.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								cogs/radio.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=bare-except, broad-exception-caught, invalid-name | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | import traceback | ||||||
|  | from discord.ext import bridge, commands, tasks | ||||||
|  | import discord | ||||||
|  |  | ||||||
|  | class Radio(commands.Cog): | ||||||
|  |     """Radio Cog for Havoc""" | ||||||
|  |     def __init__(self, bot: discord.Bot) -> None: | ||||||
|  |         self.bot: discord.Bot = bot | ||||||
|  |         self.channels: dict[tuple] = { | ||||||
|  |             'sfm': (1145182936002482196, 1221615558492029050), # Tuple: Guild Id, Chan Id | ||||||
|  |         }         | ||||||
|  |         self.STREAM_URL: str = "https://relay.sfm.codey.lol/aces.ogg" | ||||||
|  |          | ||||||
|  |         try: | ||||||
|  |             self.radio_state_loop.cancel() | ||||||
|  |         except Exception as e: | ||||||
|  |             logging.debug("Failed to cancel radio_state_loop: %s",  | ||||||
|  |                           str(e)) | ||||||
|  |       | ||||||
|  |     @commands.Cog.listener() | ||||||
|  |     async def on_ready(self) -> None: | ||||||
|  |         """Run on Bot Ready""" | ||||||
|  |         await self.radio_init() | ||||||
|  |           | ||||||
|  |     def is_radio_chan(): # pylint: disable=no-method-argument | ||||||
|  |         """Check if channel is radio chan""" | ||||||
|  |         def predicate(ctx): | ||||||
|  |             try: | ||||||
|  |                 return ctx.channel.id == 1221615558492029050 | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |                 return False | ||||||
|  |         return commands.check(predicate)      | ||||||
|  |      | ||||||
|  |     @bridge.bridge_command() | ||||||
|  |     @commands.is_owner()     | ||||||
|  |     async def reinitradio(self, ctx) -> None: | ||||||
|  |         """ | ||||||
|  |         Reinitialize serious.FM | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         loop: discord.asyncio.AbstractEventLoop = self.bot.loop | ||||||
|  |         loop.create_task(self.radio_init()) | ||||||
|  |         await ctx.respond("Done!", ephemeral=True) | ||||||
|  |      | ||||||
|  |     async def radio_init(self) -> None: | ||||||
|  |         """ | ||||||
|  |         Init Radio | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             (radio_guild, radio_chan) = self.channels['sfm'] | ||||||
|  |             channel: discord.TextChannel = self.bot.get_guild(radio_guild)\ | ||||||
|  |                 .get_channel(radio_chan)              | ||||||
|  |             if not self.bot.voice_clients: | ||||||
|  |                 await channel.connect() | ||||||
|  |             try: | ||||||
|  |                 try: | ||||||
|  |                     self.radio_state_loop.cancel() | ||||||
|  |                 except Exception as e: | ||||||
|  |                     logging.debug("Failed to cancel radio_state_loop: %s",  | ||||||
|  |                                   str(e)) | ||||||
|  |                 self.radio_state_loop.start() | ||||||
|  |                 logging.info("radio_state_loop task started!") | ||||||
|  |             except: | ||||||
|  |                 logging.critical("Could not start task...") | ||||||
|  |                 traceback.print_exc() | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return | ||||||
|  |      | ||||||
|  |     @tasks.loop(seconds=2.0) | ||||||
|  |     async def radio_state_loop(self) -> None: | ||||||
|  |         """Radio State Loop""" | ||||||
|  |         try: | ||||||
|  |             (radio_guild, radio_chan) = self.channels['sfm'] | ||||||
|  |             try: | ||||||
|  |                 vc: discord.VoiceClient = self.bot.voice_clients[-1] | ||||||
|  |             except: | ||||||
|  |                 logging.debug("No voice client, establishing new VC connection...") | ||||||
|  |                 channel = self.bot.get_guild(radio_guild)\ | ||||||
|  |                     .get_channel(radio_chan) | ||||||
|  |                 await channel.connect() | ||||||
|  |                 vc = self.bot.voice_clients[-1] | ||||||
|  |             if not(vc.is_playing()) or vc.is_paused(): | ||||||
|  |                 logging.info("Detected VC not playing... playing!") | ||||||
|  |                 source = discord.FFmpegOpusAudio(self.STREAM_URL, | ||||||
|  |                                                  before_options="-timeout 3000000") | ||||||
|  |                 vc.play(source, after=lambda e: logging.info("Error: %s", e)\ | ||||||
|  |                     if e else None)             | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |  | ||||||
|  |         # pylint: enable=superfluous-parens | ||||||
|  |          | ||||||
|  |     def cog_unload(self) -> None: | ||||||
|  |         """Run on Cog Unload"""           | ||||||
|  |         self.radio_state_loop.cancel() | ||||||
|  |      | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Radio(bot)) | ||||||
							
								
								
									
										184
									
								
								cogs/sing.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								cogs/sing.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,184 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=bare-except, broad-exception-caught, global-statement, invalid-name | ||||||
|  |  | ||||||
|  | import traceback | ||||||
|  | import logging | ||||||
|  | from typing import Optional, Pattern | ||||||
|  | import urllib | ||||||
|  | import discord | ||||||
|  | import regex | ||||||
|  | from util.sing_util import Utility | ||||||
|  | from discord.ext import bridge, commands | ||||||
|  |  | ||||||
|  | BOT_CHANIDS = [] | ||||||
|  |  | ||||||
|  | class Sing(commands.Cog): | ||||||
|  |     """Sing Cog for Havoc""" | ||||||
|  |     def __init__(self, bot): | ||||||
|  |         self.bot: discord.Bot = bot | ||||||
|  |         self.utility = Utility() | ||||||
|  |         global BOT_CHANIDS | ||||||
|  |         BOT_CHANIDS = self.bot.BOT_CHANIDS # Inherit | ||||||
|  |         self.control_strip_regex: Pattern = regex.compile(r"\x0f|\x1f|\035|\002|\u2064|\x02|(\x03([0-9]{1,2}))|(\x03|\003)(?:\d{1,2}(?:,\d{1,2})?)?", | ||||||
|  |                                                  regex.UNICODE) | ||||||
|  |      | ||||||
|  |     def is_spamchan(): # pylint: disable=no-method-argument | ||||||
|  |         """Check if channel is spam chan""" | ||||||
|  |         def predicate(ctx): | ||||||
|  |             try: | ||||||
|  |                 if not ctx.channel.id in BOT_CHANIDS: | ||||||
|  |                     logging.debug("%s not found in %s", ctx.channel.id, BOT_CHANIDS) | ||||||
|  |                 return ctx.channel.id in BOT_CHANIDS | ||||||
|  |             except: | ||||||
|  |                 traceback.print_exc() | ||||||
|  |                 return False | ||||||
|  |         return commands.check(predicate)         | ||||||
|  |               | ||||||
|  |     @bridge.bridge_command(aliases=['sing'])   | ||||||
|  |     async def s(self, ctx, *, | ||||||
|  |                 song: Optional[str] = None) -> None: | ||||||
|  |         """ | ||||||
|  |         Search for lyrics, format is artist : song.  Also reads activity. | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             song (Optional[str]): Song to search | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             with ctx.channel.typing(): | ||||||
|  |                 interaction: bool = isinstance(ctx, | ||||||
|  |                                                discord.ext.bridge.BridgeApplicationContext) | ||||||
|  |                 activity: Optional[discord.Activity] = None | ||||||
|  |                 if not song: | ||||||
|  |                     if not ctx.author.activities: | ||||||
|  |                         return | ||||||
|  |  | ||||||
|  |                     for _activity in ctx.author.activities: | ||||||
|  |                         if _activity.type == discord.ActivityType.listening: | ||||||
|  |                             activity: discord.Activity = _activity | ||||||
|  |                      | ||||||
|  |                     if not activity: | ||||||
|  |                         return await ctx.respond("**Error**: No song specified, no activity found to read.") | ||||||
|  |  | ||||||
|  |                     if interaction: | ||||||
|  |                         await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord | ||||||
|  |                  | ||||||
|  |                 (search_artist, search_song, search_subsearch) = self.utility.parse_song_input(song, activity) | ||||||
|  |                      | ||||||
|  |                 # await ctx.respond(f"So, {search_song} by {search_artist}?  Subsearch: {search_subsearch} I will try...") # Commented, useful for debugging | ||||||
|  |                 search_result: list[str] = await self.utility.lyric_search(search_artist, search_song, | ||||||
|  |                                                                            search_subsearch) | ||||||
|  |                  | ||||||
|  |                 if len(search_result) == 1: | ||||||
|  |                     return await ctx.respond(search_result[0].strip()) | ||||||
|  |                  | ||||||
|  |                 (search_result_artist, search_result_song, search_result_src, | ||||||
|  |                  search_result_confidence, search_result_time_taken) = search_result[0] #  First index is a tuple | ||||||
|  |                 search_result_wrapped: list[str] = search_result[1] # Second index is the wrapped lyrics | ||||||
|  |                 search_result_wrapped_short: list[str] = search_result[2] # Third is short wrapped lyrics | ||||||
|  |                 if not ctx.channel.id in BOT_CHANIDS: | ||||||
|  |                     search_result_wrapped: list[str] = search_result_wrapped_short # Replace with shortened lyrics for non spamchans | ||||||
|  |                 embeds: list[Optional[discord.Embed]] = [] | ||||||
|  |                 embed_url: str = f"[on codey.lol](https://codey.lol/#{urllib.parse.quote(search_artist)}/{urllib.parse.quote(search_song)})" | ||||||
|  |                 c: int = 0 | ||||||
|  |                 footer: str = "To be continued..."   #Placeholder | ||||||
|  |                 for section in search_result_wrapped: | ||||||
|  |                     c+=1 | ||||||
|  |                     if c == len(search_result_wrapped):  | ||||||
|  |                         footer: str = f"Found on: {search_result_src}" | ||||||
|  |                     section: str = self.control_strip_regex.sub('', section) | ||||||
|  |                     # if ctx.guild.id == 1145182936002482196: | ||||||
|  |                     #     section = section.upper() | ||||||
|  |                     embed: discord.Embed = discord.Embed( | ||||||
|  |                     title=f"{search_result_song} by {search_result_artist}", | ||||||
|  |                     description=discord.utils.escape_markdown(section.replace("\n", "\n\n")) | ||||||
|  |                 ) | ||||||
|  |                     embed.add_field(name="Confidence", value=search_result_confidence, | ||||||
|  |                                     inline=True) | ||||||
|  |                     embed.add_field(name="Time Taken", value=search_result_time_taken, | ||||||
|  |                                     inline=True) | ||||||
|  |                     embed.add_field(name="Link", value=embed_url) | ||||||
|  |                     embed.set_footer(text=footer) | ||||||
|  |                     embeds.append(embed) | ||||||
|  |                 await ctx.respond(embed=embeds[0])     | ||||||
|  |                 for embed in embeds[1:]: | ||||||
|  |                     await ctx.send(embed=embed) | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"ERR: {str(e)}") | ||||||
|  |              | ||||||
|  |     @commands.user_command(name="Sing") | ||||||
|  |     async def sing_context_menu(self, ctx, member: discord.Member) -> None: | ||||||
|  |         """ | ||||||
|  |         Sing Context Menu Command | ||||||
|  |         Args: | ||||||
|  |             ctx (Any): Discord context | ||||||
|  |             member (discord.Member): Discord member | ||||||
|  |         Returns: | ||||||
|  |             None | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             PODY_ID: int = 1172340700663255091 | ||||||
|  |             IS_SPAMCHAN: bool = ctx.channel.id in BOT_CHANIDS | ||||||
|  |             member_display = ctx.interaction.guild.get_member(member.id)\ | ||||||
|  |                 .display_name | ||||||
|  |             if not(ctx.interaction.guild.get_member(member.id).activities)\ | ||||||
|  |                 and not member.id == PODY_ID: | ||||||
|  |                 return await ctx.respond(f"No activity detected to read for {member_display}.", ephemeral=True) | ||||||
|  |             member_id: int = member.id #if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody! | ||||||
|  |             activity: Optional[discord.Activity] = None | ||||||
|  |             for _activity in ctx.interaction.guild.get_member(member_id).activities: | ||||||
|  |                 if _activity.type == discord.ActivityType.listening: | ||||||
|  |                     activity: discord.Activity = _activity | ||||||
|  |             parsed: tuple|bool = self.utility.parse_song_input(song=None, | ||||||
|  |                                                                activity=activity) | ||||||
|  |             if not parsed: | ||||||
|  |                 return await ctx.respond(f"Could not parse activity of {member_display}.", ephemeral=True) | ||||||
|  |             if IS_SPAMCHAN:  | ||||||
|  |                 await ctx.respond(f"***Reading activity of {member_display}...***") | ||||||
|  |              | ||||||
|  |             (search_artist, search_song, search_subsearch) = parsed | ||||||
|  |             await ctx.respond("*Searching...*", ephemeral=True) # Must respond to interactions within 3 seconds, per Discord | ||||||
|  |             search_result: list = await self.utility.lyric_search(search_artist, search_song, | ||||||
|  |                                                                   search_subsearch) | ||||||
|  |              | ||||||
|  |             if len(search_result) == 1: | ||||||
|  |                 return await ctx.send(search_result[0].strip())            | ||||||
|  |              | ||||||
|  |             (search_result_artist, search_result_song, search_result_src, | ||||||
|  |              search_result_confidence, search_result_time_taken) = search_result[0] #  First index is a tuple | ||||||
|  |             search_result_wrapped: list[str] = search_result[1] # Second index is the wrapped lyrics | ||||||
|  |             search_result_wrapped_short: list[str] = search_result[2] # Third index is shortened lyrics | ||||||
|  |              | ||||||
|  |             if not IS_SPAMCHAN: | ||||||
|  |                 search_result_wrapped: list[str] = search_result_wrapped_short # Swap for shortened lyrics if not spam chan | ||||||
|  |  | ||||||
|  |             embeds: list[Optional[discord.Embed]] = [] | ||||||
|  |             c: int = 0 | ||||||
|  |             footer: str = "To be continued..."   #Placeholder | ||||||
|  |             for section in search_result_wrapped: | ||||||
|  |                 c+=1 | ||||||
|  |                 if c == len(search_result_wrapped): | ||||||
|  |                     footer: str = f"Found on: {search_result_src}" | ||||||
|  |                 # if ctx.guild.id == 1145182936002482196: | ||||||
|  |                 #     section = section.upper() | ||||||
|  |                 embed: discord.Embed = discord.Embed( | ||||||
|  |                 title=f"{search_result_song} by {search_result_artist}", | ||||||
|  |                 description=discord.utils.escape_markdown(section.replace("\n", "\n\n")) | ||||||
|  |             ) | ||||||
|  |                 embed.add_field(name="Confidence", value=search_result_confidence, inline=True) | ||||||
|  |                 embed.add_field(name="Time Taken", value=search_result_time_taken, inline=True) | ||||||
|  |                 embed.add_field(name="Link", value=f"[on codey.lol](https://codey.lol/#{urllib.parse.quote(search_result_artist)}/{urllib.parse.quote(search_result_song)})") | ||||||
|  |                 embed.set_footer(text=footer) | ||||||
|  |                 embeds.append(embed) | ||||||
|  |                  | ||||||
|  |             for embed in embeds: | ||||||
|  |                 await ctx.send(embed=embed) | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return await ctx.respond(f"ERR: {str(e)}") | ||||||
|  |              | ||||||
|  | def setup(bot) -> None: | ||||||
|  |     """Run on Cog Load""" | ||||||
|  |     bot.add_cog(Sing(bot)) | ||||||
							
								
								
									
										15
									
								
								constructors.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								constructors.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,15 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | AI | ||||||
|  | """ | ||||||
|  | class AIException(Exception): | ||||||
|  |     """AI Exception (generic)""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | LoveHate | ||||||
|  | """ | ||||||
|  | class LoveHateException(Exception): | ||||||
|  |     """Love Hate Exception (generic)""" | ||||||
|  |     pass | ||||||
							
								
								
									
										97
									
								
								disc_havoc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								disc_havoc.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,97 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | # pylint: disable=bare-except, invalid-name, import-outside-toplevel | ||||||
|  |  | ||||||
|  | import os | ||||||
|  | import logging | ||||||
|  | import importlib | ||||||
|  | import discord | ||||||
|  | import setproctitle | ||||||
|  | import hypercorn | ||||||
|  | import hypercorn.asyncio | ||||||
|  | from dotenv import load_dotenv | ||||||
|  | from discord.ext import bridge, commands | ||||||
|  | from termcolor import colored | ||||||
|  | import api | ||||||
|  |  | ||||||
|  | logging.basicConfig(level=logging.INFO, | ||||||
|  |                     format='%(asctime)s %(message)s', | ||||||
|  |                     encoding='utf-8') | ||||||
|  | setproctitle.setproctitle('disc-havoc') | ||||||
|  |  | ||||||
|  | owners = [1172340700663255091, 992437729927376996] | ||||||
|  | BOT_CHANIDS = [ | ||||||
|  |     1145182936442875997, | ||||||
|  |     1157535700774834236, | ||||||
|  |     1156710277266542624, | ||||||
|  |     1179232748385341530, | ||||||
|  |     1219638300654964807, | ||||||
|  |     1193632849740439665, | ||||||
|  |     1202288798315335831, | ||||||
|  |     1157529874936909934, | ||||||
|  |     1272333206066167959, | ||||||
|  |     1228740577068322839, | ||||||
|  |     1228740577068322841, | ||||||
|  |     1324142398741151784, | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  | cogs_list = [ | ||||||
|  |     'misc', | ||||||
|  |     'owner', | ||||||
|  |     'sing', | ||||||
|  |     'meme', | ||||||
|  |     'ai', | ||||||
|  |     'karma', | ||||||
|  |     'lovehate', | ||||||
|  |     'quote', | ||||||
|  |     'radio', | ||||||
|  | ] | ||||||
|  |  | ||||||
|  | bot_activity = discord.CustomActivity(name="I made cookies!") | ||||||
|  |  | ||||||
|  | load_dotenv() | ||||||
|  |  | ||||||
|  | intents = discord.Intents.all() | ||||||
|  | intents.message_content = True | ||||||
|  | bot = bridge.Bot(command_prefix=".", intents=intents, | ||||||
|  |                  owner_ids=owners, activity=bot_activity, | ||||||
|  |                  help_command=commands.MinimalHelpCommand()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @bot.event | ||||||
|  | async def on_ready(): | ||||||
|  |     """Run on Bot Ready""" | ||||||
|  |     logging.info("%s online!", bot.user)   | ||||||
|  |  | ||||||
|  | def load_exts(initialRun=True): | ||||||
|  |     """Load Cogs/Extensions""" | ||||||
|  |     load_method = bot.load_extension if initialRun else bot.reload_extension | ||||||
|  |  | ||||||
|  |     for cog in cogs_list: | ||||||
|  |         logging.info("Loading: %s", cog) | ||||||
|  |         load_method(f'cogs.{cog}')    | ||||||
|  |  | ||||||
|  |     # asyncio.get_event_loop().create_task(bot.sync_commands()) | ||||||
|  |  | ||||||
|  |     importlib.reload(api) | ||||||
|  |     from api import API  # pylint: disable=unused-import     | ||||||
|  |     api_config = hypercorn.config.Config() | ||||||
|  |     api_config.bind = "10.10.10.100:5992"   | ||||||
|  |     api_instance = api.API(bot) | ||||||
|  |     try: | ||||||
|  |         bot.fapi_task.cancel() | ||||||
|  |     except: | ||||||
|  |         pass | ||||||
|  |      | ||||||
|  |     logging.info("Starting FAPI Task") | ||||||
|  |  | ||||||
|  |     bot.fapi_task = bot.loop.create_task(hypercorn.asyncio.serve(api_instance.api_app, api_config))  | ||||||
|  |              | ||||||
|  | def __init__(): | ||||||
|  |     logging.info(colored(f"Log level: {logging.getLevelName(logging.root.level)}", "red", attrs=['reverse'])) | ||||||
|  |     bot.BOT_CHANIDS = BOT_CHANIDS | ||||||
|  |     bot.load_exts = load_exts | ||||||
|  |     bot.load_exts() | ||||||
|  |     bot.run(os.getenv('TOKEN'))       | ||||||
|  |      | ||||||
|  | if __name__ == "__main__": | ||||||
|  |     __init__() | ||||||
							
								
								
									
										13
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | aiosqlite==0.20.0 | ||||||
|  | beautifulsoup4==4.12.3 | ||||||
|  | edge_tts==6.1.12 | ||||||
|  | feedparser==6.0.11 | ||||||
|  | Flask==3.0.3 | ||||||
|  | nvdlib==0.7.7 | ||||||
|  | openai==1.54.3 | ||||||
|  | requests_async==0.6.2 | ||||||
|  | shazamio==0.7.0 | ||||||
|  | streamrip==2.0.5 | ||||||
|  | Unidecode==1.3.6 | ||||||
|  | Unidecode==1.3.8 | ||||||
|  | websockets==12.0 | ||||||
							
								
								
									
										6
									
								
								util/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								util/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  |  | ||||||
|  | import importlib | ||||||
|  |  | ||||||
|  | from . import discord_helpers | ||||||
|  | importlib.reload(discord_helpers) | ||||||
							
								
								
									
										52
									
								
								util/discord_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								util/discord_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | import re | ||||||
|  | import discord | ||||||
|  | from typing import Optional, Any | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Discord Helper Methods | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | async def get_channel_by_name(bot: discord.Bot, channel: str, | ||||||
|  |                               guild: int | None = None) -> Optional[Any]: # Optional[Any] used as pycord channel types can be ambigious | ||||||
|  |     """ | ||||||
|  |     Get Channel by Name | ||||||
|  |     Args: | ||||||
|  |         bot (discord.Bot) | ||||||
|  |         channel (str) | ||||||
|  |         guild (int|None) | ||||||
|  |     Returns: | ||||||
|  |         Optional[Any] | ||||||
|  |     """ | ||||||
|  |     channel: str = re.sub(r'^#', '', channel.strip()) | ||||||
|  |     if not guild: | ||||||
|  |         return discord.utils.get(bot.get_all_channels(), | ||||||
|  |                              name=channel) | ||||||
|  |     else: | ||||||
|  |         channels: list = bot.get_guild(guild).channels | ||||||
|  |         for _channel in channels: | ||||||
|  |             if _channel.name.lower() == channel.lower().strip(): | ||||||
|  |                 return _channel | ||||||
|  |         return | ||||||
|  |  | ||||||
|  | async def send_message(bot: discord.Bot, channel: str, | ||||||
|  |                        message: str, guild: int | None = None) -> None: | ||||||
|  |     """ | ||||||
|  |     Send Message to the provided channel.  If guild is provided, will limit to channels within that guild to ensure the correct | ||||||
|  |     channel is selected.  Useful in the event a channel exists in more than one guild that the bot resides in. | ||||||
|  |     Args: | ||||||
|  |         bot (discord.Bot) | ||||||
|  |         channel (str) | ||||||
|  |         message (str) | ||||||
|  |         guild (int|None) | ||||||
|  |     Returns: | ||||||
|  |         None | ||||||
|  |     """ | ||||||
|  |     if channel.isnumeric(): | ||||||
|  |         channel: int = int(channel) | ||||||
|  |         _channel = bot.get_channel(channel) | ||||||
|  |     else: | ||||||
|  |         channel: str = re.sub(r'^#', '', channel.strip())  | ||||||
|  |         _channel = await get_channel_by_name(bot=bot, | ||||||
|  |                                         channel=channel, guild=guild) | ||||||
|  |     await _channel.send(message) | ||||||
							
								
								
									
										162
									
								
								util/lovehate_db.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								util/lovehate_db.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | import os | ||||||
|  | import logging | ||||||
|  | from typing import Optional, LiteralString | ||||||
|  | import aiosqlite as sqlite3 | ||||||
|  | from constructors import LoveHateException | ||||||
|  |  | ||||||
|  | class DB: | ||||||
|  |     """LoveHate DB Utility Class""" | ||||||
|  |     def __init__(self, bot) -> None: | ||||||
|  |         self.db_path: str|LiteralString = os.path.join("/usr/local/share", | ||||||
|  |                                     "sqlite_dbs", "lovehate.db")  | ||||||
|  |          | ||||||
|  |     async def get_wholovehates(self, thing: str, loves: bool = False, | ||||||
|  |                                hates: bool = False) -> list[tuple]|bool: | ||||||
|  |         """ | ||||||
|  |         Get a list of users who have professed their love OR hatred for <thing> | ||||||
|  |          | ||||||
|  |         Args: | ||||||
|  |             thing (str): The <thing> to check | ||||||
|  |             loves (bool): Are we looking for loves? | ||||||
|  |             hates (bool): ...or are we looking for hates? | ||||||
|  |         Returns: | ||||||
|  |             list[tuple] | ||||||
|  |         """         | ||||||
|  |  | ||||||
|  |         query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag = ?" | ||||||
|  |         params: tuple = tuple() | ||||||
|  |         flag: Optional[int] = None | ||||||
|  |  | ||||||
|  |         if hates and loves: | ||||||
|  |             raise LoveHateException("Both hates and loves may not be True") | ||||||
|  |         elif hates: | ||||||
|  |             flag: int = -1 | ||||||
|  |         elif loves: | ||||||
|  |             flag: int = 1 | ||||||
|  |         elif not hates and not loves: | ||||||
|  |             raise LoveHateException("Neither loves nor hates were requested") | ||||||
|  |          | ||||||
|  |         params: tuple = (thing, flag,) | ||||||
|  |         async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute(query, params) as db_cursor: | ||||||
|  |                 result: list[tuple] = await db_cursor.fetchall() | ||||||
|  |                 logging.debug("Result: %s", result) | ||||||
|  |                 if not result: | ||||||
|  |                     return False | ||||||
|  |                 return result | ||||||
|  |          | ||||||
|  |     async def get_lovehates(self, loves: bool = False, hates: bool = False, | ||||||
|  |                             user: str = None, thing: str = None) -> list[tuple]|bool: | ||||||
|  |         """ | ||||||
|  |         Get a list of either 1) what {user} loves/hates, or who loves/hates {thing}, depending on bools loves, hates | ||||||
|  |         Args: | ||||||
|  |             loves (bool): Are we looking for loves? | ||||||
|  |             hates (bool): ...OR are we looking for hates? | ||||||
|  |             user (Optional[str]): the user to query against | ||||||
|  |             thing (Optional[str]): ... OR the thing to query against | ||||||
|  |         Returns: | ||||||
|  |             list[tuple]|bool | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         query: str = "" | ||||||
|  |         params: tuple = tuple() | ||||||
|  |          | ||||||
|  |         if not user and not thing: | ||||||
|  |             raise LoveHateException("Neither a <user> or <thing> was specified to query against") | ||||||
|  |          | ||||||
|  |         flag: Optional[int] = None | ||||||
|  |  | ||||||
|  |         if hates and loves: | ||||||
|  |             raise LoveHateException("Both hates and loves may not be True") | ||||||
|  |         elif hates: | ||||||
|  |             flag: int = -1 | ||||||
|  |         elif loves: | ||||||
|  |             flag: int = 1 | ||||||
|  |         elif not hates and not loves: | ||||||
|  |             raise LoveHateException("Neither loves nor hates were requested") | ||||||
|  |  | ||||||
|  |         if user: | ||||||
|  |             query: str = "SELECT thing FROM lovehate WHERE display_name LIKE ? AND flag == ?" | ||||||
|  |             params: tuple  = (user, flag,) | ||||||
|  |         elif thing: | ||||||
|  |             query: str = "SELECT display_name FROM lovehate WHERE thing LIKE ? AND flag == ?" | ||||||
|  |             params: tuple = (thing, flag,) | ||||||
|  |  | ||||||
|  |         async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute(query, params) as db_cursor: | ||||||
|  |                 result = await db_cursor.fetchall() | ||||||
|  |                 if not result: | ||||||
|  |                     return False | ||||||
|  |                 return result | ||||||
|  |  | ||||||
|  |  | ||||||
|  |     async def check_existence(self, user: str, thing: str) -> Optional[int]: | ||||||
|  |         """ | ||||||
|  |         Determine whether a user is opinionated on a <thing> | ||||||
|  |         Args: | ||||||
|  |             user (str): The user to check | ||||||
|  |             thing (str): The thing to check if the user has an opinion on | ||||||
|  |         Returns: | ||||||
|  |             Optional[int] | ||||||
|  |         """ | ||||||
|  |          | ||||||
|  |         params = (user, thing,) | ||||||
|  |  | ||||||
|  |         async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute("SELECT id, flag FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?", params) as db_cursor: | ||||||
|  |                 result = await db_cursor.fetchone() | ||||||
|  |                 if not result: | ||||||
|  |                     return None | ||||||
|  |                 (_, flag) = result | ||||||
|  |                 return flag | ||||||
|  |  | ||||||
|  |     async def update(self, user: str, thing: str, flag: int) -> str: | ||||||
|  |         """ | ||||||
|  |         Updates the lovehate database, and returns an appropriate response | ||||||
|  |         Args: | ||||||
|  |             user (str): The user to update | ||||||
|  |             thing (str): The thing the user loves/hates/doesn't care about anymore | ||||||
|  |             flag (int): int representation of love (1), hate (-1), and dontcare (0) | ||||||
|  |         Returns:    | ||||||
|  |             str | ||||||
|  |         """ | ||||||
|  |         if not flag in range(-1, 2): | ||||||
|  |             raise LoveHateException(f"Invalid flag {flag} specified, is this love (1), hate (-1), or dontcare? (0)") | ||||||
|  |  | ||||||
|  |         db_query: str = "" | ||||||
|  |         params: tuple = (user, thing,) | ||||||
|  |  | ||||||
|  |         already_opinionated: bool = await self.check_existence(user, thing) | ||||||
|  |         if already_opinionated:  | ||||||
|  |             if flag == 0: | ||||||
|  |                 db_query: str = "DELETE FROM lovehate WHERE display_name LIKE ? AND thing LIKE ?" | ||||||
|  |             else: | ||||||
|  |                 loves_or_hates: str = "loves" | ||||||
|  |                 if already_opinionated == -1: | ||||||
|  |                     loves_or_hates: str = "hates" | ||||||
|  |                 raise LoveHateException(f"But {user} already {loves_or_hates} {thing}...") | ||||||
|  |         else: | ||||||
|  |             match flag: | ||||||
|  |                 case -1: | ||||||
|  |                     db_query: str = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, -1, ?)" | ||||||
|  |                 case 1: | ||||||
|  |                     db_query: str = "INSERT INTO lovehate(display_name, flag, thing) VALUES(?, 1, ?)" | ||||||
|  |                 case _: | ||||||
|  |                     raise LoveHateException("Unknown error, default case matched") | ||||||
|  |                  | ||||||
|  |  | ||||||
|  |         async with sqlite3.connect(self.db_path, timeout=2) as db_conn: | ||||||
|  |             async with await db_conn.execute(db_query, params) as db_cursor: | ||||||
|  |                 await db_conn.commit() | ||||||
|  |                 if db_cursor.rowcount != 1: | ||||||
|  |                     raise LoveHateException(f"DB Error - RowCount: {db_cursor.rowcount} for INSERT query") | ||||||
|  |                 match flag: | ||||||
|  |                     case -1: | ||||||
|  |                         return f"We're done here, {user} hates {thing}." | ||||||
|  |                     case 0:  | ||||||
|  |                         return f"We're done here, {user} no longer cares one way or the other about {thing}." | ||||||
|  |                     case 1: | ||||||
|  |                         return f"We're done here, {user} loves {thing}." | ||||||
|  |                     case _: | ||||||
|  |                         raise LoveHateException("Unknown error, default case matched [2]") | ||||||
							
								
								
									
										139
									
								
								util/sing_util.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								util/sing_util.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,139 @@ | |||||||
|  | #!/usr/bin/env python3.12 | ||||||
|  | import logging | ||||||
|  | import regex | ||||||
|  | import aiohttp | ||||||
|  | import textwrap | ||||||
|  | import traceback | ||||||
|  | from typing import Optional | ||||||
|  | from discord import Activity | ||||||
|  |  | ||||||
|  | class Utility: | ||||||
|  |     """Sing Utility""" | ||||||
|  |     def __init__(self): | ||||||
|  |         self.api_url: str = "http://127.0.0.1:52111/lyric/search" | ||||||
|  |         self.api_src: str = "DISC-HAVOC" | ||||||
|  |          | ||||||
|  |     def parse_song_input(self, song: Optional[str] = None, | ||||||
|  |                          activity: Optional[Activity] = None) -> bool|tuple: | ||||||
|  |         """Parse Song (Sing Command) Input | ||||||
|  |         Args: | ||||||
|  |             song (Optional[str]): Song to search | ||||||
|  |             activity (Optional[discord.Activity]): Discord activity, used to attempt lookup if no song is provided | ||||||
|  |         Returns: | ||||||
|  |             bool|tuple | ||||||
|  |         """ | ||||||
|  |         logging.debug("Activity? %s", activity) | ||||||
|  |         try: | ||||||
|  |             if (not song or len(song) < 2) and not activity: | ||||||
|  |                 # pylint: disable=superfluous-parens | ||||||
|  |                 return False | ||||||
|  |                 # pylint: enable=superfluous-parens | ||||||
|  |             if not song and activity: | ||||||
|  |                 match activity.name.lower(): | ||||||
|  |                     case "codey toons" | "cider" | "sonixd": | ||||||
|  |                         search_artist: str = " ".join(str(activity.state)\ | ||||||
|  |                             .strip().split(" ")[1:]) | ||||||
|  |                         search_artist: str = regex.sub(r"(\s{0,})(\[(spotify|tidal|sonixd|browser|yt music)])$", "", | ||||||
|  |                                                        search_artist.strip(), flags=regex.IGNORECASE) | ||||||
|  |                         search_song: str = str(activity.details) | ||||||
|  |                         song: str = f"{search_artist} : {search_song}" | ||||||
|  |                     case "tidal hi-fi": | ||||||
|  |                         search_artist: str = str(activity.state) | ||||||
|  |                         search_song: str = str(activity.details) | ||||||
|  |                         song: str = f"{search_artist} : {search_song}" | ||||||
|  |                     case "spotify": | ||||||
|  |                         search_artist: str = str(activity.title) | ||||||
|  |                         search_song: str = str(activity.artist) | ||||||
|  |                         song: str = f"{search_artist} : {search_song}" | ||||||
|  |                     case "serious.fm" | "cocks.fm" | "something": | ||||||
|  |                         if not activity.details: | ||||||
|  |                             song: str = str(activity.state) | ||||||
|  |                         else: | ||||||
|  |                             search_artist: str = str(activity.state) | ||||||
|  |                             search_song: str = str(activity.details) | ||||||
|  |                             song: str = f"{search_artist} : {search_song}" | ||||||
|  |                     case _: | ||||||
|  |                         return False # Unsupported activity detected | ||||||
|  |              | ||||||
|  |             search_split_by: str = ":" if not(song) or len(song.split(":")) > 1\ | ||||||
|  |                 else "-" # Support either : or - to separate artist/track | ||||||
|  |             search_artist: str = song.split(search_split_by)[0].strip() | ||||||
|  |             search_song: str = "".join(song.split(search_split_by)[1:]).strip() | ||||||
|  |             search_subsearch: Optional[str] = None | ||||||
|  |             if search_split_by == ":" and len(song.split(":")) > 2: # Support sub-search if : is used (per instructions) | ||||||
|  |                 search_song: str = song.split(search_split_by)[1].strip() # Reduce search_song to only the 2nd split of : [the rest is meant to be lyric text] | ||||||
|  |                 search_subsearch: str = "".join(song.split(search_split_by)[2:]) # Lyric text from split index 2 and beyond | ||||||
|  |             return (search_artist, search_song, search_subsearch) | ||||||
|  |         except: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return False | ||||||
|  |          | ||||||
|  |     async def lyric_search(self, artist: str, song: str, | ||||||
|  |                            sub: Optional[str] = None) -> list[str]: | ||||||
|  |         """ | ||||||
|  |         Lyric Search | ||||||
|  |         Args: | ||||||
|  |             artist (str): Artist to search | ||||||
|  |             song (str): Song to search | ||||||
|  |             sub (Optional[str]): Lyrics for subsearch | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             if not artist or not song: | ||||||
|  |                 return ["FAIL! Artist/Song not provided"] | ||||||
|  |              | ||||||
|  |             search_obj: dict = { | ||||||
|  |                 'a': artist.strip(), | ||||||
|  |                 's': song.strip(), | ||||||
|  |                 'extra': True, | ||||||
|  |                 'src': self.api_src, | ||||||
|  |             } | ||||||
|  |              | ||||||
|  |             if len(song.strip()) < 1: | ||||||
|  |                 search_obj.pop('a') | ||||||
|  |                 search_obj.pop('s') | ||||||
|  |                 search_obj['t'] = artist.strip() # Parse failed, try title without sep | ||||||
|  |              | ||||||
|  |             if sub and len(sub) >= 2: | ||||||
|  |                 search_obj['sub'] = sub.strip() | ||||||
|  |              | ||||||
|  |             async with aiohttp.ClientSession() as session: | ||||||
|  |                 async with await session.post(self.api_url, | ||||||
|  |                                               json=search_obj, | ||||||
|  |                                               timeout=aiohttp.ClientTimeout(connect=5, sock_read=10)) as request: | ||||||
|  |                     request.raise_for_status() | ||||||
|  |                     response: dict = await request.json() | ||||||
|  |                     if response.get('err'): | ||||||
|  |                         return [f"ERR: {response.get('errorText')}"] | ||||||
|  |                      | ||||||
|  |                     out_lyrics = regex.sub(r'<br>', '\u200B\n', response.get('lyrics')) | ||||||
|  |                     response_obj: dict = { | ||||||
|  |                         'artist': response.get('artist'), | ||||||
|  |                         'song': response.get('song'), | ||||||
|  |                         'lyrics': out_lyrics, | ||||||
|  |                         'src': response.get('src'), | ||||||
|  |                         'confidence': float(response.get('confidence')), | ||||||
|  |                         'time': float(response.get('time')), | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     lyrics = response_obj.get('lyrics') | ||||||
|  |                     response_obj['lyrics'] = textwrap.wrap(text=lyrics.strip(), | ||||||
|  |                                                         width=4000, drop_whitespace=False, | ||||||
|  |                                                         replace_whitespace=False, break_long_words=True, | ||||||
|  |                                                         break_on_hyphens=True, max_lines=8) | ||||||
|  |                     response_obj['lyrics_short'] = textwrap.wrap(text=lyrics.strip(), | ||||||
|  |                                                         width=750, drop_whitespace=False, | ||||||
|  |                                                         replace_whitespace=False, break_long_words=True, | ||||||
|  |                                                         break_on_hyphens=True, max_lines=1) | ||||||
|  |                      | ||||||
|  |                     return [ | ||||||
|  |                         ( | ||||||
|  |                             response_obj.get('artist'), response_obj.get('song'), response_obj.get('src'), | ||||||
|  |                             f"{int(response_obj.get('confidence'))}%", | ||||||
|  |                             f"{response_obj.get('time', -666.0):.4f}s", | ||||||
|  |                         ), | ||||||
|  |                             response_obj.get('lyrics'), | ||||||
|  |                             response_obj.get('lyrics_short'), | ||||||
|  |                         ]  | ||||||
|  |         except Exception as e: | ||||||
|  |             traceback.print_exc() | ||||||
|  |             return [f"Retrieval failed: {str(e)}"] | ||||||
		Reference in New Issue
	
	Block a user