yor-discord-bot / cogs / daily_sketchy_sketch.py
daily_sketchy_sketch.py
Raw
"""This file contains the DailySketchySketch Discord Cog extension.
"""
import os, discord, requests
from discord import NotFound
from discord.ext import commands
from asyncio import sleep
from bs4 import BeautifulSoup as bs
from collections import OrderedDict as odict


from py_files.sketchy_sketch import sketchySketch


class DailySketchySketch(commands.Cog):
    """
    Instantiates the dailySketchySketch class.
    """
    def __init__(self, bot):
        self.bot = bot
        ### === BOT SETTINGS === ###
        self.dir_path = r'C:\Users\haose\OneDrive\Desktop\irl_references'
        # self.dir_path = r'C:\Users\haose\OneDrive\Desktop\test_refs'
        self._cache_size = 50
        # only channels listed here are authorized to use this class
        self._authorized_channels = \
            {
                'hoes-hoes-๐Ÿ–๐Ÿ–๐Ÿ–', 
                'hoes-hoes-2-๐Ÿ‘๐Ÿ‘๐Ÿ‘', 
                'testing'
            }
        self._reactions = \
            {
                'crossmark':'โŒ',
                'checkmark':'โœ…',
                'bookmark':'๐Ÿ”–',
                'leftarrow':'โฌ…๏ธ',
                'rightarrow':'โžก๏ธ'
            }
        ### === END === ###
        self._sketch = sketchySketch(self.dir_path)
        self._secret_channel = None
        # caches sent messages, saves { message id : file information }
        self._sent_img = odict()
        self._sent_bookmarked = odict()
        self._sent_deleted = odict()
        
    @commands.command(name='gimmesum')
    async def random_pic(self, ctx, num=1):
        """
        Sends a random image to the text channel. The text channel must be
        an authorized text channel.
        """
        # check if channel is authorized
        if ctx.message.channel.name not in self._authorized_channels:
            await ctx.send('No.')
            return

        # remove oldest message
        if (len(self._sent_img) > self._cache_size):
            self._sent_img.popitem(last=False)

        # max number of requests
        max_request = 20
        if num > max_request:
            await ctx.send("That's too much!")
            num = max_request
        
        # send image(s) to the text channel
        for _ in range(num):
            file_path = self._sketch.random_pic()
            prev_msg = await ctx.send(file=discord.File(file_path))
            # cache sent image
            self._sent_img[prev_msg.id] = file_path
            # add reaction to message
            await prev_msg.add_reaction(self._reactions.get('crossmark'))
            await prev_msg.add_reaction(self._reactions.get('bookmark'))
            await sleep(0.5)


    @commands.Cog.listener()
    async def on_reaction_add(self, reaction, user):
        """
        Performs operations based on reactions in the text channel.
        """
        message = reaction.message
        # check if author is a bot or if reaction is added in authorized channels
        if ((user == self.bot.user) or (message.channel.name not in self._authorized_channels)):
            return

        
        # === REMOVES USER REACTION ON REACT === #
        
        if (message.id in self._sent_bookmarked or message.id in self._sent_deleted):
            try:
                await reaction.remove(user)
            except NotFound:
                pass

        
        # === ADDS IMG ON REACT === #
        # Adds the image to directory if reacted with checkmark.
        
        if (reaction.emoji == self._reactions.get('checkmark')):
            for msg_attachment in message.attachments:
                if 'image' in msg_attachment.content_type:
                    ### TODO: Check for duplicate file names
                    ### IDEA: Use message id as file name instead
                    self._sketch.add_pic(msg_attachment.filename)
                    await msg_attachment.save(os.path.join(self.dir_path, msg_attachment.filename))

        
        # === REMOVES IMG ON REACT === #
        # Only removes images sent in the previous %gimmesum call.
        
        if ((message.id in self._sent_img) and (reaction.emoji == self._reactions.get('crossmark'))):
            # move deleted image to recycling bin
            delete_imgs = set()
            for msg_attachment in message.attachments:
                delete_imgs.add(msg_attachment.filename)
                self._sketch.remove_pic(self._msg_attachment.filename)
            # send status message
            await message.channel.send('Deleted!')
            await message.delete()

        
        # === BOOKMARKS IMG ON REACT === #
        # Only bookmarks images sent in the previous %gimmesum call.
        
        if ((message.id in self._sent_img) and (reaction.emoji == self._reactions.get('bookmark'))):
            for msg_attachment in message.attachments:
                self._sketch.bookmark_pic(file_name=msg_attachment.filename)
            # send status message
            await message.channel.send('Bookmarked!')
        
        
        # === TURNS PAGES & UNBOOKMARK FOR BOOKMARK EMBEDS === #
        if (message.id in self._sent_bookmarked):
            # self._sent_bookmarked: {message.id:[embed_pages, index], ...}
            # embed_pages = [(file_name, disc_embed), ...]
            bookmarked = self._sent_bookmarked.get(message.id)
            embed_pages = bookmarked[0]
            index = bookmarked[1]
            # prev page
            if (reaction.emoji == self._reactions.get('leftarrow')):
                index = (index - 1) % len(embed_pages)
                # update embed
                await message.edit(embed=embed_pages[index][1])
            # next page
            elif (reaction.emoji == self._reactions.get('rightarrow')):
                index = (index + 1) % len(embed_pages)
                # update embed
                await message.edit(embed=embed_pages[index][1])
            # unbookmark item
            elif (reaction.emoji == self._reactions.get('bookmark')):
                self._sketch.unbookmark_pic(embed_pages[index][0])
            # delete item
            elif (reaction.emoji == self._reactions.get('crossmark')):
                self._sketch.unbookmark_pic(embed_pages[index][0])
                self._sketch.remove_pic(embed_pages[index][0])
            
            # update index
            self._sent_bookmarked.get(message.id)[1] = index
                 

    @commands.Cog.listener()
    async def on_message(self, message):
        """
        Adds the image file sent in the previous message to the directory.
        """
        # check if author is a bot or if message is sent in authorized channels
        if (message.author.bot or (message.channel.name not in self._authorized_channels)):
            return
        # check if message contains an image
        for msg_attachment in message.attachments:
            if 'image' in msg_attachment.content_type:
                # content is image
                await message.add_reaction(self._reactions.get('checkmark'))


    @commands.command(name='gimmebookies')
    async def send_bookmarked(self, ctx):
        """
        Sends bookmarked pictures as embeds.
        """
        # remove oldest message
        if len(self._sent_bookmarked) > self._cache_size:
            self._sent_bookmarked.popitem(last=False)

        # send status message
        await ctx.send("Retrieving bookmarked images!")

        # create secret channel - not designed with multiple guilds in mind
        await self._update_secret_channel(ctx.guild)

        # get embeds
        embed_pages = await self._embed_pages(self._sketch.list_bookmarked(), title='Bookmarked Images')
        if (len(embed_pages) > 0):
            # send first page of bookmarked
            sent_msg = await ctx.send(embed=embed_pages[0][1])
            await sent_msg.add_reaction(self._reactions.get('leftarrow'))
            await sent_msg.add_reaction(self._reactions.get('rightarrow'))
            await sent_msg.add_reaction(self._reactions.get('bookmark'))
            await sent_msg.add_reaction(self._reactions.get('crossmark'))
            # update bookmarked dict, stores list of embeds and current page index
            self._sent_bookmarked[sent_msg.id] = [embed_pages, 0]
        else:
            await ctx.send("There are no bookmarked images.")


    async def _update_secret_channel(self, guild):
        """
        Update the TextChannel for YorBot's secret channel from a given Guild.
        """
        # check if secret channel already exists
        tc_name = 'yor-secret-channel'
        guild_tc = guild.text_channels
        for i in range(len(guild_tc)):
            if (tc_name == str(guild_tc[i])):
                self._secret_channel = guild_tc[i]
                return
        # new channel settings
        tc_overwrites = {
            guild.default_role: discord.PermissionOverwrite(read_messages=False),
            guild.me: discord.PermissionOverwrite(read_messages=True)
        }
        # send temp imgs to retrieve url
        self._secret_channel = await guild.create_text_channel(name=tc_name, overwrite=tc_overwrites)


    async def _embed_pages(self, file_list, title):
        """
        Returns a list of formatted embeds from the list of file and title.
        """
        pages = [] # stores bookmarked pages
        for page_num, file_path in enumerate(file_list):
            # grab information on file
            file_name = os.path.split(file_path)[-1]
            pic_info = self._sketch.pic_info(file_name)
            # check if image url exists or is valid
            
            ### BROKE BEGIN
            response = requests.get(pic_info['url'])
            url_dne = False
            if (response.status_code == 404):
                url_dne = True
            elif (response.status_code == 200):
                err_msg = 'This XML file does not appear to have any style information\
                           associated with it. The document tree is shown below.'
                soup = bs(response.content, 'html.parser').find(class_='header')
                # header class exists
                if soup:
                    url_dne = True
            ### BROKE END

            # update image url
            if (not pic_info['url'] or url_dne):
                # url does not exist or is invalid
                temp_msg = await self._secret_channel.send(file=discord.File(file_path))
                # get attachment from temp file
                img_url = temp_msg.attachments[0].url
                self._sketch.update_url(file_name, img_url)
                await sleep(0.5)

            # update file info
            pic_info = self._sketch.pic_info(file_name)
            # format embed
            embed_desc = f'**rating: **{pic_info["rating"]}/10\n' + \
                         f'**tags: **{str(pic_info["tags"])}\n'
            disc_embed = discord.Embed.from_dict({'title':title, 'description':embed_desc})
            disc_embed.set_image(url=pic_info['url'])
            # add footer & color
            disc_embed.set_footer(text=f'{str(page_num + 1)}/{str(len(file_list))}')
            disc_embed.color = discord.Color.from_rgb(255, 214, 232) # pastel pink
            # add to embed pages
            pages.append((file_name, disc_embed))
        return pages


    @commands.command(name='tag')
    async def tag_pic(self, ctx):
        """
        Adds the given tag to the previously sent image.
        """
        pass


    @commands.Cog.listener()
    async def clean_up(self):
        pass
    

async def setup(bot):
    await bot.add_cog(DailySketchySketch(bot))