Exemple #1
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("voice")
        self.text = CogText("voice")

        self.locked = []
Exemple #2
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("meme")
        self.config = CogConfig("meme")

        self.fishing_pool = self.config.get("_fishing")
Exemple #3
0
    def __init__(self, bot: commands.Bot):
        super().__init__(bot)

        self.config = CogConfig("base")
        self.text = CogText("base")

        self.status_loop.start()
        self.status = "online"
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("animals")
        self.text = CogText("animals")

        self.channel = None
        self.role = None
Exemple #5
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("roles")
        self.text = CogText("roles")

        self.limit_programmes = {}
        self.limit_interests = {}
Exemple #6
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("warden")
        self.text = CogText("warden")

        self.limit_full = 3
        self.limit_hard = 7
        self.limit_soft = 14
Exemple #7
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("admin")
        self.text = CogText("admin")

        self.usage = {}

        self.jail_check.start()
        self.just_booted = True
Exemple #8
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("howto")

        try:
            self.data = hjson.load(open("data/howto/howto.hjson"))
        except:
            print("Howto init(): Could not load HOWTO source file."
                  )  # noqa: T001
            self.data = OrderedDict()
Exemple #9
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("animals")
        self.text = CogText("animals")

        self.channel = None
        self.role = None
        # Because the API doesn't return the avatar resource immediately,
        # sometimes nothing happens, because the client caches the 404
        # response (?). This is an attempt to counter that.
        self.check_delay = 10
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("actress")
        self.text = CogText("actress")

        self.supported_formats = ("jpg", "jpeg", "png", "webm", "mp4", "gif")
        self.path = os.getcwd() + "/data/actress/"
        try:
            self.reactions = hjson.load(open(self.path + "reactions.hjson"))
        except:
            self.reactions = {}
        self.usage = {}
Exemple #11
0
    def __init__(self, bot: commands.Bot):
        super().__init__(bot)

        self.config = CogConfig("sync")
        self.text = CogText("sync")

        self.slave_guild_id = self.config.get("slave_guild_id")
        self.engineer_ids = self.config.get("roles", "master_engineer_ids")
        self.slave_verify_id = self.config.get("roles", "slave_verify_id")

        self.mapping_ids = self.config.get("roles", "mapping")
        self.mapping = {}

        self.slave_guild = None
        self.slave_verify = None
Exemple #12
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("points")
        self.text = CogText("points")

        self.limits_message = self.config.get("points_message")
        self.timer_message = self.config.get("timer_message")
        self.limits_reaction = self.config.get("points_reaction")
        self.timer_reaction = self.config.get("timer_reaction")

        self.stats_message = {}
        self.stats_reaction = {}

        self.cleanup.start()
Exemple #13
0
class Account(rubbercog.Rubbercog):
    """Manage the bot account"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("account")

    @commands.is_owner()
    @commands.group(name="set")
    async def set(self, ctx):
        """Set attributes"""
        await utils.send_help(ctx)

    @set.command(name="name", aliases=["username"])
    async def set_name(self, ctx, *, name: str):
        """Set bot name"""
        await self.bot.user.edit(username=name)
        await ctx.send(self.text.get("name", "ok", name=name))

    @set.command(name="avatar", aliases=["image"])
    async def set_avatar(self, ctx, *, path: str):
        """Set bot avatar

        path: path to image file, starting in `data/` directory
        """
        if ".." in path:
            return await self.output.error(ctx,
                                           self.text.get("avatar", "invalid"))

        try:
            with open("data/" + path, "rb") as image:
                with ctx.typing():
                    await self.bot.user.edit(avatar=image.read())
        except FileNotFoundError:
            return await self.output.error(
                ctx, self.text.get("avatar", "not_found"))
        await ctx.send(self.text.get("avatar", "ok"))
Exemple #14
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("verify")
        self.config = CogConfig("verify")
Exemple #15
0
class Verify(rubbercog.Rubbercog):
    """Verify your account"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("verify")
        self.config = CogConfig("verify")

    ##
    ## Commands
    ##

    @commands.check(acl.check)
    @commands.cooldown(rate=5, per=120, type=commands.BucketType.user)
    @commands.command()
    async def verify(self, ctx, email: str):
        """Ask for verification code"""
        await utils.delete(ctx)

        if email.count("@") != 1:
            raise NotAnEmail()

        if self.config.get("placeholder") in email:
            raise PlaceholderEmail()

        # check the database for member ID
        if repo_u.get(ctx.author.id) is not None:
            raise IDAlreadyInDatabase()

        # check the database for email
        if repo_u.getByLogin(email) is not None:
            raise EmailAlreadyInDatabase()

        # check e-mail format
        role = await self._email_to_role(ctx, email)

        # generate code
        code = await self._add_user(ctx.author, login=email, role=role)

        # send mail
        try:
            await self._send_verification_email(ctx.author, email, code)
        except smtplib.SMTPException as e:
            await self.console.warning(ctx, type(e).__name__)
            await self._send_verification_email(ctx.author, email, code)

        anonymised = "[redacted]@" + email.split("@")[1]
        await ctx.send(
            self.text.get(
                "verify successful",
                mention=ctx.author.mention,
                email=anonymised,
                prefix=config.prefix,
            ),
            delete_after=config.get("delay", "verify"),
        )

    @commands.check(acl.check)
    @commands.cooldown(rate=3, per=120, type=commands.BucketType.user)
    @commands.command()
    async def submit(self, ctx, code: str):
        """Submit verification code"""
        await utils.delete(ctx)

        db_user = repo_u.get(ctx.author.id)

        if db_user is None or db_user.status in (
                "unknown", "unverified") or db_user.code is None:
            raise SubmitWithoutCode()

        if db_user.status != "pending":
            raise ProblematicVerification(status=db_user.status,
                                          login=db_user.login)

        # repair the code
        code = code.replace("I", "1").replace("O", "0").upper()
        if code != db_user.code:
            raise WrongVerificationCode(ctx.author, code, db_user.code)

        # user is verified now
        repo_u.save_verified(ctx.author.id)

        # add role
        await self._add_verify_roles(ctx.author, db_user)

        # send messages
        for role_id in config.get("roles", "native"):
            if role_id in [x.id for x in ctx.author.roles]:
                await ctx.author.send(self.text.get("verification DM native"))
                break
        else:
            await ctx.author.send(self.text.get("verification DM guest"))
        # fmt: off
        # announce the verification
        await ctx.channel.send(self.text.get(
            "verification public",
            mention=ctx.author.mention,
            role=db_user.group,
        ),
                               delete_after=config.get("delay", "verify"))
        # fmt: on

        await self.event.user(
            ctx,
            f"User {ctx.author.id} verified with group **{db_user.group}**.",
        )

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        """Add them their roles back, if they have been verified before"""
        if member.guild.id != config.guild_id:
            return

        db_user = repo_u.get(member.id)
        if db_user is None or db_user.status != "verified":
            return

        # user has been verified, give them their main roles back
        await self._add_verify_roles(member, db_user)
        await self.event.user(member,
                              f"Verification skipped (**{db_user.group}**)")

    ##
    ## Helper functions
    ##

    async def _email_to_role(self, ctx, email: str) -> discord.Role:
        """Get role from email address"""
        registered = self.config.get("suffixes")
        constraints = self.config.get("constraints")
        username = email.split("@")[0]

        for domain, role_id in list(registered.items())[:-1]:
            if not email.endswith(domain):
                continue
            # found corresponding domain, check constraint
            if domain in constraints:
                constraint = constraints[domain]
            else:
                constraint = list(constraints.values())[-1]
            match = re.fullmatch(constraint, username)
            # return
            if match is not None:
                return self.getGuild().get_role(role_id)
            else:
                await self.event.user(
                    ctx, f"Rejecting e-mail: {self.sanitise(email)}")
                raise BadEmail(constraint=constraint)

        # domain not found, fallback to basic guest role
        role_id = registered.get(".")
        constraint = list(constraints.values())[-1]
        match = re.fullmatch(constraint, username)
        # return
        if match is not None:
            return self.getGuild().get_role(role_id)
        else:
            await self.event.user(ctx,
                                  f"Rejecting e-mail: {self.sanitise(email)}")
            raise BadEmail(constraint=constraint)

    async def _add_user(self, member: discord.Member, login: str,
                        role: discord.Role) -> str:
        code_source = string.ascii_uppercase.replace("O", "").replace(
            "I", "") + string.digits
        code = "".join(random.choices(code_source, k=8))

        repo_u.add(discord_id=member.id,
                   login=login,
                   group=role.name,
                   code=code)
        await self.event.user(
            member,
            f"Adding {member.id} to database (**{role.name}**, code `{code}`)."
        )
        return code

    async def _update_user(self, member: discord.Member) -> str:
        code_source = string.ascii_uppercase.replace("O", "").replace(
            "I", "") + string.digits
        code = "".join(random.choices(code_source, k=8))

        repo_u.update(discord_id=member.id, code=code, status="pending")
        await self.event.user(member,
                              f"{member.id} updated with code `{code}`")
        return code

    async def _send_verification_email(self, member: discord.Member,
                                       email: str, code: str) -> bool:
        cleartext = self.text.get("plaintext mail").format(
            guild_name=self.getGuild().name,
            code=code,
            bot_name=self.bot.user.name,
            git_hash=utils.git_get_hash()[:7],
            prefix=config.prefix,
        )

        richtext = self.text.get(
            "html mail",
            # styling
            color_bg="#54355F",
            color_fg="white",
            font_family="Arial,Verdana,sans-serif",
            # names
            guild_name=self.getGuild().name,
            bot_name=self.bot.user.name,
            user_name=member.name,
            # codes
            code=code,
            git_hash=utils.git_get_hash()[:7],
            prefix=config.prefix,
            # images
            bot_avatar=self.bot.user.avatar_url_as(static_format="png",
                                                   size=128),
            bot_avatar_size="120px",
            user_avatar=member.avatar_url_as(static_format="png", size=32),
            user_avatar_size="20px",
        )

        msg = MIMEMultipart("alternative")
        msg["Subject"] = self.text.get("mail subject",
                                       guild_name=self.getGuild().name,
                                       user_name=member.name)
        msg["From"] = self.config.get("email", "address")
        msg["To"] = email
        msg["Bcc"] = self.config.get("email", "address")
        msg.attach(MIMEText(cleartext, "plain"))
        msg.attach(MIMEText(richtext, "html"))

        with smtplib.SMTP(self.config.get("email", "server"),
                          self.config.get("email", "port")) as server:
            server.starttls()
            server.ehlo()
            server.login(self.config.get("email", "address"),
                         self.config.get("email", "password"))
            server.send_message(msg)

    async def _add_verify_roles(self, member: discord.Member, db_user: object):
        """Return True if reverified"""
        verify = self.getVerifyRole()
        group = discord.utils.get(self.getGuild().roles, name=db_user.group)

        await member.add_roles(verify, group, reason="Verification")

    ##
    ## Error catching
    ##

    @commands.Cog.listener()
    async def on_command_error(self, ctx: commands.Context, error):
        # try to get original error
        if hasattr(ctx.command, "on_error") or hasattr(ctx.command,
                                                       "on_command_error"):
            return
        error = getattr(error, "original", error)

        # non-rubbergoddess exceptions are handled globally
        if not isinstance(error, rubbercog.RubbercogException):
            return

        # fmt: off
        # exceptions with parameters
        if isinstance(error, ProblematicVerification):
            await self.output.warning(
                ctx,
                self.text.get("ProblematicVerification", status=error.status))

            await self.event.user(
                ctx,
                f"Problem with verification: {error.login}: {error.status}")

        elif isinstance(error, BadEmail):
            await self.output.warning(
                ctx, self.text.get("BadEmail", constraint=error.constraint))

        elif isinstance(error, WrongVerificationCode):
            await self.output.warning(
                ctx,
                self.text.get("WrongVerificationCode",
                              mention=ctx.author.mention))

            await self.event.user(
                ctx,
                f"User ({error.login}) code mismatch: `{error.their}` != `{error.database}`"
            )

        # exceptions without parameters
        elif isinstance(error, VerificationException):
            await self.output.error(ctx, self.text.get(type(error).__name__))
Exemple #16
0
class Shop(rubbercog.Rubbercog):
    """Make use of your karma"""

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

        self.config = CogConfig("shop")
        self.text = CogText("shop")

    @commands.command()
    async def shop(self, ctx):
        """Display prices for various services"""
        embed = self.embed(
            ctx=ctx,
            title=self.text.get("info", "title"),
            description=self.text.get("info", "description"),
        )

        embed.add_field(
            name=self.text.get("info", "set"),
            value=self.config.get("set"),
        )
        embed.add_field(
            name=self.text.get("info", "reset"),
            value=self.config.get("reset"),
        )

        await ctx.send(embed=embed)
        await utils.room_check(ctx)

    @commands.bot_has_permissions(manage_nicknames=True)
    @commands.check(acl.check)
    @commands.group(name="nickname")
    async def nickname(self, ctx):
        """Change your nickname"""
        await utils.send_help(ctx)

    @commands.cooldown(rate=5, per=60, type=commands.BucketType.member)
    @commands.check(acl.check)
    @nickname.command(name="set")
    async def nickname_set(self, ctx, *, nick: str):
        """Set the nickname. Use command `shop` to see prices

        Attributes
        ----------
        nick: Your new nickname
        """
        # stop if user does not have nickname set
        if ctx.author.nick is None and nick is None or not len(nick):
            return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention))

        # check if user has karma
        if self.get_user_karma(ctx.author.id) < self.config.get("set"):
            return await ctx.send(
                self.text.get(
                    "not_enough_karma",
                    mention=ctx.author.mention,
                )
            )

        for char in ("@", "#", "`", "'", '"'):
            if char in nick:
                return await ctx.send(self.text.get("bad_character", mention=ctx.author.mention))

        # set nickname
        try:
            await ctx.author.edit(nick=nick, reason="Nickname purchase")
        except discord.Forbidden:
            return await ctx.send(self.text.get("higher_role"))

        repo_k.updateMemberKarma(ctx.author.id, -1 * self.price_nick)
        await ctx.send(
            self.text.get(
                "new_nick",
                mention=ctx.author.mention,
                nick=discord.utils.escape_markdown(nick),
                value=self.price_nick,
            )
        )
        await self.event.user(ctx, f"Nickname changed to {nick}.")

    @commands.cooldown(rate=1, per=60, type=commands.BucketType.member)
    @commands.check(acl.check)
    @nickname.command(name="unset")
    async def nickname_unset(self, ctx):
        """Unset the nickname"""
        if ctx.author.nick is None:
            return await ctx.send(self.text.get("no_nick", mention=ctx.author.mention))

        # check if user has karma
        if self.get_user_karma(ctx.author.id) < self.config.get("reset"):
            return await ctx.send(
                self.text.get(
                    "not_enough_karma",
                    mention=ctx.author.mention,
                )
            )

        nick = ctx.author.nick

        await ctx.author.edit(nick=None, reason="Nickname reset")
        await ctx.send(
            self.text.get(
                "nick_removed",
                mention=ctx.author.mention,
                nick=self.sanitise(nick),
            )
        )
        await self.event.user(ctx, "Nickname reset.")

    ##
    ## Logic
    ##

    def get_user_karma(self, user_id: int) -> int:
        return getattr(repo_k.getMember(user_id), "karma", 0)
Exemple #17
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("mover")
Exemple #18
0
class Mover(rubbercog.Rubbercog):
    """Move database objects"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("mover")

    ##
    ## Commands
    ##

    @commands.check(acl.check)
    @commands.group(name="move")
    async def move(self, ctx):
        """Move stuff"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @move.command(name="member")
    async def move_member(self, ctx, before: discord.Member,
                          after: discord.Member):
        """Move old member data to new one

        Roles from the `before` member are moved to the `after` one.
        """
        async with ctx.typing():
            result = self.move_user_data(before.id, after.id)
            try:
                await self.move_member_roles(before, after)
            except Exception as e:
                await self.console.error(ctx, "Could not migrate member roles",
                                         e)
            embed = self.move_user_embed(ctx, after, result)
            await ctx.send(embed=embed)

    @commands.check(acl.check)
    @move.command(name="user")
    async def move_user(self, ctx, before: int, after: int):
        """Move old user data to new one

        This is useful when the member is not on the server anymore.
        """
        async with ctx.typing():
            result = self.move_user_data(before, after)
            embed = self.move_user_embed(ctx, after, result)
            await ctx.send(embed=embed)

    ##
    ## Logic
    ##

    def move_user_data(self, before_id: int, after_id: int) -> Dict[str, int]:
        """Move user data

        Arguments
        ---------
        before_id: `int` Old user ID
        after_id: `int` New user ID

        Returns
        -------
        `dict`: mapping from table name to change counter
        """
        result = {}
        result["interaction"] = repo_interaction.move_user(before_id, after_id)
        result["karma"] = repo_karma.move_user(before_id, after_id)
        result["points"] = repo_points.move_user(before_id, after_id)
        result["user"] = repo_user.move_user(before_id, after_id)

        return result

    async def move_member_roles(self, before: discord.Member,
                                after: discord.Member):
        """Move roles from the before member to the after one."""
        roles = before.roles[1:]
        await after.add_roles(*roles, reason="Member migration")
        await before.remove_roles(*roles, reason="Member migration")

    ##
    ## Helper functions
    ##

    def move_user_embed(
        self,
        ctx,
        after: Union[discord.Member, int],
        result: Dict[str, int],
    ) -> discord.Embed:
        """Create embed for move_member and move_user

        Arguments
        ---------
        after: `Member` or `int` representing new user
        result: `Dict` mapping table - number of affected rows

        Returns
        -------
        `discord.Embed`
        """
        embed = self.embed(ctx=ctx,
                           title=self.text.get("user", "move"),
                           description=str(after))
        result_items = []
        for key, value in result.items():
            result_items.append(f"{key}: `{value}`")
        embed.add_field(
            name=self.text.get("user", "result"),
            value="\n".join(result_items),
        )
        return embed
Exemple #19
0
class Comments(rubbercog.Rubbercog):
    """Manage user information"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("comments")

    @commands.guild_only()
    @commands.check(acl.check)
    @commands.group(name="comment")
    async def comments(self, ctx):
        """Manage comments on guild users"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @comments.command(name="list")
    async def comments_list(self, ctx, user: Union[discord.Member,
                                                   discord.User]):
        comments: List[Comment] = Comment.get(ctx.guild.id, user.id)
        if not len(comments):
            return await ctx.reply(self.text.get("list", "none"))

        def format_comment(comment: Comment) -> str:
            timestamp: str = comment.timestamp.strftime("%Y-%m-%d %H:%M")
            author: discord.Member = ctx.guild.get_member(comment.author_id)
            author_name: str = (f"{comment.author_id}" if author is None else
                                discord.utils.escape_markdown(
                                    author.display_name))
            text: str = "\n".join(
                [f"> {line}" for line in comment.text.split("\n")])
            return f"**{author_name}**, {timestamp} (ID {comment.id}):\n{text}"

        response: str = "\n".join(
            [format_comment(comment) for comment in comments])
        stubs: List[str] = utils.paginate(response)
        await ctx.reply(stubs[0])
        if len(stubs) > 1:
            for stub in utils.paginate(response):
                await ctx.send(stub)

    @commands.check(acl.check)
    @comments.command(name="add")
    async def comments_add(self, ctx, user: Union[discord.Member, discord.User,
                                                  int], *, text: str):
        Comment.add(
            guild_id=ctx.guild.id,
            author_id=ctx.author.id,
            user_id=user.id,
            text=text,
        )
        await ctx.reply(self.text.get("add", "ok"))
        await self.event.sudo(ctx, f"Comment added on {user}.")

    @commands.check(acl.check)
    @comments.command(name="remove")
    async def comments_remove(self, ctx, id: int):
        success = Comment.remove(guild_id=ctx.guild.id, id=id)
        if not success:
            return await ctx.reply("remove", "none")
        await ctx.reply(self.text.get("remove", "ok"))
        await self.event.sudo(ctx, f"Comment with ID {id} removed.")
Exemple #20
0
class Meme(rubbercog.Rubbercog):
    """Interact with users"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("meme")
        self.config = CogConfig("meme")

        self.fishing_pool = self.config.get("_fishing")

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def hug(self,
                  ctx,
                  *,
                  target: Union[discord.Member, discord.Role] = None):
        """Hug someone!

        target: Discord user or role. If none, the bot will hug you.
        """
        if target is None:
            hugger = self.bot.user
            hugged = ctx.author
        else:
            hugger = ctx.author
            hugged = target

        if type(hugged) == discord.Role:
            repo_i.add(ctx.guild.id, "hug", hugger.id, None)
        else:
            repo_i.add(ctx.guild.id, "hug", hugger.id, hugged.id)

        await ctx.send(emote.hug_right +
                       (" **" + hugged.display_name +
                        "**" if type(target) == discord.Member else " ***" +
                        hugged.name + "***"))

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def whip(self, ctx, *, user: discord.Member = None):
        """Whip someone"""
        if user is None:
            whipper = self.bot.user
            whipped = ctx.author
        else:
            whipper = ctx.author
            whipped = user

        repo_i.add(ctx.guild.id, "whip", whipper.id, whipped.id)

        async with ctx.typing():
            url = whipped.avatar_url_as(format="jpg")
            response = requests.get(url)
            avatar = Image.open(BytesIO(response.content))

            frames = self.get_whip_frames(avatar)

            with BytesIO() as image_binary:
                frames[0].save(
                    image_binary,
                    format="GIF",
                    save_all=True,
                    append_images=frames[1:],
                    duration=30,
                    loop=0,
                    transparency=0,
                    disposal=2,
                    optimize=False,
                )
                image_binary.seek(0)
                await ctx.send(
                    file=discord.File(fp=image_binary, filename="whip.gif"))

            return

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def spank(self, ctx, *, user: discord.Member = None):
        """Spank someone"""
        if user is None:
            spanker = self.bot.user
            spanked = ctx.author
        else:
            spanker = ctx.author
            spanked = user

        repo_i.add(ctx.guild.id, "spank", spanker.id, spanked.id)

        async with ctx.typing():
            url = spanked.avatar_url_as(format="jpg")
            response = requests.get(url)
            avatar = Image.open(BytesIO(response.content))

            frames = self.get_spank_frames(avatar)

            with BytesIO() as image_binary:
                frames[0].save(
                    image_binary,
                    format="GIF",
                    save_all=True,
                    append_images=frames[1:],
                    duration=30,
                    loop=0,
                    transparency=0,
                    disposal=2,
                    optimize=False,
                )
                image_binary.seek(0)
                await ctx.send(
                    file=discord.File(fp=image_binary, filename="spank.gif"))

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def pet(self, ctx, *, member: discord.Member = None):
        """Pet someone!

        member: Discord user. If none, the bot will pet you.
        """
        if member is None:
            petter = self.bot.user
            petted = ctx.author
        else:
            petter = ctx.author
            petted = member

        repo_i.add(ctx.guild.id, "pet", petter.id, petted.id)

        async with ctx.typing():
            url = petted.avatar_url_as(format="jpg")
            response = requests.get(url)
            avatar = Image.open(BytesIO(response.content))

            frames = self.get_pet_frames(avatar)

            with BytesIO() as image_binary:
                frames[0].save(
                    image_binary,
                    format="GIF",
                    save_all=True,
                    append_images=frames[1:],
                    duration=40,
                    loop=0,
                    transparency=0,
                    disposal=2,
                    optimize=False,
                )
                image_binary.seek(0)
                await ctx.send(
                    file=discord.File(fp=image_binary, filename="pet.gif"))

            return

            # this is a more intensive solution that creates non-transparent
            # background without it being glitched

            with BytesIO() as image_binary:
                image_utils.save_gif(frames, 30, image_binary)
                image_binary.seek(0)

                await ctx.send(
                    file=discord.File(fp=image_binary, filename="pet.gif"))

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def hyperpet(self, ctx, *, member: discord.Member = None):
        """Pet someone really hard

        member: Discord user. If none, the bot will hyperpet you.
        """
        if member is None:
            petter = self.bot.user
            petted = ctx.author
        else:
            petter = ctx.author
            petted = member

        repo_i.add(ctx.guild.id, "hyperpet", petter.id, petted.id)

        async with ctx.typing():
            url = petted.avatar_url_as(format="jpg")
            response = requests.get(url)
            avatar = Image.open(BytesIO(response.content))

            frames = self.get_hyperpet_frames(avatar)

            with BytesIO() as image_binary:
                frames[0].save(
                    image_binary,
                    format="GIF",
                    save_all=True,
                    append_images=frames[1:],
                    duration=30,
                    loop=0,
                    transparency=0,
                    disposal=2,
                    optimize=False,
                )
                image_binary.seek(0)
                await ctx.send(
                    file=discord.File(fp=image_binary, filename="hyperpet.gif")
                )

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def bonk(self, ctx, *, member: discord.Member = None):
        """Bonk someone

        member: Discord user. If none, the bot will bonk you.
        """
        if member is None:
            bonker = self.bot.user
            bonked = ctx.author
        else:
            bonker = ctx.author
            bonked = member

        repo_i.add(ctx.guild.id, "bonk", bonker.id, bonked.id)

        async with ctx.typing():
            url = bonked.avatar_url_as(format="jpg")
            response = requests.get(url)
            avatar = Image.open(BytesIO(response.content))

            frames = self.get_bonk_frames(avatar)

            with BytesIO() as image_binary:
                frames[0].save(
                    image_binary,
                    format="GIF",
                    save_all=True,
                    append_images=frames[1:],
                    duration=30,
                    loop=0,
                    transparency=0,
                    disposal=2,
                    optimize=False,
                )
                image_binary.seek(0)
                await ctx.send(
                    file=discord.File(fp=image_binary, filename="bonk.gif"))

    @commands.guild_only()
    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def slap(self, ctx, *, member: discord.Member = None):
        """Slap someone!

        member: Discord user. If none, the bot will slap you.
        """
        if member is None:
            slapper = self.bot.user
            slapped = ctx.author
        else:
            slapper = ctx.author
            slapped = member

        options = ["つ", "づ", "ノ"]

        repo_i.add(ctx.guild.id, "slap", slapper.id, slapped.id)

        await ctx.send("**{}**{} {}".format(
            self.sanitise(slapper.display_name),
            random.choice(options),
            self.sanitise(slapped.display_name),
        ))

    @commands.guild_only()
    @commands.cooldown(rate=1, per=5, type=commands.BucketType.user)
    @commands.command()
    async def relations(self, ctx, *, user: discord.User = None):
        """Get your information about hugs, pets, ..."""
        if user is None:
            user = ctx.author

        embed = self.embed(
            ctx=ctx,
            description=f"**{self.sanitise(user.display_name)}**\n"
            f"{self.text.get('relations_help')}",
        )

        for action in ("hug", "pet", "hyperpet", "slap", "spank", "whip",
                       "bonk"):
            lookup = repo_i.get_user_action(user.id, ctx.guild.id, action)

            if lookup[0] == 0 and lookup[1] == 0:
                continue

            value = self.text.get("value", gave=lookup[0], got=lookup[1])
            embed.add_field(name=f"{config.prefix}{action}", value=value)

        await ctx.send(embed=embed)
        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=120, type=commands.BucketType.user)
    @commands.command(aliases=["owo"])
    async def uwu(self, ctx, *, message: str = None):
        """UWUize message"""
        if message is None:
            text = "OwO!"
        else:
            text = self.sanitise(self.uwuize(message),
                                 limit=1900,
                                 markdown=True)
        await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " +
                       text)
        await utils.delete(ctx.message)

    @commands.cooldown(rate=5, per=120, type=commands.BucketType.user)
    @commands.command(aliases=["rcase", "randomise"])
    async def randomcase(self, ctx, *, message: str = None):
        """raNdOMisE cAsInG"""
        if message is None:
            text = "O.o"
        else:
            text = ""
            for letter in message:
                if letter.isalpha():
                    text += letter.upper() if random.choice(
                        (True, False)) else letter.lower()
                else:
                    text += letter
            text = self.sanitise(text[:1900], markdown=True)
        await ctx.send(f"**{self.sanitise(ctx.author.display_name)}**\n>>> " +
                       text)
        await utils.delete(ctx.message)

    @commands.cooldown(rate=3, per=10, type=commands.BucketType.user)
    @commands.command()
    async def fish(self, ctx):
        """Go fishing!"""
        roll = random.uniform(0, 1)
        options = None
        for probabilty, harvest in self.fishing_pool.items():
            if roll >= float(probabilty):
                options = harvest
                break
        else:
            return await ctx.send(
                self.text.get("fishing_fail", mention=ctx.author.mention))

        await ctx.send(random.choice(options))

    ##
    ## Logic
    ##

    @staticmethod
    def uwuize(string: str) -> str:
        # Adapted from https://github.com/PhasecoreX/PCXCogs/blob/master/uwu/uwu.py
        result = []

        def uwuize_word(string: str) -> str:
            try:
                if string.lower()[0] == "m" and len(string) > 2:
                    w = "W" if string[1].isupper() else "w"
                    string = string[0] + w + string[1:]
            except Exception:
                # this is how we handle emojis
                pass
            string = string.replace("r", "w").replace("R", "W")
            string = string.replace("ř", "w").replace("Ř", "W")
            string = string.replace("l", "w").replace("L", "W")
            string = string.replace("?", "?" * random.randint(1, 3))
            string = string.replace("'", ";" * random.randint(1, 3))
            if string[-1] == ",":
                string = string[:-1] + "." * random.randint(2, 3)

            return string

        result = " ".join(
            [uwuize_word(s) for s in string.split(" ") if len(s)])
        if result[-1] == "?":
            result += " UwU"
        if result[-1] == "!":
            result += " OwO"
        if result[-1] == ".":
            result = result[:-1] + "," * random.randint(2, 4)

        return result

    @staticmethod
    def round_image(frame_avatar: Image.Image) -> Image.Image:
        """Convert square avatar to circle"""
        frame_mask = Image.new("1", frame_avatar.size, 0)
        draw = ImageDraw.Draw(frame_mask)
        draw.ellipse((0, 0) + frame_avatar.size, fill=255)
        frame_avatar.putalpha(frame_mask)
        return frame_avatar

    @staticmethod
    def get_pet_frames(avatar: Image.Image) -> List[Image.Image]:
        """Get frames for the pet"""
        frames = []
        width, height = 148, 148
        vertical_offset = (0, 0, 0, 0, 1, 2, 3, 4, 5, 4, 3, 2, 2, 1, 0)

        frame_avatar = image_utils.round_image(avatar.resize((100, 100)))

        for i in range(14):
            img = "%02d" % (i + 1)
            frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
            hand = Image.open(f"data/meme/pet/{img}.png")
            frame.paste(frame_avatar, (35, 25 + vertical_offset[i]),
                        frame_avatar)
            frame.paste(hand, (10, 5), hand)
            frames.append(frame)

        return frames

    @staticmethod
    def get_hyperpet_frames(avatar: Image.Image) -> List[Image.Image]:
        """Get frames for the hyperpet"""
        frames = []
        width, height = 148, 148
        vertical_offset = (0, 1, 2, 3, 1, 0)

        avatar = image_utils.round_image(avatar.resize((100, 100)))
        avatar_pixels = np.array(avatar)
        git_hash = int(utils.git_get_hash(), 16)

        for i in range(6):
            deform_hue = git_hash % 100**(i + 1) // 100**i / 100
            frame_avatar = Image.fromarray(
                image_utils.shift_hue(avatar_pixels, deform_hue))

            img = "%02d" % (i + 1)
            frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
            hand = Image.open(f"data/meme/hyperpet/{img}.png")
            frame.paste(frame_avatar, (35, 25 + vertical_offset[i]),
                        frame_avatar)
            frame.paste(hand, (10, 5), hand)
            frames.append(frame)

        return frames

    @staticmethod
    def get_bonk_frames(avatar: Image.Image) -> List[Image.Image]:
        """Get frames for the bonk"""
        frames = []
        width, height = 200, 170
        deformation = (0, 0, 0, 5, 10, 20, 15, 5)

        avatar = image_utils.round_image(avatar.resize((100, 100)))

        for i in range(8):
            img = "%02d" % (i + 1)
            frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
            bat = Image.open(f"data/meme/bonk/{img}.png")

            frame_avatar = avatar.resize((100, 100 - deformation[i]))

            frame.paste(frame_avatar, (80, 60 + deformation[i]), frame_avatar)
            frame.paste(bat, (10, 5), bat)
            frames.append(frame)

        return frames

    @staticmethod
    def get_whip_frames(avatar: Image.Image) -> List[Image.Image]:
        """Get frames for the whip"""
        frames = []
        width, height = 250, 150
        deformation = (0, 0, 0, 0, 0, 0, 0, 0, 2, 3, 5, 9, 6, 4, 3, 0, 0, 0, 0,
                       0, 0, 0, 0, 0, 0, 0)
        translation = (0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 2, 3, 3, 3, 2, 1, 0, 0,
                       0, 0, 0, 0, 0, 0, 0)

        avatar = image_utils.round_image(avatar.resize((100, 100)))

        for i in range(26):
            img = "%02d" % (i + 1)
            frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
            whip_frame = Image.open(f"data/meme/whip/{img}.png").resize(
                (150, 150))

            frame_avatar = avatar.resize((100 - deformation[i], 100))

            frame.paste(frame_avatar,
                        (135 + deformation[i] + translation[i], 25),
                        frame_avatar)
            frame.paste(whip_frame, (0, 0), whip_frame)
            frames.append(frame)

        return frames

    @staticmethod
    def get_spank_frames(avatar: Image.Image) -> List[Image.Image]:
        """Get frames for the spank"""
        frames = []
        width, height = 200, 120
        deformation = (4, 2, 1, 0, 0, 0, 0, 3)

        avatar = image_utils.round_image(avatar.resize((100, 100)))

        for i in range(8):
            img = "%02d" % (i + 1)
            frame = Image.new("RGBA", (width, height), (54, 57, 63, 1))
            spoon = Image.open(f"data/meme/spank/{img}.png").resize((100, 100))

            frame_avatar = avatar.resize(
                (100 + 2 * deformation[i], 100 + 2 * deformation[i]))

            frame.paste(spoon, (10, 15), spoon)
            frame.paste(frame_avatar,
                        (80 - deformation[i], 10 - deformation[i]),
                        frame_avatar)
            frames.append(frame)

        return frames
Exemple #21
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("random")
Exemple #22
0
class Random(rubbercog.Rubbercog):
    """Pick, flip, roll dice"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("random")

    @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user)
    @commands.check(check.is_verified)
    @commands.command()
    async def pick(self, ctx, *args):
        """"Pick an option"""
        for i, arg in enumerate(args):
            if arg.endswith("?"):
                args = args[i + 1:]
                break

        if not len(args):
            return

        option = self.sanitise(random.choice(args), limit=50)
        if option is not None:
            await ctx.send(
                self.text.get("answer",
                              mention=ctx.author.mention,
                              option=option))

        await utils.room_check(ctx)

    @commands.cooldown(rate=3, per=20.0, type=commands.BucketType.user)
    @commands.check(check.is_verified)
    @commands.command()
    async def flip(self, ctx):
        """Yes/No"""
        option = random.choice(self.text.get("flip"))
        await ctx.send(
            self.text.get("answer", mention=ctx.author.mention, option=option))
        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.check(check.is_verified)
    @commands.command()
    async def random(self, ctx, first: int, second: int = None):
        """Pick number from interval"""
        if second is None:
            second = 0

        if first > second:
            first, second = second, first

        option = random.randint(first, second)
        await ctx.send(
            self.text.get("answer", mention=ctx.author.mention, option=option))
        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel)
    @commands.check(check.is_verified)
    @commands.command(aliases=["unsplash"])
    async def picsum(self, ctx, *, seed: str = None):
        """Get random image from picsum.photos"""
        size = "900/600"
        url = "https://picsum.photos/"
        if seed:
            url += "seed/" + seed + "/"
        url += f"{size}.jpg?random={ctx.message.id}"

        # we cannot use the URL directly, because embed will contain other image than its thumbnail
        image = requests.get(url)
        if image.status_code != 200:
            return await ctx.send(f"E{image.status_code}")

        # get image info
        # example url: https://i.picsum.photos/id/857/600/360.jpg?hmac=.....
        image_id = image.url.split("/id/", 1)[1].split("/")[0]
        image_info = requests.get(f"https://picsum.photos/id/{image_id}/info")
        try:
            image_url = image_info.json()["url"]
        except:
            image_url = discord.Embed.Empty

        embed = self.embed(ctx=ctx,
                           title=discord.Embed.Empty,
                           description=image_url,
                           footer=seed)
        embed.set_image(url=image.url)
        await ctx.send(embed=embed)

        await utils.room_check(ctx)
Exemple #23
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("comments")
Exemple #24
0
class Errors(rubbercog.Rubbercog):
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("errors")

    @commands.Cog.listener()
    async def on_command_error(self, ctx, error):  # noqa: C901
        """Handle errors"""
        if hasattr(ctx.command, "on_error") or hasattr(ctx.command,
                                                       "on_command_error"):
            return
        error = getattr(error, "original", error)

        throw_error = await self._send_exception_message(ctx, error)
        if not throw_error:
            return

        # display error message
        await self.output.error(ctx, "", error)
        output = self._format_output(ctx, error)
        print(output)

        # send traceback to dedicated channel
        channel_stdout = self.bot.get_channel(config.get("channels", "stdout"))
        output = list(output[0 + i:1960 + i]
                      for i in range(0, len(output), 1960))
        sent = []
        for message in output:
            m = await channel_stdout.send("```\n{}```".format(message))
            sent.append(m)

        # send notification to botdev
        await self._send_notification(ctx, output, error, sent[0].jump_url)

    ##
    ## Logic
    ##

    async def _send_exception_message(self, ctx: commands.Context,
                                      error: Exception) -> bool:
        """Return True if error should be thrown"""

        # fmt: off
        if isinstance(error, acl_repo.ACLException):
            await self.output.error(
                ctx,
                self.text.get("acl", type(error).__name__, error=str(error)))
            return False

        # cog exceptions are handled in their cogs
        if isinstance(error, rubbercog.RubbercogException):
            if type(error) is not rubbercog.RubbercogException:
                return False
            await self.output.error(ctx, self.text.get("RubbercogException"),
                                    error)
            return False

        if type(error) == commands.CommandNotFound:
            return

        # Exceptions with parameters
        if type(error) == commands.MissingRequiredArgument:
            await self.output.warning(
                ctx,
                self.text.get("MissingRequiredArgument",
                              param=error.param.name))
            return False
        if type(error) == commands.CommandOnCooldown:
            time = utils.seconds2str(error.retry_after)
            await self.output.warning(
                ctx, self.text.get("CommandOnCooldown", time=time))
            return False
        if type(error) == commands.MaxConcurrencyReached:
            await self.output.warning(
                ctx,
                self.text.get("MaxConcurrencyReached",
                              num=error.number,
                              per=error.per.name))
            return False
        if type(error) == commands.MissingRole:
            # TODO Is !r OK, or should we use error.missing_role.name?
            role = f"`{error.missing_role!r}`"
            await self.output.warning(ctx,
                                      self.text.get("MissingRole", role=role))
            return False
        if type(error) == commands.BotMissingRole:
            role = f"`{error.missing_role!r}`"
            await self.output.error(ctx,
                                    self.text.get("BotMissingRole", role=role))
            return False
        if type(error) == commands.MissingAnyRole:
            roles = ", ".join(f"`{r!r}`" for r in error.missing_roles)
            await self.output.warning(
                ctx, self.text.get("MissingAnyRole", roles=roles))
            return False
        if type(error) == commands.BotMissingAnyRole:
            roles = ", ".join(f"`{r!r}`" for r in error.missing_roles)
            await self.output.error(
                ctx, self.text.get("BotMissingAnyRole", roles=roles))
            return False
        if type(error) == commands.MissingPermissions:
            perms = ", ".join(f"`{p}`" for p in error.missing_perms)
            await self.output.warning(
                ctx, self.text.get("MissingPermissions", perms=perms))
            return False
        if type(error) == commands.BotMissingPermissions:
            perms = ", ".join(f"`{p}`" for p in error.missing_perms)
            await self.output.error(
                ctx, self.text.get("BotMissingPermissions", perms=perms))
            return False
        if type(error) == commands.BadUnionArgument:
            await self.output.warning(
                ctx, self.text.get("BadUnionArgument", param=error.param.name))
            return False
        if type(error) == commands.BadBoolArgument:
            await self.output.warning(
                ctx, self.text.get("BadBoolArgument", arg=error.argument))
            return False

        # All cog-related errors
        if isinstance(error, smtplib.SMTPException):
            await self.console.error(ctx, "Could not send e-mail", error)
            await ctx.send(
                self.text.get("SMTPException", name=type(error).__name__))
            return False

        if type(error) == commands.ExtensionFailed:
            await self.output.error(
                ctx,
                self.text.get(
                    type(error).__name__,
                    extension=f"{error.name!r}",
                    error_name=error.original.__class__.__name__,
                    error=str(error.original),
                ))
            return False
        if isinstance(error, commands.ExtensionError):
            await self.output.critical(
                ctx,
                self.text.get(type(error).__name__,
                              extension=f"{error.name!r}"))
            return False

        # The rest of client exceptions
        if isinstance(error, commands.CommandError) or isinstance(
                error, discord.ClientException):
            await self.output.warning(ctx, self.text.get(type(error).__name__))
            return False

        # DiscordException, non-critical errors
        if type(error) in (
                discord.errors.NoMoreItems,
                discord.errors.HTTPException,
                discord.errors.Forbidden,
                discord.errors.NotFound,
        ):
            await self.output.error(ctx, self.text.get(type(error).__name__))
            await self.console.error(ctx, type(error).__name__, error)
            return False

        # DiscordException, critical errors
        if type(error) in (discord.errors.DiscordException,
                           discord.errors.GatewayNotFound):
            await self.output.error(ctx, self.text.get(type(error).__name__))

        # Database
        if isinstance(error, sqlalchemy.exc.SQLAlchemyError):
            error_name = ".".join(
                [type(error).__module__,
                 type(error).__name__])
            await self.output.critical(ctx, error_name)
            await self.console.critical(ctx, "Database error", error)
            await self.event.user(
                ctx,
                f"Database reported`{error_name}`. The session may be invalidated <@{config.admin_id}>",
                escape_markdown=False,
            )
            return False
        # fmt: on

        return True

    def _format_output(self, ctx: commands.Context, error) -> str:
        if isinstance(ctx.channel, discord.TextChannel):
            location = f"{ctx.guild.name}/{ctx.channel.name} ({ctx.channel.id})"
        else:
            location = type(ctx.channel).__name__

        output = "{command} by {user} in {location}\n".format(
            command=config.prefix + ctx.command.qualified_name,
            user=str(ctx.author),
            location=location,
        )

        output += "".join(
            traceback.format_exception(type(error), error,
                                       error.__traceback__))

        return output

    async def _send_notification(self, ctx: commands.Context, output: str,
                                 error: Exception, traceback_url: str):
        channel = self.bot.get_channel(config.channel_botdev)
        embed = self.embed(ctx=ctx, color=discord.Color.from_rgb(255, 0, 0))

        # fmt: off
        footer = "{user} in {channel}".format(
            user=str(ctx.author),
            channel=ctx.channel.name
            if isinstance(ctx.channel, discord.TextChannel) else type(
                ctx.channel).__name__)
        embed.set_footer(text=footer, icon_url=ctx.author.avatar_url)

        stack = output[-1]
        if len(stack) > 255:
            stack = "…" + stack[-255:]
        embed.add_field(
            name=type(error).__name__,
            value=f"```{stack}```",
            inline=False,
        )
        embed.add_field(name="Traceback", value=traceback_url, inline=False)
        await channel.send(embed=embed)
Exemple #25
0
class Librarian(rubbercog.Rubbercog):
    """Knowledge and information based commands"""

    # TODO Move czech strings to text.default.json

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

        self.config = CogConfig("librarian")
        self.text = CogText("librarian")

    @commands.command(aliases=["svátek"])
    async def svatek(self, ctx):
        url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}"
        res = await utils.fetch_json(url)
        names = []
        for i in res:
            names.append(i["name"])
        if len(names):
            await ctx.send(
                self.text.get("nameday", "cs", name=", ".join(names)))
        else:
            await ctx.send(self.text.get("nameday", "cs0"))

    @commands.command(aliases=["sviatok"])
    async def meniny(self, ctx):
        url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}"
        res = await utils.fetch_json(url)
        names = []
        for i in res:
            names.append(i["name"])
        if len(names):
            await ctx.send(
                self.text.get("nameday", "sk", name=", ".join(names)))
        else:
            await ctx.send(self.text.get("nameday", "sk0"))

    @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"])
    async def week(self, ctx: commands.Context):
        """See if the current week is odd or even"""
        cal_week = date.today().isocalendar()[1]
        stud_week = cal_week - self.config.get("starting_week") + 1
        even, odd = self.text.get("week", "even"), self.text.get("week", "odd")
        cal_type = even if cal_week % 2 == 0 else odd
        stud_type = even if stud_week % 2 == 0 else odd

        embed = self.embed(ctx=ctx)
        embed.add_field(
            name=self.text.get("week", "calendar"),
            value="{} ({})".format(cal_type, cal_week),
        )
        if 0 < stud_week <= self.config.get("total_weeks"):
            embed.add_field(
                name=self.text.get("week", "study"),
                value=str(stud_week),
            )
        await ctx.send(embed=embed)

        await utils.delete(ctx)
        await utils.room_check(ctx)

    @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"])
    async def weather(self, ctx, *, place: str = "Brno"):
        token = self.config.get("weather_token")
        place = place[:100]

        if "&" in place:
            return await ctx.send(self.text.get("weather", "place_not_found"))

        url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place +
               "&units=metric&lang=cz&appid=" + token)
        res = await utils.fetch_json(url)
        """ Example response
        {
            "coord":{
                "lon":16.61,
                "lat":49.2
            },
            "weather":[
                {
                    "id":800,
                    "temp_maixn":"Clear",
                    "description":"jasno",
                    "icon":"01d"
                }
            ],
            "base":"stations",
            "main":{
                "temp":21.98,
                "feels_like":19.72,
                "temp_min":20.56,
                "temp_max":23,
                "pressure":1013,
                "humidity":53
            },
            "visibility":10000,
            "wind":{
                "speed":4.1,
                "deg":50
            },
            "clouds":{
                "all":0
            },
            "dt":1595529518,
            "sys":{
                "type":1,
                "id":6851,
                "country":"CZ",
                "sunrise":1595474051,
                "sunset":1595529934
            },
            "timezone":7200,
            "id":3078610,
            "name":"Brno",
            "cod":200
        }
        """

        if str(res["cod"]) == "404":
            return await ctx.send(self.text.get("weather", "place_not_found"))
        elif str(res["cod"]) == "401":
            return await ctx.send(self.text.get("weather", "token"))
        elif str(res["cod"]) != "200":
            return await ctx.send(
                self.text.get("weather", "place_error",
                              message=res["message"]))

        title = res["weather"][0]["description"]
        description = (self.text.get("weather",
                                     "description",
                                     name=res["name"],
                                     country=res["sys"]["country"])
                       if "country" in res["sys"] else self.text.get(
                           "weather", "description_short", name=res["name"]))
        if description.endswith("CZ"):
            description = description[:-4]
        embed = self.embed(ctx=ctx,
                           title=title[0].upper() + title[1:],
                           description=description)
        embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png".
                            format(res["weather"][0]["icon"]))

        embed.add_field(
            name=self.text.get("weather", "temperature"),
            value=self.text.get(
                "weather",
                "temperature_value",
                real=round(res["main"]["temp"], 1),
                feel=round(res["main"]["feels_like"], 1),
            ) + "\n" + self.text.get(
                "weather",
                "temperature_minmax",
                min=round(res["main"]["temp_min"], 1),
                max=round(res["main"]["temp_max"], 1),
            ),
            inline=False,
        )

        embed.add_field(
            name=self.text.get("weather", "humidity"),
            value=str(res["main"]["humidity"]) + " %",
        )
        embed.add_field(
            name=self.text.get("weather", "clouds"),
            value=(str(res["clouds"]["all"]) + " %"),
        )
        if "visibility" in res:
            if res["visibility"] == 10000:
                value = self.text.get("weather", "visibility_max")
            else:
                value = f"{res['visibility']/1000} km"
            embed.add_field(
                name=self.text.get("weather", "visibility"),
                value=value,
            )
        embed.add_field(name=self.text.get("weather", "wind"),
                        value=f"{res['wind']['speed']} m/s")

        await utils.send(ctx, embed=embed)
        await utils.room_check(ctx)

    @commands.command(aliases=["b64"])
    async def base64(self, ctx, direction: str, *, data: str):
        """Get base64 data

        direction: [encode, e, -e; decode, d, -d]
        text: string (under 1000 characters)
        """
        if data is None or not len(data):
            return await utils.send_help(ctx)

        data = data[:1000]
        if direction in ("encode", "e", "-e"):
            direction = "encode"
            result = base64.b64encode(data.encode("utf-8")).decode("utf-8")
        elif direction in ("decode", "d", "-d"):
            direction = "decode"
            try:
                result = base64.b64decode(data.encode("utf-8")).decode("utf-8")
            except Exception as e:
                return await ctx.send(f"> {e}")
        else:
            return await utils.send_help(ctx)

        quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "")
        await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```")

        await utils.room_check(ctx)

    @commands.command()
    async def hashlist(self, ctx):
        """Get list of available hash functions"""
        result = "**hashlib**\n"
        result += "> " + " ".join(sorted(hashlib.algorithms_available))

        await ctx.send(result)

    @commands.command()
    async def hash(self, ctx, fn: str, *, data: str):
        """Get hash function result

        Run hashlist command to see available algorithms
        """
        if fn in hashlib.algorithms_available:
            result = hashlib.new(fn, data.encode("utf-8")).hexdigest()
        else:
            return await ctx.send(self.text.get("invalid_hash"))

        quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "")
        await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```")

    @commands.command(aliases=["maclookup"])
    async def macaddress(self, ctx, mac: str):
        """Get information about MAC address"""
        apikey = self.config.get("maclookup_token")

        if apikey == 0:
            return await self.output.error(
                ctx,
                self.text.get("maclookup", "no_token"),
            )

        if "&" in mac or "?" in mac:
            return await self.output.error(
                ctx,
                self.text.get("maclookup",
                              "bad_mac",
                              mention=ctx.author.mention),
            )

        url = f"https://api.maclookup.app/v2/macs/{mac}?format=json&apiKey={apikey}"
        res = await utils.fetch_json(url)

        if res["success"] is False:
            embed = self.embed(
                ctx=ctx,
                title=self.text.get("maclookup",
                                    "error",
                                    errcode=res["errorCode"]),
                description=res["error"],
                footer="maclookup.app",
            )
            return await ctx.send(embed=embed)

        if res["found"] is False:
            embed = self.embed(
                ctx=ctx,
                title=self.text.get("maclookup", "error", errcode="404"),
                description=self.text.get("maclookup", "not_found"),
                footer="maclookup.app",
            )
            return await ctx.send(embed=embed)

        embed = self.embed(ctx=ctx,
                           title=res["macPrefix"],
                           footer="maclookup.app")
        embed.add_field(
            name=self.text.get("maclookup", "company"),
            value=res["company"],
            inline=False,
        )
        embed.add_field(name=self.text.get("maclookup", "country"),
                        value=res["country"])

        block = f"`{res['blockStart']}`"
        if res["blockStart"] != res["blockEnd"]:
            block += f"\n`{res['blockEnd']}`"
        embed.add_field(name=self.text.get("maclookup", "block"),
                        value=f'`{res["blockStart"]}`')

        await ctx.send(embed=embed)

    @commands.cooldown(rate=2, per=20, type=commands.BucketType.user)
    # The API has limit of 45 requests per minute
    @commands.cooldown(rate=45, per=55, type=commands.BucketType.default)
    @commands.command(aliases=["iplookup"])
    async def ipaddress(self, ctx, query: str):
        """Get information about an IP address or a domain name"""
        if "&" in query or "?" in query or not len(query):
            return await self.output.error(
                ctx,
                self.text.get("iplookup",
                              "bad_query",
                              mention=ctx.author.mention),
            )

        url = (
            f"http://ip-api.com/json/{query}"
            "?fields=query,status,message,country,regionName,city,lat,lon,isp,org"
        )
        res = await utils.fetch_json(url)
        # TODO The API states that we should be listening for the `X-Rl` header.
        # If it is `0`, we must stop for `X-ttl` seconds.
        # https://ip-api.com/docs/api:json

        if res["status"] == "fail":
            embed = self.embed(
                ctx=ctx,
                title=self.text.get("iplookup", "error"),
                description="`" + res["message"] + "`",
                footer="ip-api.com",
            )
            return await ctx.send(embed=embed)

        embed = self.embed(ctx=ctx, title=res["query"], footer="ip-api.com")
        embed.add_field(
            name=res["city"],
            value=f"{res['regionName']}, {res['country']}",
            inline=False,
        )
        embed.add_field(
            name=self.text.get("iplookup", "geo"),
            value=f"{res['lon']}, {res['lat']}",
        )
        embed.add_field(
            name=self.text.get("iplookup", "org"),
            value=res["org"],
        )
        embed.add_field(
            name=self.text.get("iplookup", "isp"),
            value=res["isp"],
        )

        await ctx.send(embed=embed)
Exemple #26
0
class Roles(rubbercog.Rubbercog):
    """Manage roles and subjects"""
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("roles")
        self.text = CogText("roles")

        self.limit_programmes = {}
        self.limit_interests = {}

    ##
    ## Getters
    ##

    def getLimitProgrammes(self, channel: discord.TextChannel) -> discord.Role:
        gid = str(channel.guild.id)
        if gid not in self.limit_programmes:
            self.limit_programmes[gid] = discord.utils.get(
                channel.guild.roles, name="---PROGRAMMES")
        return self.limit_programmes[gid]

    def getLimitInterests(self, channel: discord.TextChannel) -> discord.Role:
        gid = str(channel.guild.id)
        if gid not in self.limit_interests:
            self.limit_interests[gid] = discord.utils.get(channel.guild.roles,
                                                          name="---INTERESTS")
        return self.limit_interests[gid]

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_message(self, message):
        """Listen for react-to-role message"""
        if not isinstance(message.channel, discord.TextChannel):
            return

        if message.channel.id not in self.config.get("r2r_channels"):
            return

        emote_channel_list = await self._message_to_tuple_list(message)

        if emote_channel_list is None:
            # do not throw errors if nothing is found
            return

        for emote_channel in emote_channel_list:
            try:
                await message.add_reaction(emote_channel[0])
            except (discord.errors.Forbidden, discord.errors.HTTPException):
                continue

    @commands.Cog.listener()
    async def on_raw_message_edit(self,
                                  payload: discord.RawMessageUpdateEvent):
        """Listen for react-to-role message changes"""
        if payload.channel_id not in self.config.get("r2r_channels"):
            return

        message_channel = self.bot.get_channel(payload.channel_id)
        message = await message_channel.fetch_message(payload.message_id)

        # make a list of current emotes
        emote_channel_list = await self._message_to_tuple_list(message)
        for emote_channel in emote_channel_list:
            try:
                await message.add_reaction(emote_channel[0])
            except (discord.errors.Forbidden, discord.errors.HTTPException):
                continue

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload):
        # extract data from payload
        payload = await self._reaction_payload_to_tuple(payload)
        if payload is None:
            return
        channel, message, member, emoji = payload
        emote_channel_list = await self._message_to_tuple_list(message)
        result = None
        for emote_channel in emote_channel_list:
            if str(emoji) == emote_channel[0]:
                # try both subject and role
                subject = await self._get_subject(channel, emote_channel[1])
                if subject is not None:
                    result = await self._subject_add(message.channel, member,
                                                     subject)
                    break
                role = await self._get_role(channel, emote_channel[1])
                if role is not None:
                    result = await self._role_add(message.channel, member,
                                                  role)
                    break
        else:
            # another emote was added
            result = None

        if not result:
            try:
                await message.remove_reaction(emoji, member)
            except Exception:
                pass

    @commands.Cog.listener()
    async def on_raw_reaction_remove(self, payload):
        # extract data from payload
        payload = await self._reaction_payload_to_tuple(payload)
        if payload is None:
            return
        channel, message, member, emoji = payload
        emote_channel_list = await self._message_to_tuple_list(message)
        for emote_channel in emote_channel_list:
            if str(emoji) == emote_channel[0]:
                # try both subject and role
                subject = await self._get_subject(channel, emote_channel[1])
                if subject is not None:
                    await self._subject_remove(message.channel, member,
                                               subject)
                    break
                role = await self._get_role(channel, emote_channel[1])
                if role is not None:
                    await self._role_remove(message.channel, member, role)
                    break

    ##
    ## Helper functions
    ##
    async def _get_subject(self, location,
                           shortcut: str) -> discord.TextChannel:
        db_subject = repo_s.get(shortcut)
        if db_subject is not None:
            return discord.utils.get(location.guild.text_channels,
                                     name=shortcut)
        return

    async def _get_role(self, location, role: str) -> discord.Role:
        return discord.utils.get(location.guild.roles, name=role)

    async def _message_to_tuple_list(self, message: discord.Message) -> list:
        """Return (emote, channel/role) list"""
        # preprocess message content
        content = message.content.replace("*",
                                          "").replace("_",
                                                      "").replace("#", "")
        try:
            content = content.rstrip().split("\n")
        except ValueError:
            await message.channel.send(self.text.get("role_help"))
            return

        # check every line
        result = []
        for line in content:
            try:
                line_ = line.split(" ")
                emote = line_[0]
                target = line_[1]

                if "<#" in emote:
                    # custom emote, get it's ID
                    emote = int(emote.replace("<#", "").replace(">", ""))
                result.append((emote, target))
            except Exception:
                # do not send errors if message is in #add-* channel
                if message.channel.id in self.config.get("r2r_channels"):
                    return
                await self._send(
                    message.channel,
                    self.text.fill("invalid_role_line",
                                   line=self.sanitise(line, limit=50)),
                )
                return
        return result

    async def _reaction_payload_to_tuple(
            self, payload: discord.RawMessageUpdateEvent) -> tuple:
        """Return (channel, message, member, emoji) or None"""
        # channel
        channel = self.bot.get_channel(payload.channel_id)
        if not isinstance(channel, discord.TextChannel):
            return
        # message
        try:
            message = await channel.fetch_message(payload.message_id)
        except discord.NotFound:
            return

        # halt if not react-to-role message
        if channel.id not in self.config.get("r2r_channels"):
            return

        # member
        member = message.guild.get_member(payload.user_id)
        if member.bot:
            return
        # emoji
        if payload.emoji.is_custom_emoji():
            emoji = self.bot.get_emoji(payload.emoji.id) or payload.emoji
        else:
            emoji = payload.emoji.name

        return channel, message, member, emoji

    def _get_teacher_channel(
            self, subject: discord.TextChannel) -> discord.TextChannel:
        return discord.utils.get(
            subject.guild.text_channels,
            name=subject.name + config.get("channels", "teacher suffix"),
        )

    ##
    ## Logic
    ##
    async def _subject_add(
        self,
        source: discord.TextChannel,
        member: discord.Member,
        channel: discord.TextChannel,
    ) -> bool:
        # check permission
        for subject_role in self.config.get("subject_roles"):
            if subject_role in [r.id for r in member.roles]:
                break
        else:
            # they do not have neccesary role
            await self._send(
                source, self.text.get("deny_subject", mention=member.mention))
            return False

        await channel.set_permissions(member, view_channel=True)
        teacher_channel = self._get_teacher_channel(channel)
        if teacher_channel is not None:
            await teacher_channel.set_permissions(member, view_channel=True)
        return True

    async def _subject_remove(
        self,
        source: discord.TextChannel,
        member: discord.Member,
        channel: discord.TextChannel,
    ):
        # we do not need to check for permissions
        await channel.set_permissions(member, overwrite=None)
        teacher_channel = self._get_teacher_channel(channel)
        if teacher_channel is not None:
            await teacher_channel.set_permissions(member, overwrite=None)

    async def _role_add(self, channel: discord.TextChannel,
                        member: discord.Member, role: discord.Role) -> bool:
        if role < self.getLimitProgrammes(
                channel) and role > self.getLimitInterests(channel):
            # role is programme, check if user has permission
            for programme_role in self.config.get("programme_roles"):
                if programme_role in [r.id for r in member.roles]:
                    break
            else:
                await self._send(
                    channel,
                    self.text.get("deny_programme", mention=member.mention))
                return False

            # check if user already doesn't have some programme role
            for user_role in member.roles:
                # fmt: off
                if user_role < self.getLimitProgrammes(channel) \
                and user_role > self.getLimitInterests(channel):
                    await self._send(
                        channel,
                        self.text.get("deny_second_programme",
                                      mention=member.mention),
                    )
                    return False
                # fmt: on

        elif role < self.getLimitInterests(channel):
            # role is below interests limit, continue
            pass
        else:
            # role is limit itself or something above programmes
            await self._send(
                channel, self.text.get("deny_high_role",
                                       mention=member.mention))
            return False

        await member.add_roles(role)

        # optionally, hide channel
        if channel.id in self.config.get("r2h_channels"):
            await channel.set_permissions(member, read_messages=False)

        return True

    async def _role_remove(self, channel: discord.TextChannel,
                           member: discord.Member, role: discord.Role):
        if role < self.getLimitProgrammes(
                channel) and role > self.getLimitInterests(channel):
            # role is programme, check if user has permission
            for programme_role in self.config.get("programme_roles"):
                if programme_role in [r.id for r in member.roles]:
                    break
            else:
                await self._send(
                    channel,
                    self.text.get("deny_programme", mention=member.mention))
                return
        elif role < self.getLimitInterests(channel):
            # role is below interests limit, continue
            pass
        else:
            # role is limit itself or something above programmes
            return await self._send(
                channel, self.text.get("deny_high_role",
                                       mention=member.mention))

        await member.remove_roles(role)

    async def _send(self, channel: discord.TextChannel, text: str):
        if channel.id in self.config.get("r2r_channels"):
            return

        await channel.send(text,
                           delete_after=config.get("delay", "user error"))
Exemple #27
0
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("librarian")
        self.text = CogText("librarian")
Exemple #28
0
class Librarian(rubbercog.Rubbercog):
    """Knowledge and information based commands"""

    # TODO Move czech strings to text.default.json

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

        self.config = CogConfig("librarian")
        self.text = CogText("librarian")

    @commands.command(aliases=["svátek"])
    async def svatek(self, ctx):
        url = f"http://svatky.adresa.info/json?date={date.today().strftime('%d%m')}"
        res = await self.fetch_json(url)
        names = []
        for i in res:
            names.append(i["name"])
        await ctx.send(self.text.get("nameday", "cs", name=", ".join(names)))

    @commands.command(aliases=["sviatok"])
    async def meniny(self, ctx):
        url = f"http://svatky.adresa.info/json?lang=sk&date={date.today().strftime('%d%m')}"
        res = await self.fetch_json(url)
        names = []
        for i in res:
            names.append(i["name"])
        await ctx.send(self.text.get("nameday", "sk", name=", ".join(names)))

    @commands.command(aliases=["tyden", "týden", "tyzden", "týždeň"])
    async def week(self, ctx: commands.Context):
        """See if the current week is odd or even"""
        cal_week = date.today().isocalendar()[1]
        stud_week = cal_week - self.config.get("starting_week")
        even, odd = self.text.get("week", "even"), self.text.get("week", "odd")
        cal_type = even if cal_week % 2 == 0 else odd
        stud_type = even if stud_week % 2 == 0 else odd

        embed = self.embed(ctx=ctx)
        embed.add_field(
            name=self.text.get("week", "study"),
            value="{} ({})".format(stud_type, stud_week),
        )
        embed.add_field(
            name=self.text.get("week", "calendar"),
            value="{} ({})".format(cal_type, cal_week),
        )
        await ctx.send(embed=embed)

        await utils.delete(ctx)
        await utils.room_check(ctx)

    @commands.command(aliases=["počasí", "pocasi", "počasie", "pocasie"])
    async def weather(self, ctx, *, place: str = "Brno"):
        token = self.config.get("weather_token")
        place = place[:100]

        if "&" in place:
            return await ctx.send(self.text.get("weather", "place_not_found"))

        url = ("https://api.openweathermap.org/data/2.5/weather?q=" + place +
               "&units=metric&lang=cz&appid=" + token)
        res = await self.fetch_json(url)
        """ Example response
        {
            "coord":{
                "lon":16.61,
                "lat":49.2
            },
            "weather":[
                {
                    "id":800,
                    "temp_maixn":"Clear",
                    "description":"jasno",
                    "icon":"01d"
                }
            ],
            "base":"stations",
            "main":{
                "temp":21.98,
                "feels_like":19.72,
                "temp_min":20.56,
                "temp_max":23,
                "pressure":1013,
                "humidity":53
            },
            "visibility":10000,
            "wind":{
                "speed":4.1,
                "deg":50
            },
            "clouds":{
                "all":0
            },
            "dt":1595529518,
            "sys":{
                "type":1,
                "id":6851,
                "country":"CZ",
                "sunrise":1595474051,
                "sunset":1595529934
            },
            "timezone":7200,
            "id":3078610,
            "name":"Brno",
            "cod":200
        }
        """

        if str(res["cod"]) == "404":
            return await ctx.send(self.text.get("weather", "place_not_found"))
        elif str(res["cod"]) == "401":
            return await ctx.send(self.text.get("weather", "token"))
        elif str(res["cod"]) != "200":
            return await ctx.send(
                self.text.get("weather", "place_error",
                              message=res["message"]))

        title = res["weather"][0]["description"]
        description = self.text.get("weather",
                                    "description",
                                    name=res["name"],
                                    country=res["sys"]["country"])
        if description.endswith("CZ"):
            description = description[:-4]
        embed = self.embed(ctx=ctx,
                           title=title[0].upper() + title[1:],
                           description=description)
        embed.set_thumbnail(url="https://openweathermap.org/img/w/{}.png".
                            format(res["weather"][0]["icon"]))

        embed.add_field(
            name=self.text.get("weather", "temperature"),
            value=self.text.get(
                "weather",
                "temperature_value",
                real=round(res["main"]["temp"], 1),
                feel=round(res["main"]["feels_like"], 1),
            ),
            inline=False,
        )

        embed.add_field(
            name=self.text.get("weather", "humidity"),
            value=str(res["main"]["humidity"]) + " %",
        )
        embed.add_field(
            name=self.text.get("weather", "clouds"),
            value=(str(res["clouds"]["all"]) + " %"),
        )
        if "visibility" in res:
            embed.add_field(
                name=self.text.get("weather", "visibility"),
                value=f"{int(res['visibility']/1000)} km",
            )
        embed.add_field(name=self.text.get("weather", "wind"),
                        value=f"{res['wind']['speed']} m/s")

        await utils.send(ctx, embed=embed)
        await utils.room_check(ctx)

    @commands.command(aliases=["b64"])
    async def base64(self, ctx, direction: str, *, data: str):
        """Get base64 data

        direction: [encode, e, -e; decode, d, -d]
        text: string (under 1000 characters)
        """
        if data is None or not len(data):
            return await utils.send_help(ctx)

        data = data[:1000]
        if direction in ("encode", "e", "-e"):
            direction = "encode"
            result = base64.b64encode(data.encode("utf-8")).decode("utf-8")
        elif direction in ("decode", "d", "-d"):
            direction = "decode"
            try:
                result = base64.b64decode(data.encode("utf-8")).decode("utf-8")
            except Exception as e:
                return await ctx.send(f"> {e}")
        else:
            return await utils.send_help(ctx)

        quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "")
        await ctx.send(f"**base64 {direction}** ({quote}):\n> ```{result}```")

        await utils.room_check(ctx)

    @commands.command()
    async def hashlist(self, ctx):
        """Get list of available hash functions"""
        result = "**hashlib**\n"
        result += "> " + " ".join(sorted(hashlib.algorithms_available))

        await ctx.send(result)

    @commands.command()
    async def hash(self, ctx, fn: str, *, data: str):
        """Get hash function result

        Run hashlist command to see available algorithms
        """
        if fn in hashlib.algorithms_available:
            result = hashlib.new(fn, data.encode("utf-8")).hexdigest()
        else:
            return await ctx.send(self.text.get("invalid_hash"))

        quote = self.sanitise(data[:50]) + ("…" if len(data) > 50 else "")
        await ctx.send(f"**{fn}** ({quote}):\n> ```{result}```")

    async def fetch_json(self, url: str) -> dict:
        """Fetch data from a URL and return a dict"""

        async with aiohttp.ClientSession() as cs:
            async with cs.get(url) as r:
                return await r.json()
Exemple #29
0
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("errors")
Exemple #30
0
class Animals(rubbercog.Rubbercog):
    """Private zone"""
    def __init__(self, bot):
        super().__init__(bot)

        self.config = CogConfig("animals")
        self.text = CogText("animals")

        self.channel = None
        self.role = None
        # Because the API doesn't return the avatar resource immediately,
        # sometimes nothing happens, because the client caches the 404
        # response (?). This is an attempt to counter that.
        self.check_delay = 10

    def getChannel(self):
        if self.channel is None:
            self.channel = self.bot.get_channel(self.config.get("channel"))
        return self.channel

    def getRole(self):
        if self.role is None:
            self.role = self.getChannel().guild.get_role(
                self.config.get("role"))
        return self.role

    ##
    ## Commands
    ##

    @commands.check(acl.check)
    @commands.command()
    async def animal(self, ctx, member: discord.Member):
        """Send vote embed"""
        await self.check(member, "manual")

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_user_update(self, before: discord.User, after: discord.User):
        # only act if user is verified
        member = self.getGuild().get_member(after.id)
        if member is None:
            return

        # only act if user is verified
        if self.getVerifyRole() not in member.roles:
            return

        # only act if user has changed their avatar
        if before.avatar_url == after.avatar_url:
            return

        await asyncio.sleep(self.check_delay)
        await self.check(after, "on_user_update")

    @commands.Cog.listener()
    async def on_member_update(self, before: discord.Member,
                               after: discord.Member):
        # only act if the user has been verified
        verify = self.getVerifyRole()
        if not (verify not in before.roles and verify in after.roles):
            return

        # only act if their avatar is not default
        if after.avatar_url == after.default_avatar_url:
            await self.console.debug(f"{after} verified",
                                     "Not an animal (default avatar).")
            return

        # lookup user timestamp, only allow new verifications
        db_user = repo_u.get(after.id)
        if db_user is not None and db_user.status == "verified":
            db_user = repo_u.get(after.id)
            timestamp = datetime.strptime(db_user.changed, "%Y-%m-%d %H:%M:%S")
            now = datetime.now()
            if (now - timestamp).total_seconds() > 5:
                # this was probably temporary unverify, they have been checked before
                await self.console.debug(f"{after} reverified",
                                         "Skipping (unverify).")
                return

        await asyncio.sleep(self.check_delay)
        await self.check(after, "on_member_update")

    @commands.Cog.listener()
    async def on_raw_reaction_add(self,
                                  payload: discord.RawReactionActionEvent):
        """Vote"""
        if payload.channel_id != self.getChannel().id:
            return

        if payload.member.bot:
            return

        try:
            message = await self.getChannel().fetch_message(payload.message_id)
        except Exception:
            message = None

        if (message is None or len(message.embeds) != 1
                or message.embeds[0].title != self.text.get("title")):
            return

        if str(payload.emoji) not in ("☑️", "❎"):
            return await message.remove_reaction(payload.emoji, payload.member)

        animal_id = int(message.embeds[0].description.split(" | ")[1])
        if animal_id == payload.member.id:
            return await message.remove_reaction(payload.emoji, payload.member)

        try:
            animal = await self.getChannel().guild.fetch_member(animal_id)
        except discord.errors.NotFound:
            animal = None

        if animal is None:
            await self.console.error(
                "animals", f"Could not find member {animal_id}: abort.")
            return await utils.delete(message)

        # delete if the user has changed their avatar since the embed creation
        if str(message.embeds[0].image.url) != str(animal.avatar_url):
            await self.console.debug(
                animal, "Avatar has changed since. Vote aborted.")
            return await utils.delete(message)

        animal_avatar_url = animal.avatar_url_as(format="jpg")
        animal_avatar_data = requests.get(animal_avatar_url)
        animal_avatar = Image.open(BytesIO(animal_avatar_data.content))
        animal_avatar_file = tempfile.TemporaryFile()

        for r in message.reactions:
            if r.emoji == "☑️" and r.count > self.config.get("limit"):
                avatar_result: Image.Image = Animals.add_border(
                    animal_avatar, 3, True)
                avatar_result.save(animal_avatar_file, "png")
                animal_avatar_file.seek(0)
                if self.getRole() in animal.roles:
                    # member is an animal and has been before
                    await self.getChannel().send(
                        self.text.get(
                            "result",
                            "yes_yes",
                            nickname=self.sanitise(animal.display_name),
                        ),
                        file=discord.File(fp=animal_avatar_file,
                                          filename="animal.png"),
                    )
                else:
                    # member is an animal and has not been before
                    try:
                        await animal.add_roles(self.getRole())
                        await self.getChannel().send(
                            self.text.get("result",
                                          "no_yes",
                                          mention=animal.mention),
                            file=discord.File(fp=animal_avatar_file,
                                              filename="animal.png"),
                        )
                    except Exception as e:
                        await self.console.error(message,
                                                 "Could not add animal", e)
                break
            elif r.emoji == "❎" and r.count > self.config.get("limit"):
                avatar_result: Image.Image = Animals.add_border(
                    animal_avatar, 3, False)
                avatar_result.save(animal_avatar_file, "png")
                animal_avatar_file.seek(0)
                if self.getRole() in animal.roles:
                    # member is not an animal and has been before
                    try:
                        await animal.remove_roles(self.getRole())
                        await self.getChannel().send(
                            self.text.get("result",
                                          "yes_no",
                                          mention=animal.mention),
                            file=discord.File(fp=animal_avatar_file,
                                              filename="animal.png"),
                        )
                    except Exception as e:
                        await self.console.error(message,
                                                 "Could not remove animal", e)
                else:
                    # member is not an animal and has not been before
                    await self.getChannel().send(
                        self.text.get("result",
                                      "no_no",
                                      mention=animal.mention),
                        file=discord.File(fp=animal_avatar_file,
                                          filename="animal.png"),
                    )
                break
        else:
            return

        # Edit original message
        result = [0, 0]
        for r in message.reactions:
            if r.emoji == "☑️":
                result[0] = r.count - 1
            elif r.emoji == "❎":
                result[1] = r.count - 1

        await message.edit(
            embed=None,
            content=self.text.get("edit",
                                  nickname=self.sanitise(animal.display_name),
                                  yes=result[0],
                                  no=result[1]),
        )
        try:
            await message.unpin()
        except Exception as e:
            await self.console.error(message,
                                     "Could not unpin Animal vote embed", e)

    ##
    ## Logic
    ##

    async def check(self, member: discord.Member, source: str):
        """Create vote embed"""
        embed = self.embed(
            title=self.text.get("title"),
            description=f"{self.sanitise(str(member))} | {member.id}",
        )
        embed.add_field(
            name=self.text.get("source", source),
            value=self.text.get("required", limit=self.config.get("limit")),
            inline=False,
        )
        embed.set_image(url=member.avatar_url)
        message = await self.getChannel().send(embed=embed)
        await message.add_reaction("☑️")
        await message.add_reaction("❎")

        try:
            await message.pin()
        except Exception as e:
            await self.console.warning(member,
                                       "Could not pin Animal check embed.", e)

        await asyncio.sleep(0.5)
        messages = await message.channel.history(limit=5,
                                                 after=message).flatten()
        for m in messages:
            if m.type == discord.MessageType.pins_add:
                await utils.delete(m)
                break

    @staticmethod
    def add_border(image: Image.Image, border: int,
                   animal: bool) -> Image.Image:
        """Add border to created image.

        image: The avatar.
        border: width of the border.
        animal: whether the avatar is an animal or not.
        """
        image_size = 160
        frame_color = (22, 229, 0, 1) if animal else (221, 56, 31, 1)
        frame = Image.new("RGBA",
                          (image_size + border * 2, image_size + border * 2),
                          frame_color)
        frame = image_utils.round_image(frame)
        avatar = image_utils.round_image(image.resize(
            (image_size, image_size)))
        frame.paste(avatar, (border, border), avatar)
        return frame