diff --git a/README.md b/README.md index 27a56cf..4e89d05 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,16 @@ Memerr A discord bot to manage you and your friend's meme collection +You can get this on [Docker](https://hub.docker.com/r/clortox/memerr) Features ---- -- [ ] Run in a docker container -- [ ] Query for memes by name -- [ ] Fuzzy search for memes -- [ ] Get a random meme -- [ ] Get a set of memes +- [X] Run in a docker container +- [X] Query for memes by name +- [X] Fuzzy search for memes +- [X] Get a random meme +- [X] Get a set of memes - [ ] Upload memes to bucket - [ ] Check status of Plex instance diff --git a/make_venv.sh b/make_venv.sh index c01a955..61de777 100755 --- a/make_venv.sh +++ b/make_venv.sh @@ -1,13 +1,17 @@ #!/bin/bash -mkdir ./venv 2> /dev/null - -python -m venv ./venv +if [ ! -d "./venv" ]; then + mkdir ./venv 2> /dev/null + python -m venv ./venv +fi cp ./src/* ./venv/ cp ./src/.env ./venv/.env 2> /dev/null cd ./venv +pip install -r requirements.txt --user touch .env +echo "evn $(cat .env | xargs) python ./main.py" >> run.sh +chmod +x run.sh env $(cat .env | xargs) python ./main.py diff --git a/src/main.py b/src/main.py index 1e77eaf..c437dea 100755 --- a/src/main.py +++ b/src/main.py @@ -24,6 +24,8 @@ client = discord.Client() async def on_message(message): if message.author == client.user: return + if len(message.content) == 0: # if its empty, like an uploaded image, ignore it + return return await parse_message(client, message) # messege delete handler diff --git a/src/memes.py b/src/memes.py index 8e710ea..af5241e 100644 --- a/src/memes.py +++ b/src/memes.py @@ -7,8 +7,22 @@ from minio.error import S3Error from catbox import Uploader # memes we have most recently gotten -last_memes = [] -last_memes_max = 20 +class RecentMemeQueue: + #last_memes = [] + #last_memes_max = 20 + def __init__(self): + self.last_memes = [] + self.last_memes_max = 20 + def insertMeme(self, meme): + self.last_memes.insert(0, meme) + if len(self.last_memes) > self.last_memes_max: + self.last_memes.pop() + def inQueue(self, meme) -> bool: + return meme in self.last_memes + def getList(self): + return self.last_memes + +recentMemes = RecentMemeQueue() # list of all current memes' names all_memes = [] @@ -72,14 +86,16 @@ def getCurrentMemeList(): global all_memes now = datetime.now() # if no update in the past 5 mins - if (last_checked_all_memes - now).seconds / 60 > 5: + if (now - last_checked_all_memes).seconds > 300: + print("Enough time has elapsed, refreshing meme cache...") last_checked_all_memes = now all_memes.clear() myClient = getClient() for obj in myClient.list_objects(S3_BUCKET): if not obj.is_dir: - print(f'{obj.object_name}') + #print(f'{obj.object_name}') all_memes.append(obj.object_name) + print(f"Got {len(all_memes)} memes") return all_memes # given a file, we will return: @@ -87,7 +103,6 @@ def getCurrentMemeList(): # A catbox.moe link if its larger than 8mb def getDiscordReadyObjectFromS3(file_name): size = 0 - out_file_path = "/tmp/" + file_name #get the file client = getClient() stream = "" @@ -101,12 +116,58 @@ def getDiscordReadyObjectFromS3(file_name): # check the object size if size >= 8000000: # to big, use catbox + print(f"{file_name} was to big, uploading to catbox...") catbox_uploader = Uploader(token='') - url = catbox_uploader.upload(file_raw=stream) + extension = file_name.split('.')[-1] + if len(extension) == 0: + extension = None + url = catbox_uploader.upload(file_raw=stream, file_type=extension) return url["file"] else: #small enough, use discord + print(f"{file_name} was small enough to send using discord") return discord.File(fp=io.BytesIO(stream), filename=file_name) +# get a meme whos name closest matches the provided query +def getCloseMemeToQuery(query, returnAllClose = False): + global all_memes + all_memes = getCurrentMemeList() + top_meme = '' + top_memes = [] + top_score = 0 + + for meme in all_memes: + current_score = fuzz.partial_ratio(query, meme) + if current_score > top_score: + top_meme = meme + top_score = current_score + if current_score == 100: + top_memes.append(meme) + top_meme = meme + + print("Top memes we found:") + if len(top_memes) > 0: + print(top_memes) + if top_meme != '': + print(top_meme) + + # if there was one or more perfect matches, get them + if top_score == 100: + top_meme = random.choice(top_memes) + if not all(elem in recentMemes.getList() for elem in top_memes): + while top_meme in recentMemes.getList(): + top_meme = random.choice(top_memes) + recentMemes.insertMeme(top_meme) + + if returnAllClose and len(top_memes) > 1: + print("Returning all found memes...") + for meme in top_memes: + recentMemes.insertMeme(meme) + return top_memes, 1 if len(top_memes) <= 1 else len(top_memes) + else: + print(f"Returning {top_meme}...") + recentMemes.insertMeme(top_meme) + return top_meme, 1 if len(top_memes) <= 1 else len(top_memes) + # get the name of the user who uploaded the file # their tag is always under the 'uploader' tag def getMemeUploader(file_name): @@ -118,6 +179,17 @@ def getMemeUploader(file_name): return uploader return "Unkown" +# gets a random meme +# this will ensure that the meme is not in the last_memes list +def getRandomMeme(): + all_memes = getCurrentMemeList() + choice = random.choice(all_memes) + while recentMemes.inQueue(choice): + choice = random.choice(all_memes) + recentMemes.insertMeme(choice) + return choice + + ############################################################################### # Calldict methods @@ -133,24 +205,56 @@ async def parseMeme(command, message, client): if len(command) > 1: # if there is a query query_string = ' '.join(command[1:]) #get QUERY_STRING - all_memes = getCurrentMemeList() #first, check if the exact meme exists + all_memes = getCurrentMemeList() if query_string in all_memes: # found the exact meme, return it return getDiscordReadyObjectFromS3(query_string) #if we get here then we didnt find the meme, lets find the closest meme + close_meme, total_close_memes = getCloseMemeToQuery(query_string) + await message.channel.send(f"Found a close meme (from {total_close_memes} choices)") + return getDiscordReadyObjectFromS3(close_meme) else: #there is no query, get a random meme - pass + print(f'Getting a random meme...') + meme = getRandomMeme() + print(f'Got a random meme ({meme})') + return getDiscordReadyObjectFromS3(meme) # get all ememes that are similar to the given query async def allMemes(command, message, client): - pass + if len(command) > 1: + query_string = ' '.join(command[1:]) + close_memes, total_close_memes = getCloseMemeToQuery(query_string, + returnAllClose=True) + if total_close_memes == 0: + return "I found nothing similar to " + query_string + elif total_close_memes == 1: + return getDiscordReadyObjectFromS3(close_memes) + else: + for meme in close_memes: + ready_meme = getDiscordReadyObjectFromS3(meme) + if isinstance(ready_meme, str): + await message.channel.send(ready_meme) + else: + await message.channel.send(file=ready_meme) + return "Enjoy your memes :)" + else: + return "I need a query!" # get total number of memes stored async def memeCount(command, message, client): - pass + all_memes = getCurrentMemeList() + return "Currently we have " + str(len(all_memes)) + " memes" # get a set of random memes async def memeDump(command, message, client): - pass + await message.channel.send("Getting your memes...") + for i in range(0,5): + meme = getRandomMeme() + ready_meme = getDiscordReadyObjectFromS3(meme) + if isinstance(ready_meme, str): + await message.channel.send(ready_meme) + else: + await message.channel.send(file=ready_meme) + return "Enjoy your memes :)" diff --git a/src/on_message.py b/src/on_message.py index c322560..d583247 100644 --- a/src/on_message.py +++ b/src/on_message.py @@ -4,6 +4,21 @@ import os, random, sys from memes import parseMeme, memeCount, memeDump, allMemes import discord +async def showHelp(very, useless, arguments): + str = "```Usage:\n" + str += "!meme [QUERY]\n" + str += " Get a meme. Query for the exact name or search. Paramater is optional\n" + str += "!memecount\n" + str += " Print total number of memes in the store\n" + str += "!memedump\n" + str += " Get 5 random memes\n" + str += "!allmemes QUERY\n" + str += " Get all memes that are like the given query\n" + str += "!help\n" + str += " Print this message\n" + str += "```" + return str + # dictionary associating # a command to a function name calldict = { @@ -12,10 +27,12 @@ calldict = { "!memecount" : memeCount, "!memedump" : memeDump, "!allmemes" : allMemes, + "!help" : showHelp, # plex #"!plexleaderboard" : getTopUsers, } + # parse the message async def parse_message(client, message): command = message.content.split() @@ -25,8 +42,10 @@ async def parse_message(client, message): print(f'{client.user} was passed a command I know! ({message.content})') result = await calldict[command[0]](command, message, client) if isinstance(result, discord.File): + print(f'{client.user} is replying with a file ({result.filename})') return await message.channel.send(file=result) else: + print(f'{client.user} is replying with the string ({result})') return await message.channel.send(result) pass