From 3dac8033055fd033468bee40892ed16489d00ce7 Mon Sep 17 00:00:00 2001 From: codey Date: Thu, 15 May 2025 15:49:28 -0400 Subject: [PATCH] meme dupe snitching/misc --- cogs/karma.py | 2 +- cogs/meme.py | 85 +++++++++++++++++++++++++++++++++++++++++----- cogs/misc.py | 8 ++--- cogs/sing.py | 20 +++-------- requirements.txt | 2 +- util/jesusmemes.py | 4 +-- util/misc_util.py | 35 ++++++++----------- util/sing_util.py | 4 ++- 8 files changed, 105 insertions(+), 55 deletions(-) diff --git a/cogs/karma.py b/cogs/karma.py index 192ec05..bc73222 100644 --- a/cogs/karma.py +++ b/cogs/karma.py @@ -100,7 +100,7 @@ class Util: top_formatted: str = "" for x, item in enumerate(top): top_formatted += ( - f"{x+1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n" + f"{x + 1}. **{discord.utils.escape_markdown(item[0])}**: *{item[1]}*\n" ) top_formatted = top_formatted.strip() embed: discord.Embed = discord.Embed( diff --git a/cogs/meme.py b/cogs/meme.py index 462b8ed..332885f 100644 --- a/cogs/meme.py +++ b/cogs/meme.py @@ -4,6 +4,9 @@ import json import io import asyncio import random +import copy +from PIL import Image, UnidentifiedImageError +import imagehash from typing import LiteralString, Optional, Any, Union import aiosqlite as sqlite3 import logging @@ -132,9 +135,12 @@ class Meme(commands.Cog): def __init__(self, bot: Havoc) -> None: self.bot: Havoc = bot - self.stats_db_path: LiteralString = os.path.join( + self.stats_db_path: str = os.path.join( "/usr/local/share", "sqlite_dbs", "stats.db" ) + self.memedb_path: str = os.path.join( + "/usr/local/share", "sqlite_dbs", "meme.db" + ) self.meme_choices: list = [] self.meme_counter: int = 0 self.THREADS: dict[str, dict[int, list]] = { @@ -243,6 +249,45 @@ class Meme(commands.Cog): count = result["count"] self.meme_leaderboard[uid] = count + async def insert_meme( + self, discord_uid: int, timestamp: int, message_id: int, image_url: str + ) -> Optional[bool]: + """ + INSERT MEME -> SQLITE DB + """ + try: + _image: io.BytesIO = io.BytesIO( + requests.get(image_url, stream=True, timeout=20).raw.read() + ) + image_copy = copy.deepcopy(_image) + image = Image.open(image_copy) + except UnidentifiedImageError: + return None + phash: str = str(imagehash.phash(image)) + query: str = "INSERT INTO memes(discord_uid, timestamp, image, message_ids, phash) VALUES(?, ?, ?, ?, ?)" + async with sqlite3.connect(self.memedb_path, timeout=2) as db_conn: + insert = await db_conn.execute_insert( + query, (discord_uid, timestamp, _image.read(), message_id, phash) + ) + if insert: + await db_conn.commit() + return True + return None + + async def dupe_check(self, image) -> bool | int: + """ + CHECK DB FOR DUPLICATE MEMES! + """ + phash: str = str(imagehash.phash(image)) + query: str = "SELECT message_ids FROM memes WHERE phash = ? LIMIT 1" + async with sqlite3.connect(self.memedb_path, timeout=2) as db_conn: + db_conn.row_factory = sqlite3.Row + async with await db_conn.execute(query, (phash,)) as db_cursor: + result = await db_cursor.fetchone() + if result: + return result["message_ids"] + return False + @commands.Cog.listener() async def on_ready(self) -> None: """Run on Bot Ready""" @@ -644,6 +689,7 @@ class Meme(commands.Cog): Also monitors for messages to #memes-top-10 to autodelete, only Havoc may post in #memes-top-10! """ lb_chanid: int = 1352373745108652145 + meme_chanid: int = 1147229098544988261 if not self.bot.user: # No valid client instance return if not isinstance(message.channel, discord.TextChannel): @@ -666,12 +712,37 @@ class Meme(commands.Cog): return if not message.guild: return - if not message.channel.id == 1147229098544988261: # Not meme channel + if message.channel.id not in [ + 1157529874936909934, + meme_chanid, + ]: # Not meme channel return if not message.attachments: # No attachments to consider a meme return - await self.leaderboard_increment(message.author.id) + unique_memes: list = [] + 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() + ) + dupe_check = await self.dupe_check(Image.open(image)) + if dupe_check: + channel = message.channel + original_message = await channel.fetch_message(dupe_check) # type: ignore + original_message_url = original_message.jump_url + await message.add_reaction( + emoji="<:quietscheentchen:1255956612804247635>" + ) + await message.reply(original_message_url) + else: + unique_memes.append(item.url) + if unique_memes: + await self.leaderboard_increment(message.author.id) + for meme_url in unique_memes: + author_id: int = message.author.id + timestamp: int = int(message.created_at.timestamp()) + await self.insert_meme(author_id, timestamp, message.id, meme_url) async def get_top(self, n: int = 10) -> Optional[list[tuple]]: """ @@ -686,9 +757,7 @@ class Meme(commands.Cog): out_top: list[tuple[int, int]] = [] async with sqlite3.connect(self.stats_db_path, timeout=2) as db_conn: db_conn.row_factory = sqlite3.Row - query: str = ( - "SELECT discord_uid, count FROM memes WHERE count > 0 ORDER BY count DESC" - ) + query: str = "SELECT discord_uid, count FROM memes WHERE count > 0 ORDER BY count DESC" async with db_conn.execute(query) as db_cursor: db_result = await db_cursor.fetchall() for res in db_result: @@ -734,9 +803,7 @@ class Meme(commands.Cog): if not member: continue display_name: str = member.display_name - top_formatted += ( - f"{x+1}. **{discord.utils.escape_markdown(display_name)}**: *{count}*\n" - ) + top_formatted += f"{x + 1}. **{discord.utils.escape_markdown(display_name)}**: *{count}*\n" top_formatted = top_formatted.strip() embed: discord.Embed = discord.Embed( title=f"Top {n} Memes", description=top_formatted, colour=0x25BD6B diff --git a/cogs/misc.py b/cogs/misc.py index beb3fd5..0b9a7d6 100644 --- a/cogs/misc.py +++ b/cogs/misc.py @@ -215,9 +215,7 @@ class Misc(commands.Cog): logging.debug("Failed to add xmas reaction: %s", str(e)) await ctx.respond( f"Only {days} days, {hours} hours, {minutes} minutes,\ - {seconds} seconds and {ms} ms left! (UTC)".translate( - xmas_trans - ) + {seconds} seconds and {ms} ms left! (UTC)".translate(xmas_trans) ) except Exception as e: traceback.print_exc() @@ -1292,7 +1290,7 @@ class Misc(commands.Cog): Satan(!?) """ out_msg: str = ( - "## There is no Satan\n" "### He isn't real.\n" "-# And neither are you." + "## There is no Satan\n### He isn't real.\n-# And neither are you." ) return await ctx.respond(out_msg) @@ -1403,7 +1401,7 @@ class Misc(commands.Cog): except Exception as e: traceback.print_exc() return await ctx.respond(f"Error: {str(e)}") - + @bridge.bridge_command() async def no(self, ctx) -> None: """No (As A Service)""" diff --git a/cogs/sing.py b/cogs/sing.py index 8d797ad..8c3f901 100644 --- a/cogs/sing.py +++ b/cogs/sing.py @@ -94,9 +94,7 @@ class Sing(commands.Cog): search_result_src, search_result_confidence, search_result_time_taken, - ) = search_result[ - 0 - ] # First index is a tuple + ) = search_result[0] # First index is a tuple search_result_wrapped: list[str] = search_result[ 1 ] # Second index is the wrapped lyrics @@ -124,9 +122,7 @@ class Sing(commands.Cog): # if ctx.guild.id == 1145182936002482196: # section = section.upper() section = regex.sub(r"\p{Vert_Space}", " / ", section.strip()) - msg: str = ( - f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" - ) + msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" if c > 1: msg = "\n".join(msg.split("\n")[1:]) out_messages.append(msg.strip()) @@ -159,9 +155,7 @@ class Sing(commands.Cog): f"No activity detected to read for {member_display}.", ephemeral=True, ) - member_id: int = ( - member.id - ) # if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody! + member_id: int = member.id # if not(member.id == PODY_ID) else 1234134345497837679 # Use Thomas for Pody! activity: Optional[discord.Activity] = None if IS_SPAMCHAN: await ctx.respond(f"***Reading activity of {member_display}...***") @@ -199,9 +193,7 @@ class Sing(commands.Cog): search_result_src, search_result_confidence, search_result_time_taken, - ) = search_result[ - 0 - ] # First index is a tuple + ) = search_result[0] # First index is a tuple search_result_wrapped: list = search_result[ 1 ] # Second index is the wrapped lyrics @@ -230,9 +222,7 @@ class Sing(commands.Cog): # if ctx.guild.id == 1145182936002482196: # section = section.upper() section = regex.sub(r"\p{Vert_Space}", " / ", section.strip()) - msg: str = ( - f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" - ) + msg: str = f"**{search_result_song}** by **{search_result_artist}**\n-# {section}\n{footer}" if c > 1: msg = "\n".join(msg.split("\n")[1:]) out_messages.append(msg.strip()) diff --git a/requirements.txt b/requirements.txt index 5ea9346..924247e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ edge_tts==6.1.12 feedparser==6.0.11 Flask==3.0.3 nvdlib==0.7.7 -openai==1.54.3 +openai==1.54.3d requests_async==0.6.2 shazamio==0.7.0 streamrip==2.0.5 diff --git a/util/jesusmemes.py b/util/jesusmemes.py index 6ba20c2..b6c8a85 100644 --- a/util/jesusmemes.py +++ b/util/jesusmemes.py @@ -52,9 +52,7 @@ class JesusMemeGenerator: if len(top_line) < 1 or len(bottom_line) < 1: return None - formed_url: str = ( - f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}" - ) + formed_url: str = f"{self.MEMEAPIURL}{meme}&top={top_line.strip()}&bottom={bottom_line.strip()}" formed_url = self.url_regex_1.sub( "+", self.url_regex_2.sub("%23", formed_url.strip()) ) diff --git a/util/misc_util.py b/util/misc_util.py index d132893..df81dbd 100644 --- a/util/misc_util.py +++ b/util/misc_util.py @@ -257,7 +257,7 @@ class Util: except Exception as e: traceback.print_exc() return (term, f"ERR: {str(e)}") - + async def get_no(self) -> str: try: async with ClientSession() as session: @@ -266,19 +266,20 @@ class Util: headers={ "content-type": "application/json; charset=utf-8", }, - timeout=ClientTimeout(connect=5, sock_read=5) + timeout=ClientTimeout(connect=5, sock_read=5), ) as request: request.raise_for_status() - response = await request.json() - no: str = response.get('no', None) + response = await request.json(encoding="utf-8") + no: str = response.get("no", None) if not no: - logging.debug("Incorrect response received, JSON keys: %s", - response.keys()) + logging.debug( + "Incorrect response received, JSON keys: %s", + response.keys(), + ) return "No." return no except Exception as e: - logging.debug("Exception: %s", - str(e)) + logging.debug("Exception: %s", str(e)) return "No." async def get_insult(self, recipient: str) -> str: @@ -323,13 +324,11 @@ class Util: if not whisky_db: return None async with sqlite3.connect(database=whisky_db, timeout=2) as db_conn: - db_query: str = ( - "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1" - ) + db_query: str = "SELECT name, category, description FROM whiskeys ORDER BY random() LIMIT 1" async with await db_conn.execute(db_query) as db_cursor: - db_result: Optional[Union[sqlite3.Row, tuple]] = ( - await db_cursor.fetchone() - ) + db_result: Optional[ + Union[sqlite3.Row, tuple] + ] = await db_cursor.fetchone() if not db_result: return None (name, category, description) = db_result @@ -398,9 +397,7 @@ class Util: async with sqlite3.connect(database=strains_db, timeout=2) as db_conn: db_params: Optional[tuple] = None if not strain: - db_query: str = ( - "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1" - ) + db_query: str = "SELECT name, description FROM strains_w_desc ORDER BY random() LIMIT 1" else: db_query = ( "SELECT name, description FROM strains_w_desc WHERE name LIKE ?" @@ -442,9 +439,7 @@ class Util: if not rjokes_db: return None async with sqlite3.connect(database=rjokes_db, timeout=2) as db_conn: - db_query: str = ( - "SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'" - ) + db_query: str = "SELECT title, body, score FROM jokes WHERE score >= 100 ORDER BY RANDOM() LIMIT 1'" async with await db_conn.execute(db_query) as cursor: (title, body, score) = await cursor.fetchone() return (title, body, score) diff --git a/util/sing_util.py b/util/sing_util.py index e808ecd..9abc9d0 100644 --- a/util/sing_util.py +++ b/util/sing_util.py @@ -82,7 +82,9 @@ class Utility: if ( search_split_by == ":" and len(song.split(":")) > 2 ): # Support sub-search if : is used (per instructions) - search_song = song.split(search_split_by)[ + search_song = song.split( + search_split_by + )[ 1 ].strip() # Reduce search_song to only the 2nd split of : [the rest is meant to be lyric text] search_subsearch = "".join(