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