<commit_before>import discord
from discord.ext import commands
import aiohttp
import asyncio
import datetime
from utils.helpers import TimeParser
from utils.checks import has_perm, is_ml

class MuvLuv(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.ml_guild = 169056767219597312
        self.ml_announce = self.bot.get_channel(235502028930023424)
        self.ml_invite = "https://discord.gg/8J7ANjx"
        self.ml_roles = {358065563164999690: [discord.Object(id=188030091148656641)],        # cadet
                         358066129345708032: [discord.Object(id=173104384392167425),
                                              discord.Object(id=188030091148656641)],  # eishi
                         358066125696794624: [discord.Object(id=173100534474080257),
                                              discord.Object(id=173104384392167425),
                                              discord.Object(id=188030091148656641)],  # valk
                         593245068702253076: [discord.Object(id=360896131015639042)],    # lewd
                         593245318888292383: [discord.Object(id=180756419174203393)]}    # spoiler

    @commands.command(enabled=False)
    @has_perm("kick_members")
    @is_ml()
    async def mute(self, ctx, target: discord.Member, time: TimeParser=0, *, reason=""):
        """Mutes a member for [time], with an optional reason"""
        if time.seconds < 1:
            self.bot.reply("Please use a valid time")
            return
        mute_role = discord.utils.get(ctx.guild.roles, id=203241764260151296)
        await self.bot.add_roles(target, mute_role)
        ret = " | Reason: " + reason if reason else ""
        await self.ml_announce.send("@here {} muted {} for {} seconds".format(ctx.message.author.mention, target.mention, time.seconds) + ret)

        await asyncio.sleep(time.seconds)
        await self.bot.remove_roles(target, mute_role)
        await self.ml_announce.send("{} has been unmuted".format(target.mention))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_join(self, member):
        create_delta = datetime.datetime.utcnow() - discord.utils.snowflake_time(member.id)
        create_list = ["today", "a day ago", "two days ago", "a few days ago", "a few days ago", "a few days ago", "a few days ago", "a week ago", "a week ago", "a week ago"]
        create_str = "A while ago " if create_delta.days > 9 else create_list[create_delta.days]

        await self.ml_announce.send("@here Member joined: {a.name} [{a.mention}] created {b} [{c} ago]".format(a=member, b=create_str, c=str(create_delta)))

        if member.bot:
            return
        await member.add_roles(discord.utils.get(member.guild.roles, id=588420756208353291))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_remove(self, member):
        await self.ml_announce.send("Member left: {a.name}, ({a.id})".format(a=member))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_ban(self, guild, member):
        await self.ml_announce.send("🔨🔨 Member banned: {a.name}, ({a.id}) 🔨🔨".format(a=member))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_unban(self, guild, user):
        await self.ml_announce.send("Member unbanned: {a.name}, ({a.id})".format(a=user))

    @commands.Cog.listener()
    @is_ml()
    async def on_message_edit(self, before, after):
        if before.content == after.content:
            return
        if before.channel == self.ml_announce:
            return
        ret = "-{}\n+{}".format(before.clean_content, after.clean_content)
        if len(ret) > 1850:
            async with aiohttp.ClientSession() as session:
                async with session.post("https://hastebin.com/documents", data=ret) as r:
                    resp = await r.json()
                    try:
                        hb_url = "https://hastebin.com/{}".format(resp["key"])
                    except KeyError:
                        hb_url = "There was an error uploading to hb: {}".format(resp)
            await self.ml_announce.send("{} in #{} edited a pretty big message, hastebin link: {}.diff".format(str(after.author), after.channel.name, hb_url))
            return
        await self.ml_announce.send("{} in #{} edited a message:\n ```diff\n{}```".format(str(after.author), after.channel.name, ret))

    @commands.Cog.listener()
    @is_ml()
    async def on_message_delete(self, message):
        if message.channel == self.ml_announce:
            return
        await asyncio.sleep(3)
        deleter = None
        async for entry in message.guild.audit_logs(limit=3, action=discord.AuditLogAction.message_delete):
            if abs(datetime.datetime.utcnow().timestamp() - discord.utils.snowflake_time(entry.id).timestamp()) < 8:
                deleter = entry.user.id
        # audit log check: if none then self delete(or bot), else get deleter
        if len(message.clean_content) > 1900:
            async with aiohttp.ClientSession() as session:
                async with session.post("https://hastebin.com/documents", data=message.clean_content) as r:
                    resp = await r.json()
                    try:
                        hb_url = "https://hastebin.com/{}".format(resp["key"])
                    except KeyError:
                        hb_url = "There was an error uploading to hb: {}".format(resp)
            if deleter:
                await self.ml_announce.send("in #{} a pretty big message by {} was deleted by {}, hastebin link: {}".format(message.channel.name, str(message.author), str(message.guild.get_member(deleter)), hb_url))
                return
            await self.ml_announce.send("{} in #{} deleted a pretty big message, hastebin link: {}".format(str(message.author), message.channel.name, hb_url))
            return
        if deleter:
            await self.ml_announce.send("in #{} a message by {} was deleted by {}:\n```{}```".format(message.channel.name, str(message.author), str(message.guild.get_member(deleter)), message.clean_content))
            return
        await self.ml_announce.send("{} in #{} deleted a message:\n```{}```".format(str(message.author), message.channel.name, message.clean_content))

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload):
        if payload.channel_id != 588421329590419456:
            return
        # if payload.emoji.id == 358065563164999690:  # cadet
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=188030091148656641)])
        # if payload.emoji.id == 358066129345708032:  # eishi
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=173104384392167425)])
        # if payload.emoji.id == 358066125696794624:  # valk
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=173100534474080257)])
        # if payload.emoji.id == 593245318888292383:  # spoiler (check2)
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=180756419174203393)])
        # if payload.emoji.id == 593245068702253076:  # lewd (check1)
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=360896131015639042)])
        if payload.emoji.id == 593245363918602260:  # done (check3)
            await asyncio.sleep(1)
            await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).remove_roles(*[discord.Object(id=588420756208353291)])
        await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*self.ml_roles[payload.emoji.id])

def setup(bot):
    bot.add_cog(MuvLuv(bot))
<commit_msg>Update MuvLuv.py<commit_after>import discord
from discord.ext import commands
import aiohttp
import asyncio
import datetime
from .utils.helpers import TimeParser
from .utils.checks import has_perm, is_ml

class MuvLuv(commands.Cog):
    def __init__(self, bot):
        self.bot = bot
        self.ml_guild = 169056767219597312
        self.ml_announce = self.bot.get_channel(235502028930023424)
        self.ml_invite = "https://discord.gg/8J7ANjx"
        self.ml_roles = {358065563164999690: [discord.Object(id=188030091148656641)],        # cadet
                         358066129345708032: [discord.Object(id=173104384392167425),
                                              discord.Object(id=188030091148656641)],  # eishi
                         358066125696794624: [discord.Object(id=173100534474080257),
                                              discord.Object(id=173104384392167425),
                                              discord.Object(id=188030091148656641)],  # valk
                         593245068702253076: [discord.Object(id=360896131015639042)],    # lewd
                         593245318888292383: [discord.Object(id=180756419174203393)]}    # spoiler

    @commands.command(enabled=False)
    @has_perm("kick_members")
    @is_ml()
    async def mute(self, ctx, target: discord.Member, time: TimeParser=0, *, reason=""):
        """Mutes a member for [time], with an optional reason"""
        if time.seconds < 1:
            self.bot.reply("Please use a valid time")
            return
        mute_role = discord.utils.get(ctx.guild.roles, id=203241764260151296)
        await self.bot.add_roles(target, mute_role)
        ret = " | Reason: " + reason if reason else ""
        await self.ml_announce.send("@here {} muted {} for {} seconds".format(ctx.message.author.mention, target.mention, time.seconds) + ret)

        await asyncio.sleep(time.seconds)
        await self.bot.remove_roles(target, mute_role)
        await self.ml_announce.send("{} has been unmuted".format(target.mention))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_join(self, member):
        create_delta = datetime.datetime.utcnow() - discord.utils.snowflake_time(member.id)
        create_list = ["today", "a day ago", "two days ago", "a few days ago", "a few days ago", "a few days ago", "a few days ago", "a week ago", "a week ago", "a week ago"]
        create_str = "A while ago " if create_delta.days > 9 else create_list[create_delta.days]

        await self.ml_announce.send("@here Member joined: {a.name} [{a.mention}] created {b} [{c} ago]".format(a=member, b=create_str, c=str(create_delta)))

        if member.bot:
            return
        await member.add_roles(discord.utils.get(member.guild.roles, id=588420756208353291))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_remove(self, member):
        await self.ml_announce.send("Member left: {a.name}, ({a.id})".format(a=member))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_ban(self, guild, member):
        await self.ml_announce.send("🔨🔨 Member banned: {a.name}, ({a.id}) 🔨🔨".format(a=member))

    @commands.Cog.listener()
    @is_ml()
    async def on_member_unban(self, guild, user):
        await self.ml_announce.send("Member unbanned: {a.name}, ({a.id})".format(a=user))

    @commands.Cog.listener()
    @is_ml()
    async def on_message_edit(self, before, after):
        if before.content == after.content:
            return
        if before.channel == self.ml_announce:
            return
        ret = "-{}\n+{}".format(before.clean_content, after.clean_content)
        if len(ret) > 1850:
            async with aiohttp.ClientSession() as session:
                async with session.post("https://hastebin.com/documents", data=ret) as r:
                    resp = await r.json()
                    try:
                        hb_url = "https://hastebin.com/{}".format(resp["key"])
                    except KeyError:
                        hb_url = "There was an error uploading to hb: {}".format(resp)
            await self.ml_announce.send("{} in #{} edited a pretty big message, hastebin link: {}.diff".format(str(after.author), after.channel.name, hb_url))
            return
        await self.ml_announce.send("{} in #{} edited a message:\n ```diff\n{}```".format(str(after.author), after.channel.name, ret))

    @commands.Cog.listener()
    @is_ml()
    async def on_message_delete(self, message):
        if message.channel == self.ml_announce:
            return
        await asyncio.sleep(3)
        deleter = None
        async for entry in message.guild.audit_logs(limit=3, action=discord.AuditLogAction.message_delete):
            if abs(datetime.datetime.utcnow().timestamp() - discord.utils.snowflake_time(entry.id).timestamp()) < 8:
                deleter = entry.user.id
        # audit log check: if none then self delete(or bot), else get deleter
        if len(message.clean_content) > 1900:
            async with aiohttp.ClientSession() as session:
                async with session.post("https://hastebin.com/documents", data=message.clean_content) as r:
                    resp = await r.json()
                    try:
                        hb_url = "https://hastebin.com/{}".format(resp["key"])
                    except KeyError:
                        hb_url = "There was an error uploading to hb: {}".format(resp)
            if deleter:
                await self.ml_announce.send("in #{} a pretty big message by {} was deleted by {}, hastebin link: {}".format(message.channel.name, str(message.author), str(message.guild.get_member(deleter)), hb_url))
                return
            await self.ml_announce.send("{} in #{} deleted a pretty big message, hastebin link: {}".format(str(message.author), message.channel.name, hb_url))
            return
        if deleter:
            await self.ml_announce.send("in #{} a message by {} was deleted by {}:\n```{}```".format(message.channel.name, str(message.author), str(message.guild.get_member(deleter)), message.clean_content))
            return
        await self.ml_announce.send("{} in #{} deleted a message:\n```{}```".format(str(message.author), message.channel.name, message.clean_content))

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload):
        if payload.channel_id != 588421329590419456:
            return
        # if payload.emoji.id == 358065563164999690:  # cadet
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=188030091148656641)])
        # if payload.emoji.id == 358066129345708032:  # eishi
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=173104384392167425)])
        # if payload.emoji.id == 358066125696794624:  # valk
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=173100534474080257)])
        # if payload.emoji.id == 593245318888292383:  # spoiler (check2)
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=180756419174203393)])
        # if payload.emoji.id == 593245068702253076:  # lewd (check1)
        #     await asyncio.sleep(1)
        #     await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*[discord.Object(id=360896131015639042)])
        if payload.emoji.id == 593245363918602260:  # done (check3)
            await asyncio.sleep(1)
            await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).remove_roles(*[discord.Object(id=588420756208353291)])
        await self.bot.get_guild(self.ml_guild).get_member(payload.user_id).add_roles(*self.ml_roles[payload.emoji.id])

def setup(bot):
    bot.add_cog(MuvLuv(bot))
<|endoftext|>


<commit_before>import discord
from discord.ext import commands

import re
import io
import PIL.Image

class Emoji:
    def __init__(self, bot):
        self.bot = bot
        self.raw_emoji_regex = "(<(a|):([a-zA-Z0-9_*/-:]*):([0-9]*)>)"
        self.emoji_regex = "(<(a|):[a-zA-Z0-9_*/-:]*:[0-9]*>)"
        self.bot.extract_emojis = self.extract_emojis
        self.bot.download_and_add_emoji = self.download_and_add_emoji
        self.emoji_guild_id = int(self.bot.config['base']['emoji-guild'])
        self.max_jumbo = 5
        self.max_emoji_size = 256 * 1024
        self.emoji_dim_max = 128

    def extract_emojis(self, text: str, raw=False):
        regex_to_use = self.raw_emoji_regex if raw else self.emoji_regex
        regex_results = re.findall(regex_to_use, text)
        if raw:
            return regex_results
        emojis = list(dict(regex_results).keys())
        return emojis

    async def resize_emoji_png(self, image_bytes, no_none=False, check_size=True):
        """Resizes a given emoji to the hardcoded max emoji dimensions of discord.

        If no_none (default: False) is set to True, function return old image data if it fails to resize and stay under size limitations. If it is set to false, function return None.

        If check_size (default: True) is set to True, function will return None or emoji data depending on no_none if emoji is bigger than discord's maximum size, which is currently hardcoded to 256kb. If it is set to false, function will return the image no matter the size (to ensure that you never get None, even when image is smaller than discord dimensions yet too big, set no_none to True)."""

        bigger_than_max = len(image_bytes) > self.max_emoji_size
        # Prepare fail result so that we can use tertiary if in
        # other palces and not have terrible code
        fail_return = image_bytes if no_none else None

        # Load image from bytes
        im = PIL.Image.open(io.BytesIO(image_bytes))
        w, h = im.size

        # Check if dimensions are smaller or equal to hardcoded max size
        if w <= self.emoji_dim_max and h <= self.emoji_dim_max:
            # Return fail result (image bytes or None) if image is bigger
            # than limit, otherwise return image_bytes, as it's smaller
            # than size and dimension limit
            return fail_return if (check_size and bigger_than_max) else image_bytes
        elif w > h:
            # Get height to hardcoded discord max size ratio
            # so we can we scale down height
            ratio = w / self.emoji_dim_max
            # Scale down height by ratio determined on prev step
            # and turn it to int so that PIL doesn't complain
            new_h = int(h / ratio)
            im = im.resize((self.emoji_dim_max, new_h))
        # If both dimensions are equal or height is bigger than width
        # resize based on height
        else:
            ratio = h / self.emoji_dim_max
            new_w = int(w / ratio)
            im = im.resize((new_w, self.emoji_dim_max))

        # Create a BytesIO storage to store image
        emoji_bytes = io.BytesIO()
        im.save(emoji_bytes, format='PNG')
        emoji_bytes = emoji_bytes.getvalue()

        # If check_size and emoji size is bigger than discord limit
        # Return fail_return (None or base byte data, based on no_none)
        # if it is not, return resized emoji
        if check_size and (len(emoji_bytes) > self.max_emoji_size):
            return fail_return
        return emoji_bytes

    async def resize_emoji_gif(self, image_bytes):
        """INCOMPLETE: Currently checks if a gif is smaller than max limit and returns image bytes back, and if it is not, returns None."""
        bigger_than_max = len(image_bytes) > self.max_emoji_size
        return None if bigger_than_max else image_bytes

    async def download_and_add_emoji(self, guild_id: int, emoji_name: str, url: str):
        emoji_guild = self.bot.get_guild(guild_id)
        emoji_bytes = await self.bot.aiogetbytes(url)
        result_bytes = None

        filename = await self.bot.url_get_filename(url)
        file_ext = await self.bot.filename_get_ext(filename)

        if file_ext.lower() == "gif":
            result_bytes = await self.resize_emoji_gif(emoji_bytes)
        else:
            result_bytes = await self.resize_emoji_png(emoji_bytes)

        # Check if result is None or not, returns None if it is
        # If it is not, adds emoji and returns that
        if result_bytes:
            self.bot.log.info(f"Emoji resize process complete, byte size: {len(result_bytes)}")
            added_emoji = await emoji_guild.create_custom_emoji(name=emoji_name, image=emoji_bytes)
            return added_emoji
        return None

    def construct_emoji_url(self, emoji_id, emoji_format):
        return f"https://cdn.discordapp.com/emojis/{emoji_id}.{emoji_format}?v=1"

    @commands.command(aliases=['avemoji', 'avemojiinvite', 'avemojisinvite', 'ainvite'])
    async def avemojis(self, ctx):
        """Gives an invite link to Avemojis. """
        await ctx.send(f"{ctx.message.author.mention}: {self.bot.config['emojis']['emojiinvite']}")

    @commands.command(hidden=True)
    async def emojilist(self, ctx):
        """Gives out the emoji list for the current server. Priv or up only."""

        # Permission checks
        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 2:
            return

        messages = []
        current_message = ""
        for emoji in ctx.guild.emojis:
            text_to_add = f"{str(emoji)} `:{emoji.name}:`"

            # Try to split messages as cleanly as possible. One extra character for \n.
            if (len(text_to_add) + len(current_message) + 1) > 2000:
                messages.append(current_message)
                current_message = text_to_add
            else:
                current_message += "\n" + text_to_add
        messages.append(current_message)

        for message in messages:
            await ctx.send(message)

    @commands.command(hidden=True)
    async def addavemoji(self, ctx, emoji_name: str = "", url: str = ""):
        """Adds an emoji to avemojis. Mod only.

        Automatically resizes images down to discord limits.

        You can use attachments, but only first image will be used.

        If you don't specify emoji name, then filename will be used."""

        # Permission checks
        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        # If there's no emoji name, then pick the filename from url, or from the attachment.
        if not emoji_name:
            url_filename = ""
            if url:
                url_filename = await self.bot.url_get_filename(url)
            else:
                url_filename = ctx.message.attachments[0].filename
            emoji_name = await self.bot.filename_get_woext(url_filename)

        # If there's no URL, then pick the first attachment and use its url.
        if not url and ctx.message.attachments:
            url = ctx.message.attachments[0].url

        added_emoji = await self.download_and_add_emoji(self.emoji_guild_id, emoji_name, url)
        result_str = f"Added {str(added_emoji)}" if added_emoji else "This emoji is too big or there aren't emoji slots left."
        await ctx.send(f"{ctx.message.author.mention}: {result_str}")

        announcements_channel = self.bot.get_channel(
            int(self.bot.config['base']['emoji-announcements-channel']))
        await announcements_channel.send(f"New emoji! {str(added_emoji)}, added by {ctx.author.mention} ({ctx.author}).")
        await announcements_channel.send(str(added_emoji))

    @commands.command(hidden=True)
    async def deleteavemoji(self, ctx, emoji_string: str):
        """Deletes one or more avemoji(s), mod+ only"""

        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        emojis = self.extract_emojis(emoji_string, True)
        for i_emoji in emojis:
            emoji_name = i_emoji[2]
            emoji_id = int(i_emoji[3])
            the_emoji = self.bot.get_emoji(emoji_id)

            await the_emoji.delete(reason=f"delete requested by {ctx.author} / {ctx.author.id}")
            await ctx.send(f"{ctx.author.mention}: Emoji `:{emoji_name}:` deleted.")

            announcements_channel = self.bot.get_channel(
                int(self.bot.config['base']['emoji-announcements-channel']))
            await announcements_channel.send(f"Emoji deletion: `:{emoji_name}:` got removed by {ctx.author.mention} ({ctx.author})")

    @commands.command(hidden=True)
    async def editavemoji(self, ctx, emoji_string: str, new_name: str):
        """Renames an emoji, mod+ only.

        This only works if the emoji is added by avebot."""

        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        emojis = self.extract_emojis(emoji_string, True)
        if not emojis:
            await ctx.send(f"{ctx.author.mention}: no emojis found in specified thingy")
            return
        initial_emoji_name = emojis[0][2]
        emoji_id = int(emojis[0][3])
        the_emoji = self.bot.get_emoji(emoji_id)

        self.bot.log.info(f"rename on {the_emoji} - {emoji_id}")

        await the_emoji.edit(name=new_name, reason=f"rename requested by {ctx.author} / {ctx.author.id}")
        await ctx.send(f"{ctx.author.mention}: Successfully renamed - {the_emoji}")

        announcements_channel = self.bot.get_channel(
            int(self.bot.config['base']['emoji-announcements-channel']))
        await announcements_channel.send(f"Emoji `:{initial_emoji_name}:` {the_emoji} renamed to `:{new_name}:` by {ctx.author.mention} ({ctx.author})")

    @commands.is_owner()
    @commands.command(hidden=True)
    async def stealavemoji(self, ctx, *, emoji_string: str):
        emojis = self.extract_emojis(emoji_string, True)
        for emoji in emojis:
            emoji_format = "gif" if emoji[1] == "a" else "png"
            emoji_name = emoji[2]
            emoji_url = self.construct_emoji_url(emoji[3], emoji_format)

            added_emoji = await self.download_and_add_emoji(self.emoji_guild_id, emoji_name, emoji_url)
            result_str = f"Added {str(added_emoji)}" if added_emoji else "This emoji is too big."
            await ctx.send(f"{ctx.author.mention}: {result_str}")

    @commands.command(aliases=['emoji', 'einfo', 'emojiinfo', 'jumbo'])
    async def jumbomoji(self, ctx, *, emoji_string: str):
        """Jumboifies an emoji."""
        emojis = self.extract_emojis(emoji_string, True)
        if len(emojis) > self.max_jumbo:
            await ctx.send(f"{ctx.message.author.mention}: that's a little too many emojis. Max is {self.max_jumbo}.")
            return
        elif len(emojis) == 0:
            await ctx.send(f"{ctx.message.author.mention}: you didn't send any emojis.")
            return
        emoji_text = f"{ctx.message.author.mention}: "
        for emoji in emojis:
            emoji_format = "gif" if emoji[1] == "a" else "png"
            emoji_url = self.construct_emoji_url(emoji[3], emoji_format)
            emoji_text += f"\n{emoji_url}"
        await ctx.send(emoji_text)

def setup(bot):
    bot.add_cog(Emoji(bot))
<commit_msg>emojis: fix resizes not occurring<commit_after>import discord
from discord.ext import commands

import re
import io
import PIL.Image

class Emoji:
    def __init__(self, bot):
        self.bot = bot
        self.raw_emoji_regex = "(<(a|):([a-zA-Z0-9_*/-:]*):([0-9]*)>)"
        self.emoji_regex = "(<(a|):[a-zA-Z0-9_*/-:]*:[0-9]*>)"
        self.bot.extract_emojis = self.extract_emojis
        self.bot.download_and_add_emoji = self.download_and_add_emoji
        self.emoji_guild_id = int(self.bot.config['base']['emoji-guild'])
        self.max_jumbo = 5
        self.max_emoji_size = 256 * 1024
        self.emoji_dim_max = 128

    def extract_emojis(self, text: str, raw=False):
        regex_to_use = self.raw_emoji_regex if raw else self.emoji_regex
        regex_results = re.findall(regex_to_use, text)
        if raw:
            return regex_results
        emojis = list(dict(regex_results).keys())
        return emojis

    async def resize_emoji_png(self, image_bytes, no_none=False, check_size=True):
        """Resizes a given emoji to the hardcoded max emoji dimensions of discord.

        If no_none (default: False) is set to True, function return old image data if it fails to resize and stay under size limitations. If it is set to false, function return None.

        If check_size (default: True) is set to True, function will return None or emoji data depending on no_none if emoji is bigger than discord's maximum size, which is currently hardcoded to 256kb. If it is set to false, function will return the image no matter the size (to ensure that you never get None, even when image is smaller than discord dimensions yet too big, set no_none to True)."""

        bigger_than_max = len(image_bytes) > self.max_emoji_size
        # Prepare fail result so that we can use tertiary if in
        # other palces and not have terrible code
        fail_return = image_bytes if no_none else None

        # Load image from bytes
        im = PIL.Image.open(io.BytesIO(image_bytes))
        w, h = im.size

        # Check if dimensions are smaller or equal to hardcoded max size
        if w <= self.emoji_dim_max and h <= self.emoji_dim_max:
            # Return fail result (image bytes or None) if image is bigger
            # than limit, otherwise return image_bytes, as it's smaller
            # than size and dimension limit
            return fail_return if (check_size and bigger_than_max) else image_bytes
        elif w > h:
            # Get height to hardcoded discord max size ratio
            # so we can we scale down height
            ratio = w / self.emoji_dim_max
            # Scale down height by ratio determined on prev step
            # and turn it to int so that PIL doesn't complain
            new_h = int(h / ratio)
            im = im.resize((self.emoji_dim_max, new_h))
        # If both dimensions are equal or height is bigger than width
        # resize based on height
        else:
            ratio = h / self.emoji_dim_max
            new_w = int(w / ratio)
            im = im.resize((new_w, self.emoji_dim_max))

        # Create a BytesIO storage to store image
        emoji_bytes = io.BytesIO()
        im.save(emoji_bytes, format='PNG')
        emoji_bytes = emoji_bytes.getvalue()

        # If check_size and emoji size is bigger than discord limit
        # Return fail_return (None or base byte data, based on no_none)
        # if it is not, return resized emoji
        if check_size and (len(emoji_bytes) > self.max_emoji_size):
            return fail_return
        return emoji_bytes

    async def resize_emoji_gif(self, image_bytes):
        """INCOMPLETE: Currently checks if a gif is smaller than max limit and returns image bytes back, and if it is not, returns None."""
        bigger_than_max = len(image_bytes) > self.max_emoji_size
        return None if bigger_than_max else image_bytes

    async def download_and_add_emoji(self, guild_id: int, emoji_name: str, url: str):
        emoji_guild = self.bot.get_guild(guild_id)
        emoji_bytes = await self.bot.aiogetbytes(url)
        result_bytes = None

        filename = await self.bot.url_get_filename(url)
        file_ext = await self.bot.filename_get_ext(filename)

        if file_ext.lower() == "gif":
            result_bytes = await self.resize_emoji_gif(emoji_bytes)
        else:
            result_bytes = await self.resize_emoji_png(emoji_bytes)

        # Check if result is None or not, returns None if it is
        # If it is not, adds emoji and returns that
        if result_bytes:
            self.bot.log.info(f"Emoji resize process complete, byte size: {len(result_bytes)}")
            added_emoji = await emoji_guild.create_custom_emoji(name=emoji_name, image=result_bytes)
            return added_emoji
        return None

    def construct_emoji_url(self, emoji_id, emoji_format):
        return f"https://cdn.discordapp.com/emojis/{emoji_id}.{emoji_format}?v=1"

    @commands.command(aliases=['avemoji', 'avemojiinvite', 'avemojisinvite', 'ainvite'])
    async def avemojis(self, ctx):
        """Gives an invite link to Avemojis. """
        await ctx.send(f"{ctx.message.author.mention}: {self.bot.config['emojis']['emojiinvite']}")

    @commands.command(hidden=True)
    async def emojilist(self, ctx):
        """Gives out the emoji list for the current server. Priv or up only."""

        # Permission checks
        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 2:
            return

        messages = []
        current_message = ""
        for emoji in ctx.guild.emojis:
            text_to_add = f"{str(emoji)} `:{emoji.name}:`"

            # Try to split messages as cleanly as possible. One extra character for \n.
            if (len(text_to_add) + len(current_message) + 1) > 2000:
                messages.append(current_message)
                current_message = text_to_add
            else:
                current_message += "\n" + text_to_add
        messages.append(current_message)

        for message in messages:
            await ctx.send(message)

    @commands.command(hidden=True)
    async def addavemoji(self, ctx, emoji_name: str = "", url: str = ""):
        """Adds an emoji to avemojis. Mod only.

        Automatically resizes images down to discord limits.

        You can use attachments, but only first image will be used.

        If you don't specify emoji name, then filename will be used."""

        # Permission checks
        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        # If there's no emoji name, then pick the filename from url, or from the attachment.
        if not emoji_name:
            url_filename = ""
            if url:
                url_filename = await self.bot.url_get_filename(url)
            else:
                url_filename = ctx.message.attachments[0].filename
            emoji_name = await self.bot.filename_get_woext(url_filename)

        # If there's no URL, then pick the first attachment and use its url.
        if not url and ctx.message.attachments:
            url = ctx.message.attachments[0].url

        added_emoji = await self.download_and_add_emoji(self.emoji_guild_id, emoji_name, url)
        result_str = f"Added {str(added_emoji)}" if added_emoji else "This emoji is too big or there aren't emoji slots left."
        await ctx.send(f"{ctx.message.author.mention}: {result_str}")

        announcements_channel = self.bot.get_channel(
            int(self.bot.config['base']['emoji-announcements-channel']))
        await announcements_channel.send(f"New emoji! {str(added_emoji)}, added by {ctx.author.mention} ({ctx.author}).")
        await announcements_channel.send(str(added_emoji))

    @commands.command(hidden=True)
    async def deleteavemoji(self, ctx, emoji_string: str):
        """Deletes one or more avemoji(s), mod+ only"""

        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        emojis = self.extract_emojis(emoji_string, True)
        for i_emoji in emojis:
            emoji_name = i_emoji[2]
            emoji_id = int(i_emoji[3])
            the_emoji = self.bot.get_emoji(emoji_id)

            await the_emoji.delete(reason=f"delete requested by {ctx.author} / {ctx.author.id}")
            await ctx.send(f"{ctx.author.mention}: Emoji `:{emoji_name}:` deleted.")

            announcements_channel = self.bot.get_channel(
                int(self.bot.config['base']['emoji-announcements-channel']))
            await announcements_channel.send(f"Emoji deletion: `:{emoji_name}:` got removed by {ctx.author.mention} ({ctx.author})")

    @commands.command(hidden=True)
    async def editavemoji(self, ctx, emoji_string: str, new_name: str):
        """Renames an emoji, mod+ only.

        This only works if the emoji is added by avebot."""

        author_level = await self.bot.get_permission(ctx.author.id)
        if author_level < 8:
            return

        emojis = self.extract_emojis(emoji_string, True)
        if not emojis:
            await ctx.send(f"{ctx.author.mention}: no emojis found in specified thingy")
            return
        initial_emoji_name = emojis[0][2]
        emoji_id = int(emojis[0][3])
        the_emoji = self.bot.get_emoji(emoji_id)

        self.bot.log.info(f"rename on {the_emoji} - {emoji_id}")

        await the_emoji.edit(name=new_name, reason=f"rename requested by {ctx.author} / {ctx.author.id}")
        await ctx.send(f"{ctx.author.mention}: Successfully renamed - {the_emoji}")

        announcements_channel = self.bot.get_channel(
            int(self.bot.config['base']['emoji-announcements-channel']))
        await announcements_channel.send(f"Emoji `:{initial_emoji_name}:` {the_emoji} renamed to `:{new_name}:` by {ctx.author.mention} ({ctx.author})")

    @commands.is_owner()
    @commands.command(hidden=True)
    async def stealavemoji(self, ctx, *, emoji_string: str):
        emojis = self.extract_emojis(emoji_string, True)
        for emoji in emojis:
            emoji_format = "gif" if emoji[1] == "a" else "png"
            emoji_name = emoji[2]
            emoji_url = self.construct_emoji_url(emoji[3], emoji_format)

            added_emoji = await self.download_and_add_emoji(self.emoji_guild_id, emoji_name, emoji_url)
            result_str = f"Added {str(added_emoji)}" if added_emoji else "This emoji is too big."
            await ctx.send(f"{ctx.author.mention}: {result_str}")

    @commands.command(aliases=['emoji', 'einfo', 'emojiinfo', 'jumbo'])
    async def jumbomoji(self, ctx, *, emoji_string: str):
        """Jumboifies an emoji."""
        emojis = self.extract_emojis(emoji_string, True)
        if len(emojis) > self.max_jumbo:
            await ctx.send(f"{ctx.message.author.mention}: that's a little too many emojis. Max is {self.max_jumbo}.")
            return
        elif len(emojis) == 0:
            await ctx.send(f"{ctx.message.author.mention}: you didn't send any emojis.")
            return
        emoji_text = f"{ctx.message.author.mention}: "
        for emoji in emojis:
            emoji_format = "gif" if emoji[1] == "a" else "png"
            emoji_url = self.construct_emoji_url(emoji[3], emoji_format)
            emoji_text += f"\n{emoji_url}"
        await ctx.send(emoji_text)

def setup(bot):
    bot.add_cog(Emoji(bot))
<|endoftext|>


<commit_before>from discord.ext import commands

from bot.bot import group, cooldown
from cogs.cog import Cog
from utils.utilities import split_string

class Emotes(Cog):
    def __init__(self, bot):
        super().__init__(bot)

    @staticmethod
    def get_emotes(guild):
        global_emotes = []
        local_emotes = []
        animated_emotes = []
        emotes = guild.emojis

        for emote in emotes:
            if emote.managed:
                global_emotes.append(emote)
            elif emote.animated:
                animated_emotes.append(emote)
            else:
                local_emotes.append(emote)

        return global_emotes, local_emotes, animated_emotes

    @staticmethod
    def _format_emotes(emotes, type_=None, include_name=True, delim='\n'):
        e = f'{len(emotes)}/50 {type_} emotes\n' if type_ else ''

        for i, emote in enumerate(emotes):
            if include_name:
                e += '{} {}'.format(emote.name, emote)
            else:
                e += '{}'.format(emote)

            if i%2:
                e += delim
            else:
                e += ' '

        return e.strip('\n')

    @cooldown(1, 10, type=commands.BucketType.guild)
    @group()
    async def emotes(self, ctx):
        """Show emotes on this server"""
        guild = ctx.guild

        if ctx.invoked_subcommand is None:
            global_emotes, local_emotes, animated_emotes = self.get_emotes(guild)

            if global_emotes:
                s = 'Global emotes:\n' + self._format_emotes(global_emotes)
            elif local_emotes:
                s = 'Local emotes:\n' + self._format_emotes(local_emotes, 'local')
            elif animated_emotes:
                s = 'Animated emotes:\n' + self._format_emotes(animated_emotes, 'animated')
            else:
                s = 'No emotes'

            strings = split_string(s, maxlen=2000, splitter='\n')

            for s in strings:
                await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(name='global')
    async def global_(self, ctx, include_name: bool=True):
        """Show global emotes on this server"""
        guild = ctx.guild

        global_, _, _ = self.get_emotes(guild)
        s = self._format_emotes(global_, include_name=include_name)
        s = s if s else 'No global emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(name='local')
    async def local_(self, ctx, include_name: bool=True):
        """Show all non global emotes on this server"""
        guild = ctx.guild

        _, local_, _ = self.get_emotes(guild)
        s = self._format_emotes(local_, 'local', include_name)
        s = s if s else 'No local emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(aliases=['gif'])
    async def animated(self, ctx, include_name: bool=True):
        """Show all non global emotes on this server"""
        guild = ctx.guild

        _, _, animated = self.get_emotes(guild)
        s = self._format_emotes(animated, 'animated', include_name)
        s = s if s else 'No animated emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

def setup(bot):
    bot.add_cog(Emotes(bot))
<commit_msg>emote commands now take guild emote limit into account<commit_after>from discord.ext import commands

from bot.bot import group, cooldown
from cogs.cog import Cog
from utils.utilities import split_string

class Emotes(Cog):
    def __init__(self, bot):
        super().__init__(bot)

    @staticmethod
    def get_emotes(guild):
        global_emotes = []
        local_emotes = []
        animated_emotes = []
        emotes = guild.emojis

        for emote in emotes:
            if emote.managed:
                global_emotes.append(emote)
            elif emote.animated:
                animated_emotes.append(emote)
            else:
                local_emotes.append(emote)

        return global_emotes, local_emotes, animated_emotes

    @staticmethod
    def _format_emotes(emotes, limit, type_=None, include_name=True, delim='\n', width=2):
        e = f'{len(emotes)}/{limit} {type_} emotes\n' if type_ else ''

        for i, emote in enumerate(emotes):
            if include_name:
                e += '{} {}'.format(emote.name, emote)
            else:
                e += '{}'.format(emote)

            if i % width == 0:
                e += delim
            else:
                e += ' '

        return e.strip('\n')

    @cooldown(1, 10, type=commands.BucketType.guild)
    @group()
    async def emotes(self, ctx):
        """Show emotes on this server"""
        guild = ctx.guild
        emote_limit = guild.emoji_limit

        if ctx.invoked_subcommand is None:
            global_emotes, local_emotes, animated_emotes = self.get_emotes(guild)

            if global_emotes:
                s = 'Global emotes:\n' + self._format_emotes(global_emotes, emote_limit)
            elif local_emotes:
                s = 'Local emotes:\n' + self._format_emotes(local_emotes, emote_limit, 'local')
            elif animated_emotes:
                s = 'Animated emotes:\n' + self._format_emotes(animated_emotes, emote_limit, 'animated')
            else:
                s = 'No emotes'

            strings = split_string(s, maxlen=2000, splitter='\n')

            for s in strings:
                await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(name='global')
    async def global_(self, ctx, include_name: bool=True):
        """Show global emotes on this server"""
        guild = ctx.guild
        emote_limit = guild.emoji_limit

        global_, _, _ = self.get_emotes(guild)
        s = self._format_emotes(global_, emote_limit, include_name=include_name)
        s = s if s else 'No global emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(name='local')
    async def local_(self, ctx, include_name: bool=True):
        """Show all non global emotes on this server"""
        guild = ctx.guild
        emote_limit = guild.emoji_limit

        _, local_, _ = self.get_emotes(guild)
        s = self._format_emotes(local_, emote_limit, 'local', include_name)
        s = s if s else 'No local emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

    @cooldown(1, 10, type=commands.BucketType.guild)
    @emotes.command(aliases=['gif'])
    async def animated(self, ctx, include_name: bool=True):
        """Show all non global emotes on this server"""
        guild = ctx.guild
        emote_limit = guild.emoji_limit

        _, _, animated = self.get_emotes(guild)
        s = self._format_emotes(animated, emote_limit, 'animated', include_name)
        s = s if s else 'No animated emotes'
        for s in split_string(s, maxlen=2000, splitter='\n'):
            await ctx.send(s)

def setup(bot):
    bot.add_cog(Emotes(bot))
<|endoftext|>


<commit_before>import logging
import os
from asyncio import Queue, Lock
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from io import BytesIO
from random import randint, random

from PIL import Image
from discord.ext.commands import cooldown
from selenium.webdriver import PhantomJS
from selenium.webdriver.support.select import Select

from bot.bot import command
from cogs.cog import Cog
from utils.imagetools import resize_keep_aspect_ratio, image_from_url
from utils.utilities import get_image_from_message

logger = logging.getLogger('debug')

class Fun(Cog):
    def __init__(self, bot):
        super().__init__(bot)
        self.driver = PhantomJS(self.bot.config.phantomjs)
        self.threadpool = ThreadPoolExecutor(3)
        self._driver_lock = Lock()
        self.queue = Queue()
        self.queue.put_nowait(1)

    @command(pass_context=True, ignore_extra=True)
    @cooldown(5, 5)
    async def anime_deaths(self, ctx, image):
        path = os.path.join('data', 'templates', 'saddest-anime-deaths.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        await self.bot.send_typing(ctx.message.channel)
        x, y = 9, 10
        w, h = 854, 480
        template = Image.open(path)
        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False, resample=Image.BILINEAR)
        new_w, new_h = img.width, img.height
        if new_w != w:
            x += int((w - new_w)/2)

        if new_h != h:
            y += int((h - new_h) / 2)

        img = img.convert("RGBA")
        template.paste(img, (x, y), img)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='top10-anime-deaths.png')

    @command(pass_context=True, ignore_extra=True)
    @cooldown(5, 5)
    async def anime_deaths2(self, ctx, image):
        path = os.path.join('data', 'templates', 'saddest-anime-deaths2.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        await self.bot.send_typing(ctx.message.channel)
        x, y = 9, 10
        w, h = 854, 480
        template = Image.open(path)
        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False, resample=Image.BILINEAR)
        new_w, new_h = img.width, img.height
        if new_w != w:
            x += int((w - new_w)/2)

        if new_h != h:
            y += int((h - new_h) / 2)

        img = img.convert("RGBA")
        template.paste(img, (x, y), img)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='top10-anime-deaths.png')

    @command(pass_context=True, ignore_extra=True, usage="""""")
    @cooldown(2, 5)
    async def trap(self, ctx, image=None):
        """Is it a trap?

        """
        path = os.path.join('data', 'templates', 'is_it_a_trap.png')
        path2 = os.path.join('data', 'templates', 'is_it_a_trap_layer.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        img = img.convert("RGBA")
        await self.bot.send_typing(ctx.message.channel)
        x, y = 820, 396
        w, h = 355, 505
        rotation = -22

        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False,
                                       resample=Image.BILINEAR)
        img = img.rotate(rotation, expand=True, resample=Image.BILINEAR)
        x_place = x - int(img.width / 2)
        y_place = y - int(img.height / 2)

        template = Image.open(path)

        template.paste(img, (x_place, y_place), img)
        layer = Image.open(path2)
        template.paste(layer, (0, 0), layer)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='is_it_a_trap.png')

    async def get_url(self, url):
        # Attempt at making phantomjs async friendly
        # After visiting the url remember to put 1 item in self.queue
        # Otherwise the browser will be locked

        # If lock is not locked lock it until this operation finishes
        unlock = False
        if not self._driver_lock.locked():
            await self._driver_lock.acquire()
            unlock = True

        f = partial(self.driver.get, url)
        await self.bot.loop.run_in_executor(self.threadpool, f)
        if unlock:
            try:
                self._driver_lock.release()
            except RuntimeError:
                pass

    @command(pass_context=True, ignore_extra=True)
    @cooldown(2, 2)
    async def pokefusion(self, ctx):
        """Gets a random pokemon fusion from http://pokefusion.japeal.com"""
        async with self._driver_lock:
            try:
                await self.bot.send_typing(ctx.message.channel)
                if not self.driver.current_url.startswith('http://pokefusion.japeal.com'):
                    await self.get_url('http://pokefusion.japeal.com/')
                    self.driver.switch_to.frame('inneriframe')

                b1 = self.driver.find_element_by_id('myButtonR')
                b2 = self.driver.find_element_by_id('myButtonL')
                b_color = self.driver.find_element_by_id('myButtonColor')
                if b1:
                    b1.click()
                if b2:
                    b2.click()

                script = "var e = document.getElementById('%s'); return {text: e.options[e.selectedIndex].text, value: e.value}"
                poke1 = self.driver.execute_script(script % 's1')
                poke2 = self.driver.execute_script(script % 's2')
                s = 'Fusion of {0[text]} and {1[text}'.format(poke1, poke2)
                url = 'http://pokefusion.japeal.com/{0[value]}/{1[value]}'.format(poke1, poke2)

                color_poke = None
                if b_color and random() < 0.3:
                    b_color.click()
                    color_poke = self.driver.execute_script(script % 's3')

                if color_poke and color_poke['text'].lower() == 'none':
                    color_poke = None

                if color_poke:
                    s += ' using the color palette of {0[text]\n{1}/{0[value]}}'.format(color_poke, url)

                img = BytesIO(self.driver.get_screenshot_as_png())
            except:
                logger.exception('Failed to get pokefusion. Refreshing page')
                await self.get_url('http://pokefusion.japeal.com/')
                self.driver.switch_to.frame('inneriframe')
                return await self.bot.say('Failed to fuse pokemon')

        img.seek(0)
        img = Image.open(img)
        img = img.crop((141, 465, 502, 740))
        file = BytesIO()
        img.save(file, 'PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='pokefusion.png', content=s)

def setup(bot):
    bot.add_cog(Fun(bot))
<commit_msg>Formatting error fixed<commit_after>import logging
import os
from asyncio import Queue, Lock
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from io import BytesIO
from random import randint, random

from PIL import Image
from discord.ext.commands import cooldown
from selenium.webdriver import PhantomJS
from selenium.webdriver.support.select import Select

from bot.bot import command
from cogs.cog import Cog
from utils.imagetools import resize_keep_aspect_ratio, image_from_url
from utils.utilities import get_image_from_message

logger = logging.getLogger('debug')

class Fun(Cog):
    def __init__(self, bot):
        super().__init__(bot)
        self.driver = PhantomJS(self.bot.config.phantomjs)
        self.threadpool = ThreadPoolExecutor(3)
        self._driver_lock = Lock()
        self.queue = Queue()
        self.queue.put_nowait(1)

    @command(pass_context=True, ignore_extra=True)
    @cooldown(5, 5)
    async def anime_deaths(self, ctx, image):
        path = os.path.join('data', 'templates', 'saddest-anime-deaths.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        await self.bot.send_typing(ctx.message.channel)
        x, y = 9, 10
        w, h = 854, 480
        template = Image.open(path)
        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False, resample=Image.BILINEAR)
        new_w, new_h = img.width, img.height
        if new_w != w:
            x += int((w - new_w)/2)

        if new_h != h:
            y += int((h - new_h) / 2)

        img = img.convert("RGBA")
        template.paste(img, (x, y), img)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='top10-anime-deaths.png')

    @command(pass_context=True, ignore_extra=True)
    @cooldown(5, 5)
    async def anime_deaths2(self, ctx, image):
        path = os.path.join('data', 'templates', 'saddest-anime-deaths2.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        await self.bot.send_typing(ctx.message.channel)
        x, y = 9, 10
        w, h = 854, 480
        template = Image.open(path)
        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False, resample=Image.BILINEAR)
        new_w, new_h = img.width, img.height
        if new_w != w:
            x += int((w - new_w)/2)

        if new_h != h:
            y += int((h - new_h) / 2)

        img = img.convert("RGBA")
        template.paste(img, (x, y), img)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='top10-anime-deaths.png')

    @command(pass_context=True, ignore_extra=True, usage="""""")
    @cooldown(2, 5)
    async def trap(self, ctx, image=None):
        """Is it a trap?

        """
        path = os.path.join('data', 'templates', 'is_it_a_trap.png')
        path2 = os.path.join('data', 'templates', 'is_it_a_trap_layer.png')
        img = get_image_from_message(ctx, image)
        if img is None:
            return await self.bot.say('No image found from %s' % image)

        img = await image_from_url(img, self.bot.aiohttp_client)
        if img is None:
            return await self.bot.say('Could not extract image from {}.'.format(image))

        img = img.convert("RGBA")
        await self.bot.send_typing(ctx.message.channel)
        x, y = 820, 396
        w, h = 355, 505
        rotation = -22

        img = resize_keep_aspect_ratio(img, (w, h), can_be_bigger=False,
                                       resample=Image.BILINEAR)
        img = img.rotate(rotation, expand=True, resample=Image.BILINEAR)
        x_place = x - int(img.width / 2)
        y_place = y - int(img.height / 2)

        template = Image.open(path)

        template.paste(img, (x_place, y_place), img)
        layer = Image.open(path2)
        template.paste(layer, (0, 0), layer)
        file = BytesIO()
        template.save(file, format='PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='is_it_a_trap.png')

    async def get_url(self, url):
        # Attempt at making phantomjs async friendly
        # After visiting the url remember to put 1 item in self.queue
        # Otherwise the browser will be locked

        # If lock is not locked lock it until this operation finishes
        unlock = False
        if not self._driver_lock.locked():
            await self._driver_lock.acquire()
            unlock = True

        f = partial(self.driver.get, url)
        await self.bot.loop.run_in_executor(self.threadpool, f)
        if unlock:
            try:
                self._driver_lock.release()
            except RuntimeError:
                pass

    @command(pass_context=True, ignore_extra=True)
    @cooldown(2, 2)
    async def pokefusion(self, ctx):
        """Gets a random pokemon fusion from http://pokefusion.japeal.com"""
        async with self._driver_lock:
            try:
                await self.bot.send_typing(ctx.message.channel)
                if not self.driver.current_url.startswith('http://pokefusion.japeal.com'):
                    await self.get_url('http://pokefusion.japeal.com/')
                    self.driver.switch_to.frame('inneriframe')

                b1 = self.driver.find_element_by_id('myButtonR')
                b2 = self.driver.find_element_by_id('myButtonL')
                b_color = self.driver.find_element_by_id('myButtonColor')
                if b1:
                    b1.click()
                if b2:
                    b2.click()

                script = "var e = document.getElementById('%s'); return {text: e.options[e.selectedIndex].text, value: e.value}"
                poke1 = self.driver.execute_script(script % 's1')
                poke2 = self.driver.execute_script(script % 's2')
                s = 'Fusion of {0[text]} and {1[text]}'.format(poke1, poke2)
                url = 'http://pokefusion.japeal.com/{0[value]}/{1[value]}'.format(poke1, poke2)

                color_poke = None
                if b_color and random() < 0.3:
                    b_color.click()
                    color_poke = self.driver.execute_script(script % 's3')

                if color_poke and color_poke['text'].lower() == 'none':
                    color_poke = None

                if color_poke:
                    s += ' using the color palette of {0[text]\n{1}/{0[value]}}'.format(color_poke, url)

                img = BytesIO(self.driver.get_screenshot_as_png())
            except:
                logger.exception('Failed to get pokefusion. Refreshing page')
                await self.get_url('http://pokefusion.japeal.com/')
                self.driver.switch_to.frame('inneriframe')
                return await self.bot.say('Failed to fuse pokemon')

        img.seek(0)
        img = Image.open(img)
        img = img.crop((141, 465, 502, 740))
        file = BytesIO()
        img.save(file, 'PNG')
        file.seek(0)
        await self.bot.send_file(ctx.message.channel, file, filename='pokefusion.png', content=s)

def setup(bot):
    bot.add_cog(Fun(bot))
<|endoftext|>


<commit_before>from discord.ext import commands
from .utils import checks
import aiohttp
import json
import discord
from lxml import etree

class CodeBlock:
    missing_error = 'Missing code block. Please use the following markdown\n\\`\\`\\`language\ncode here\n\\`\\`\\`'
    def __init__(self, argument):
        try:
            block, code = argument.split('\n', 1)
        except ValueError:
            raise commands.BadArgument(self.missing_error)

        if not block.startswith('```') and not code.endswith('```'):
            raise commands.BadArgument(self.missing_error)

        language = block[3:]
        self.command = self.get_command_from_language(language.lower())
        self.source = code.rstrip('`').replace('```', '')

    def get_command_from_language(self, language):
        cmds = {
            'cpp': 'g++ -std=c++1z -O2 -Wall -Wextra -pedantic -pthread main.cpp -lstdc++fs && ./a.out',
            'c': 'mv main.cpp main.c && gcc -std=c11 -O2 -Wall -Wextra -pedantic main.c && ./a.out',
            'py': 'python3 main.cpp',
            'python': 'python3 main.cpp',
            'haskell': 'runhaskell main.cpp'
        }

        cpp = cmds['cpp']
        for alias in ('cc', 'h', 'c++', 'h++', 'hpp'):
            cmds[alias] = cpp
        try:
            return cmds[language]
        except KeyError as e:
            if language:
                fmt = f'Unknown language to compile for: {language}'
            else:
                fmt = 'Could not find a language to compile with.'
            raise commands.BadArgument(fmt) from e

class Lounge(commands.Cog, name='Lounge<C++>'):
    """Commands made for Lounge<C++>.

    Don't abuse these.
    """

    def __init__(self, bot):
        self.bot = bot

    @commands.command()
    @checks.is_lounge_cpp()
    async def coliru(self, ctx, *, code: CodeBlock):
        """Compiles code via Coliru.

        You have to pass in a code block with the language syntax
        either set to one of these:

        - cpp
        - c
        - python
        - py
        - haskell

        Anything else isn't supported. The C++ compiler uses g++ -std=c++14.

        The python support is now 3.5.2.

        Please don't spam this for Stacked's sake.
        """
        payload = {
            'cmd': code.command,
            'src': code.source
        }

        data = json.dumps(payload)

        async with ctx.typing():
            async with ctx.session.post('http://coliru.stacked-crooked.com/compile', data=data) as resp:
                if resp.status != 200:
                    await ctx.send('Coliru did not respond in time.')
                    return

                output = await resp.text(encoding='utf-8')

                if len(output) < 1992:
                    await ctx.send(f'```\n{output}\n```')
                    return

                # output is too big so post it in gist
                async with ctx.session.post('http://coliru.stacked-crooked.com/share', data=data) as r:
                    if r.status != 200:
                        await ctx.send('Could not create coliru shared link')
                    else:
                        shared_id = await r.text()
                        await ctx.send(f'Output too big. Coliru link: http://coliru.stacked-crooked.com/a/{shared_id}')

    @coliru.error
    async def coliru_error(self, ctx, error):
        if isinstance(error, commands.BadArgument):
            await ctx.send(error)
        if isinstance(error, commands.MissingRequiredArgument):
            await ctx.send(CodeBlock.missing_error)

    @commands.command()
    async def cpp(self, ctx, *, query: str):
        """Search something on cppreference"""

        url = 'http://en.cppreference.com/w/cpp/index.php'
        params = {
            'title': 'Special:Search',
            'search': query
        }

        async with ctx.session.get(url, params=params) as resp:
            if resp.status != 200:
                return await ctx.send(f'An error occurred (status code: {resp.status}). Retry later.')

            if resp.url.path != '/w/cpp/index.php':
                return await ctx.send(f'<{resp.url}>')

            e = discord.Embed()
            root = etree.fromstring(await resp.text(), etree.HTMLParser())

            nodes = root.findall(".//div[@class='mw-search-result-heading']/a")

            description = []
            special_pages = []
            for node in nodes:
                href = node.attrib['href']
                if not href.startswith('/w/cpp'):
                    continue

                if href.startswith(('/w/cpp/language', '/w/cpp/concept')):
                    # special page
                    special_pages.append(f'[{node.text}](http://en.cppreference.com{href})')
                else:
                    description.append(f'[`{node.text}`](http://en.cppreference.com{href})')

            if len(special_pages) > 0:
                e.add_field(name='Language Results', value='\n'.join(special_pages), inline=False)
                if len(description):
                    e.add_field(name='Library Results', value='\n'.join(description[:10]), inline=False)
            else:
                if not len(description):
                    return await ctx.send('No results found.')

                e.title = 'Search Results'
                e.description = '\n'.join(description[:15])

            e.add_field(name='See More', value=f'[`{discord.utils.escape_markdown(query)}` results]({resp.url})')
            await ctx.send(embed=e)

def setup(bot):
    bot.add_cog(Lounge(bot))
<commit_msg>Log channel moves to the meta channel in the Lounge<commit_after>from discord.ext import commands
from .utils import checks
import aiohttp
import json
import discord
from lxml import etree

class CodeBlock:
    missing_error = 'Missing code block. Please use the following markdown\n\\`\\`\\`language\ncode here\n\\`\\`\\`'
    def __init__(self, argument):
        try:
            block, code = argument.split('\n', 1)
        except ValueError:
            raise commands.BadArgument(self.missing_error)

        if not block.startswith('```') and not code.endswith('```'):
            raise commands.BadArgument(self.missing_error)

        language = block[3:]
        self.command = self.get_command_from_language(language.lower())
        self.source = code.rstrip('`').replace('```', '')

    def get_command_from_language(self, language):
        cmds = {
            'cpp': 'g++ -std=c++1z -O2 -Wall -Wextra -pedantic -pthread main.cpp -lstdc++fs && ./a.out',
            'c': 'mv main.cpp main.c && gcc -std=c11 -O2 -Wall -Wextra -pedantic main.c && ./a.out',
            'py': 'python3 main.cpp',
            'python': 'python3 main.cpp',
            'haskell': 'runhaskell main.cpp'
        }

        cpp = cmds['cpp']
        for alias in ('cc', 'h', 'c++', 'h++', 'hpp'):
            cmds[alias] = cpp
        try:
            return cmds[language]
        except KeyError as e:
            if language:
                fmt = f'Unknown language to compile for: {language}'
            else:
                fmt = 'Could not find a language to compile with.'
            raise commands.BadArgument(fmt) from e

class Lounge(commands.Cog, name='Lounge<C++>'):
    """Commands made for Lounge<C++>.

    Don't abuse these.
    """

    def __init__(self, bot):
        self.bot = bot

    @commands.Cog.listener()
    async def on_guild_channel_update(self, before, after):
        if after.guild.id != 145079846832308224 or after.position == before.position:
            return

        channel = after.guild.get_channel(335119551702499328)
        if channel is None:
            return

        fmt = "{1.mention} was moved:\nCategory: {0.category} -> {1.category}\nPosition: {0.position} -> {1.position}"
        await channel.send(fmt.format(before, after))

    @commands.command()
    @checks.is_lounge_cpp()
    async def coliru(self, ctx, *, code: CodeBlock):
        """Compiles code via Coliru.

        You have to pass in a code block with the language syntax
        either set to one of these:

        - cpp
        - c
        - python
        - py
        - haskell

        Anything else isn't supported. The C++ compiler uses g++ -std=c++14.

        The python support is now 3.5.2.

        Please don't spam this for Stacked's sake.
        """
        payload = {
            'cmd': code.command,
            'src': code.source
        }

        data = json.dumps(payload)

        async with ctx.typing():
            async with ctx.session.post('http://coliru.stacked-crooked.com/compile', data=data) as resp:
                if resp.status != 200:
                    await ctx.send('Coliru did not respond in time.')
                    return

                output = await resp.text(encoding='utf-8')

                if len(output) < 1992:
                    await ctx.send(f'```\n{output}\n```')
                    return

                # output is too big so post it in gist
                async with ctx.session.post('http://coliru.stacked-crooked.com/share', data=data) as r:
                    if r.status != 200:
                        await ctx.send('Could not create coliru shared link')
                    else:
                        shared_id = await r.text()
                        await ctx.send(f'Output too big. Coliru link: http://coliru.stacked-crooked.com/a/{shared_id}')

    @coliru.error
    async def coliru_error(self, ctx, error):
        if isinstance(error, commands.BadArgument):
            await ctx.send(error)
        if isinstance(error, commands.MissingRequiredArgument):
            await ctx.send(CodeBlock.missing_error)

    @commands.command()
    async def cpp(self, ctx, *, query: str):
        """Search something on cppreference"""

        url = 'http://en.cppreference.com/w/cpp/index.php'
        params = {
            'title': 'Special:Search',
            'search': query
        }

        async with ctx.session.get(url, params=params) as resp:
            if resp.status != 200:
                return await ctx.send(f'An error occurred (status code: {resp.status}). Retry later.')

            if resp.url.path != '/w/cpp/index.php':
                return await ctx.send(f'<{resp.url}>')

            e = discord.Embed()
            root = etree.fromstring(await resp.text(), etree.HTMLParser())

            nodes = root.findall(".//div[@class='mw-search-result-heading']/a")

            description = []
            special_pages = []
            for node in nodes:
                href = node.attrib['href']
                if not href.startswith('/w/cpp'):
                    continue

                if href.startswith(('/w/cpp/language', '/w/cpp/concept')):
                    # special page
                    special_pages.append(f'[{node.text}](http://en.cppreference.com{href})')
                else:
                    description.append(f'[`{node.text}`](http://en.cppreference.com{href})')

            if len(special_pages) > 0:
                e.add_field(name='Language Results', value='\n'.join(special_pages), inline=False)
                if len(description):
                    e.add_field(name='Library Results', value='\n'.join(description[:10]), inline=False)
            else:
                if not len(description):
                    return await ctx.send('No results found.')

                e.title = 'Search Results'
                e.description = '\n'.join(description[:15])

            e.add_field(name='See More', value=f'[`{discord.utils.escape_markdown(query)}` results]({resp.url})')
            await ctx.send(embed=e)

def setup(bot):
    bot.add_cog(Lounge(bot))
<|endoftext|>


<commit_before>import asyncio
import logging

import aiohttp
import discord
import discord.ext.commands as commands

import paths
from .util import config, utils

log = logging.getLogger(__name__)

def setup(bot):
    cog = Twitch(bot)
    bot.add_cog(cog)

    # Force the on_ready call if the bot is already ready
    if bot.is_ready():
        bot.loop.create_task(cog.on_ready())

class TwitchConfig(config.ConfigElement):
    def __init__(self, client_id, **kwargs):
        self.client_id = client_id
        self.follows = kwargs.get('follows', {})
        for k, v in self.follows.items():
            self.follows[k] = utils.dict_keys_to_int(v)

    def remove_channels(self, *channels):
        """Unregister the given channels from every followed channel, and
        removes any followed channel that end up without any channel.
        """
        channels = set(c.id for c in channels)
        conf_to_remove = set()

        # Check every followed channel
        for stream_id, destinations in self.follows.items():
            # Remove the given channels from this followed channel
            for channel in channels:
                try:
                    del destinations[channel]
                except KeyError:
                    pass
            if not destinations:
                conf_to_remove.add(stream_id)

        # Cleanup the followed channels
        for stream_id in conf_to_remove:
            del self.follows[stream_id]

class Twitch:
    """Follow Twitch channel and display alerts in Discord when one goes live."""
    def __init__(self, bot):
        self.bot = bot
        self.conf = config.Config(paths.TWITCH_CONFIG, encoding='utf-8')
        default_headers = {
            'Client-ID': self.conf.client_id,
            'Accept': 'application/vnd.twitchtv.v5+json'
        }
        self.session = aiohttp.ClientSession(loop=bot.loop, headers=default_headers)
        self.live_cache = {}
        self.daemon = None

    def __unload(self):
        self.session.close()
        self.daemon.cancel()

    async def on_channel_delete(self, channel):
        if channel.guild is not None:
            self.conf.remove_channels(channel)
            self.conf.save()

    async def on_guild_remove(self, guild):
        self.conf.remove_channels(*guild.channels)
        self.conf.save()

    async def on_ready(self):
        if not self.daemon:
            self.daemon = self.bot.loop.create_task(self._daemon())

    async def _daemon(self):
        # Live cache initialisation
        channels = ','.join(self.conf.follows.keys())
        if channels:
            try:
                streams = await utils.fetch_page('https://api.twitch.tv/kraken/streams', session=self.session, params={'channel': channels})
            except utils.HTTPError as e:
                # Re-schedule the daemon on error
                log.info(f'Error on initial poll, rescheduling. {e}')
                self.daemon = self.bot.loop.create_task(self._daemon())
                return
            else:
                self.live_cache = {str(s['channel']['_id']): s for s in streams['streams']}

        # ERMAHGERD ! MAH FRAVRIT LERP !
        while True:
            try:
                await self.poll_streams()
            except Exception as e:
                log.info(f'Polling error, retrying in 60 seconds: {e}')
                await asyncio.sleep(60)

    async def poll_streams(self):
        live_cache = {}

        # Get the streams statuses in chunks of 100
        channels = list(self.conf.follows.keys())
        for i in range(0, len(channels), 100):
            chunk = channels[i:i + 100]
            try:
                streams_chunk = await utils.fetch_page('https://api.twitch.tv/kraken/streams', session=self.session, params={'channel': ', '.join(chunk), 'limit': 100})
            except utils.HTTPError as e:
                raise Exception(f'HTTP error when fetching streams chunk #{i / 100 + 1}') from e

            # Extract the newly live streams
            for stream in streams_chunk['streams']:
                stream_id = str(stream['channel']['_id'])
                live_cache[stream_id] = stream

        # Send notifications for the streams that went live after we successfully retrieved all the chunks
        for stream_id in live_cache:
            if stream_id not in self.live_cache:
                try:
                    await self.notify(live_cache[stream_id])
                except Exception as e:
                    log.error(f'Notification error: {e}')

        # Update the live cache
        del self.live_cache
        self.live_cache = live_cache

    async def notify(self, stream):
        # Build the embed
        embed = discord.Embed(title='Click here to join the fun !', url=stream['channel']['url'], colour=discord.Colour.blurple())
        embed.set_author(name=stream['channel']['display_name'])
        embed.set_thumbnail(url=stream['channel']['logo'])
        embed.set_image(url=stream['preview']['large'])
        embed.add_field(name=stream['channel']['status'], value='Playing ' + stream['game'])

        # Make sure we're ready to send messages
        await self.bot.wait_until_ready()

        # Send the notification to every interested channel
        for channel_id, message in self.conf.follows[str(stream['channel']['_id'])].items():
            destination = self.bot.get_channel(channel_id)
            await destination.send(message, embed=embed)

    async def get_user(self, channel):
        data = await utils.fetch_page('https://api.twitch.tv/kraken/users', session=self.session, params={'login': channel})
        if data['_total'] == 0:
            raise commands.BadArgument('Channel not found.')
        elif data['_total'] > 1:
            raise commands.BadArgument('More than one channel found.')

        return data['users'][0]

    @commands.group(name='twitch')
    @commands.has_permissions(manage_guild=True)
    async def twitch_group(self, ctx):
        pass

    @twitch_group.command(name='follow')
    @commands.guild_only()
    @commands.has_permissions(manage_guild=True)
    async def twitch_follow(self, ctx, channel, *, message=''):
        """Follows a twitch channel.

        When the given Twitch channel goes online, a notification will be sent
        in the Discord channel this command was used in.

        The message will be sent along the stream notification.
        """
        user = await self.get_user(channel)
        user_id = user['_id']

        # Register it in the conf
        if user_id not in self.conf.follows.keys():
            self.conf.follows[user_id] = {ctx.channel.id: message}
        elif ctx.channel.id in self.conf.follows[user_id]:
            raise commands.BadArgument(f'Already following "{channel}" on this channel.')
        else:
            self.conf.follows[user_id][ctx.channel.id] = message
        self.conf.save()

        # Update the live streams cache if the stream is live
        streams = await utils.fetch_page('https://api.twitch.tv/kraken/streams', session=self.session, params={'channel': user_id})
        if streams['_total'] == 1 and user_id not in self.live_cache:
            self.live_cache[user_id] = streams['streams'][0]

        await ctx.send('\N{OK HAND SIGN}')

    @twitch_group.command(name='unfollow')
    @commands.guild_only()
    @commands.has_permissions(manage_guild=True)
    async def twitch_unfollow(self, ctx, channel):
        """Unfollows a twitch channel.

        The notifications about the given channel will not be displayed in
        the Discord channel this command was used in anymore.
        """
        user = await self.get_user(channel)
        user_id = user['_id']
        if user_id not in self.conf.follows.keys() or ctx.channel.id not in self.conf.follows[user_id]:
            raise commands.BadArgument(f'Not following "{channel}" on this channel.')

        # Remove the discord channel from the conf and clean it up if no other channel follows that stream
        del self.conf.follows[user_id][ctx.channel.id]
        if not self.conf.follows[user_id]:
            del self.conf.follows[user_id]
            # Cleanup the live streams cache
            try:
                del self.live_cache[user_id]
            except KeyError:
                pass

        self.conf.save()

        await ctx.send('\N{OK HAND SIGN}')
<commit_msg>Twitter: Minor refactoring.<commit_after>import asyncio
import logging

import aiohttp
import discord
import discord.ext.commands as commands

import paths
from .util import config, utils

log = logging.getLogger(__name__)

def setup(bot):
    cog = Twitch(bot)
    bot.add_cog(cog)

    # Force the on_ready call if the bot is already ready
    if bot.is_ready():
        bot.loop.create_task(cog.on_ready())

class TwitchConfig(config.ConfigElement):
    def __init__(self, client_id, **kwargs):
        self.client_id = client_id
        self.follows = kwargs.get('follows', {})
        for k, v in self.follows.items():
            self.follows[k] = utils.dict_keys_to_int(v)

    def remove_channels(self, *channels):
        """Unregister the given channels from every followed channel, and
        removes any followed channel that end up without any channel.
        """
        channels = set(c.id for c in channels)
        conf_to_remove = set()

        # Check every followed channel
        for stream_id, destinations in self.follows.items():
            # Remove the given channels from this followed channel
            for channel in channels:
                try:
                    del destinations[channel]
                except KeyError:
                    pass
            if not destinations:
                conf_to_remove.add(stream_id)

        # Cleanup the followed channels
        for stream_id in conf_to_remove:
            del self.follows[stream_id]

class Twitch:
    """Follow Twitch channel and display alerts in Discord when one goes live."""
    def __init__(self, bot):
        self.bot = bot
        self.conf = config.Config(paths.TWITCH_CONFIG, encoding='utf-8')
        default_headers = {
            'Client-ID': self.conf.client_id,
            'Accept': 'application/vnd.twitchtv.v5+json'
        }
        self.session = aiohttp.ClientSession(loop=bot.loop, headers=default_headers)
        self.live_cache = {}
        self.daemon = None

    def __unload(self):
        self.session.close()
        self.daemon.cancel()

    async def on_channel_delete(self, channel):
        if channel.guild is not None:
            self.conf.remove_channels(channel)
            self.conf.save()

    async def on_guild_remove(self, guild):
        self.conf.remove_channels(*guild.channels)
        self.conf.save()

    async def on_ready(self):
        if not self.daemon:
            self.daemon = self.bot.loop.create_task(self._daemon())

    async def _daemon(self):
        if self.conf.follows:
            # Live cache initialisation
            while not self.live_cache:
                try:
                    streams = await self.poll_streams()
                except Exception as e:
                    log.info(f'Error on initial poll, retrying in 20 seconds: {e}')
                    await asyncio.sleep(20)
                else:
                    self.live_cache = set(streams.keys())

        # ERMAHGERD ! MAH FRAVRIT LERP !
        while True:
            try:
                streams = await self.poll_streams()
            except Exception as e:
                log.info(f'Polling error, retrying in 60 seconds: {e}')
            else:
                # Send notifications for the streams that went live after we successfully retrieved all the chunks
                for stream_id in streams:
                    if stream_id not in self.live_cache:
                        try:
                            await self.notify(streams[stream_id])
                        except Exception as e:
                            log.error(f'Notification error: {e}')

                # Update the live cache
                del self.live_cache
                self.live_cache = set(streams.keys())
            finally:
                await asyncio.sleep(60)

    async def poll_streams(self):
        streams = {}

        # Get the streams statuses in chunks of 100
        channels = list(self.conf.follows.keys())
        for i in range(0, len(channels), 100):
            chunk = channels[i:i + 100]
            try:
                streams_chunk = await utils.fetch_page('https://api.twitch.tv/kraken/streams', session=self.session, params={'channel': ', '.join(chunk), 'limit': 100})
            except utils.HTTPError as e:
                raise Exception(f'HTTP error when fetching streams chunk #{i / 100 + 1}') from e

            # Extract the newly live streams
            for stream_info in streams_chunk['streams']:
                stream_id = str(stream_info['channel']['_id'])
                streams[stream_id] = stream_info

        return streams

    async def notify(self, stream_info):
        # Build the embed
        embed = discord.Embed(title='Click here to join the fun !', url=stream_info['channel']['url'], colour=discord.Colour.blurple())
        embed.set_author(name=stream_info['channel']['display_name'])
        embed.set_thumbnail(url=stream_info['channel']['logo'])
        embed.set_image(url=stream_info['preview']['large'])
        embed.add_field(name=stream_info['channel']['status'], value='Playing ' + stream_info['game'])

        # Make sure we're ready to send messages
        await self.bot.wait_until_ready()

        # Send the notification to every interested channel
        for channel_id, message in self.conf.follows[str(stream_info['channel']['_id'])].items():
            destination = self.bot.get_channel(channel_id)
            await destination.send(message, embed=embed)

    async def get_user(self, channel):
        data = await utils.fetch_page('https://api.twitch.tv/kraken/users', session=self.session, params={'login': channel})
        if data['_total'] == 0:
            raise commands.BadArgument('Channel not found.')
        elif data['_total'] > 1:
            raise commands.BadArgument('More than one channel found.')

        return data['users'][0]

    @commands.group(name='twitch')
    @commands.has_permissions(manage_guild=True)
    async def twitch_group(self, ctx):
        pass

    @twitch_group.command(name='follow')
    @commands.guild_only()
    @commands.has_permissions(manage_guild=True)
    async def twitch_follow(self, ctx, channel, *, message=''):
        """Follows a twitch channel.

        When the given Twitch channel goes online, a notification will be sent
        in the Discord channel this command was used in.

        The message will be sent along the stream notification.
        """
        user = await self.get_user(channel)
        user_id = user['_id']

        # Register it in the conf
        if user_id not in self.conf.follows.keys():
            self.conf.follows[user_id] = {ctx.channel.id: message}
        elif ctx.channel.id in self.conf.follows[user_id]:
            raise commands.BadArgument(f'Already following "{channel}" on this channel.')
        else:
            self.conf.follows[user_id][ctx.channel.id] = message
        self.conf.save()

        # Update the live streams cache if the stream is live
        streams = await utils.fetch_page('https://api.twitch.tv/kraken/streams', session=self.session, params={'channel': user_id})
        if streams['_total'] == 1 and user_id not in self.live_cache:
            self.live_cache[user_id] = streams['streams'][0]

        await ctx.send('\N{OK HAND SIGN}')

    @twitch_group.command(name='unfollow')
    @commands.guild_only()
    @commands.has_permissions(manage_guild=True)
    async def twitch_unfollow(self, ctx, channel):
        """Unfollows a twitch channel.

        The notifications about the given channel will not be displayed in
        the Discord channel this command was used in anymore.
        """
        user = await self.get_user(channel)
        user_id = user['_id']
        if user_id not in self.conf.follows.keys() or ctx.channel.id not in self.conf.follows[user_id]:
            raise commands.BadArgument(f'Not following "{channel}" on this channel.')

        # Remove the discord channel from the conf and clean it up if no other channel follows that stream
        del self.conf.follows[user_id][ctx.channel.id]
        if not self.conf.follows[user_id]:
            del self.conf.follows[user_id]
            # Cleanup the live streams cache
            try:
                del self.live_cache[user_id]
            except KeyError:
                pass

        self.conf.save()

        await ctx.send('\N{OK HAND SIGN}')
<|endoftext|>