Exemplo n.º 1
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"))
Exemplo n.º 2
0
class Warden(rubbercog.Rubbercog):
    """A cog for database lookups"""

    # TODO Implement template matching to prevent false positives

    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

    def doCheckRepost(self, message: discord.Message):
        return (
            message.channel.id in self.config.get("deduplication channels")
            and message.attachments is not None
            and len(message.attachments) > 0
            and not message.author.bot
        )

    @commands.Cog.listener()
    async def on_message(self, message: discord.Message):
        if message.author.bot:
            return

        # repost check - test for duplicates
        if self.doCheckRepost(message):
            if len(message.attachments) > 0:
                await self.checkDuplicate(message)

        # gif check
        for link in self.config.get("penalty strings"):
            if link in message.content:
                penalty = self.config.get("penalty value")
                await message.channel.send(
                    self.text.get("gif warning", mention=message.author, value=penalty)
                )
                repo_k.update_karma_get(message.author, penalty)
                await utils.delete(message)
                await self.console.debug(message, f"Removed message linking to {link}")
                break

    @commands.Cog.listener()
    async def on_message_delete(self, message: discord.Message):
        if self.doCheckRepost(message):
            i = repo_i.deleteByMessage(message.id)
            await self.console.debug(self, f"Removed {i} dhash(es) from database")

            # try to detect repost embed
            messages = await message.channel.history(
                after=message, limit=10, oldest_first=True
            ).flatten()
            for m in messages:
                if not m.author.bot:
                    continue
                if len(m.embeds) != 1 or type(m.embeds[0].footer.text) != str:
                    continue
                if str(message.id) != m.embeds[0].footer.text.split(" | ")[1]:
                    continue

                try:
                    await m.delete()
                except Exception as e:
                    await self.console.error(message, "Could not delete report embed.", e)
                break

    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload: discord.RawReactionActionEvent):
        """Handle 'This is a repost' embed"""
        if payload.channel_id not in self.config.get("deduplication channels"):
            return
        if payload.member.bot:
            return
        if not hasattr(payload.emoji, "name") or payload.emoji.name not in ("🆗", "❎"):
            return

        try:
            embed_message = (
                await self.getGuild()
                .get_channel(payload.channel_id)
                .fetch_message(payload.message_id)
            )
        except Exception as e:
            return await self.console.debug(self, "Reaction's message not found", e)
        if not embed_message or not embed_message.author.bot:
            return

        try:
            repost_message = embed_message.embeds[0].footer.text.split(" | ")[1]
            repost_message = await embed_message.channel.fetch_message(int(repost_message))
        except Exception:
            return await self.console.debug(embed_message, "Could not find repost's original.")

        for r in embed_message.reactions:
            if r.emoji not in ("🆗", "❎"):
                continue

            if (
                payload.emoji.name == "❎"
                and r.emoji == "❎"
                and r.count > self.config.get("not duplicate limit")
            ):
                # remove bot's reactions, it is not a repost
                try:
                    await repost_message.remove_reaction("♻️", self.bot.user)
                    await repost_message.remove_reaction("🤷🏻", self.bot.user)
                    await repost_message.remove_reaction("🤔", self.bot.user)
                except Exception as e:
                    await self.console.error(embed_message, "Could not remove bot's reaction", e)
                    return
                await embed_message.delete()

            elif payload.emoji.name == "🆗" and r.emoji == "🆗":
                # get original author's ID
                repost_author_id = embed_message.embeds[0].footer.text.split(" | ")[0]
                if str(repost_message.author.id) != repost_author_id:
                    return await embed_message.remove_reaction("🆗", embed_message.author)
                # contract the embed
                info_field = embed_message.embeds[0].fields[0]
                embed = discord.Embed()
                embed.add_field(name=info_field.name, value=info_field.value)
                embed.set_footer(text=embed_message.embeds[0].footer.text)
                try:
                    await embed_message.edit(embed=embed)
                    await embed_message.clear_reactions()
                except discord.NotFound:
                    pass

    async def saveMessageHashes(self, message: discord.Message):
        for f in message.attachments:
            if f.size > self.config.get("max_size") * 1024:
                continue

            fp = BytesIO()
            await f.save(fp)
            try:
                i = Image.open(fp)
            except OSError:
                # not an image
                continue
            h = dhash.dhash_int(i)

            # fmt: off
            repo_i.add_image(
                channel_id=message.channel.id,
                message_id=message.id,
                attachment_id=f.id,
                dhash=str(hex(h)),
            )
            # fmt: on
            yield h

    @commands.group()
    @commands.check(acl.check)
    async def scan(self, ctx):
        """Scan for reposts"""
        if ctx.invoked_subcommand is None:
            await ctx.send_help(ctx.invoked_with)

    @commands.check(acl.check)
    @commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
    @commands.bot_has_permissions(read_message_history=True)
    @scan.command(name="history")
    async def scan_history(self, ctx, limit):
        """Scan current channel for images and save them as hashes

        limit: [all | <int>]
        """
        # parse parameter
        if limit == "all":
            limit = None
        else:
            try:
                limit = int(limit)
                if limit < 1:
                    raise ValueError
            except ValueError:
                raise commands.BadArgument("Expected 'all' or positive integer")

        messages = await ctx.channel.history(limit=limit).flatten()

        # fmt: off
        title = "**INITIATING...**\n\nLoaded {} messages"
        await asyncio.sleep(0.5)
        template = (
            "**SCANNING IN PROGRESS**\n\n"
            "Processed **{}** of **{}** messages ({:.1f} %)\n"
            "Computed **{}** hashes"
        )
        # fmt: on
        msg = await ctx.send(title.format(len(messages)))

        ctr_nofile = 0
        ctr_hashes = 0
        i = 0
        now = time.time()
        for i, message in enumerate(messages):
            # update info on every 10th message
            if i % 20 == 0:
                # fmt: off
                await msg.edit(content=template.format(
                    i, len(messages), (i / len(messages) * 100),
                    ctr_hashes
                ))
                # fmt: on

            if len(message.attachments) == 0:
                ctr_nofile += 1
                continue

            hashes = [x async for x in self.saveMessageHashes(message)]
            ctr_hashes += len(hashes)

        # fmt: off
        await msg.edit(content=
            "**SCAN COMPLETE**\n\n"
            f"Processed **{len(messages)}** messages.\n"
            f"Computed **{ctr_hashes}** hashes in {(time.time() - now):.1f} seconds."
        )
        # fmt: on

    @commands.check(acl.check)
    @scan.command(name="compare", aliases=["messages"])
    async def scan_compare(self, ctx, first: int, second: int):
        """Scan two messages and report comparison result

        Arguments
        ---------
        first: Message ID
        second: Message ID
        """
        hashes1 = repo_i.get_by_message(first)
        hashes2 = repo_i.get_by_message(second)

        if len(hashes1) == 0:
            return await ctx.send(self.text.get("comparison", "not_found", message_id=str(first)))
        if len(hashes2) == 0:
            return await ctx.send(self.text.get("comparison", "not_found", message_id=str(second)))

        text = []
        text.append(self.text.get("comparison", "header", message_id=str(first)))
        for h in hashes1:
            text.append(self.text.get("comparison", "line", hash=str(h.dhash)[2:]))
        text.append("")
        text.append(self.text.get("comparison", "header", message_id=str(second)))
        for h in hashes2:
            text.append(self.text.get("comparison", "line", hash=str(h.dhash)))

        if len(hashes1) == 1 or len(hashes2) == 1:
            hash1 = int(hashes1[0].dhash, 16)
            hash2 = int(hashes2[0].dhash, 16)

            hamming = dhash.get_num_bits_different(hash1, hash2)
            prob = "{:.1f}".format((1 - hamming / 128) * 100)

            text.append("")
            text.append(self.text.get("comparison", "footer", percent=str(prob), bits=str(hamming)))

        await ctx.send("\n".join(text))

    async def checkDuplicate(self, message: discord.Message):
        """Check if uploaded files are known"""
        hashes = [x async for x in self.saveMessageHashes(message)]

        if len(message.attachments) > 0 and len(hashes) == 0:
            await message.add_reaction("▶")
            await asyncio.sleep(2)
            await message.remove_reaction("▶", self.bot.user)

        duplicates = {}
        posts_all = None
        for image_hash in hashes:
            # try to look up hash directly
            posts_full = repo_i.getHash(str(hex(image_hash)))

            if len(posts_full) > 0:
                # full match found
                for post in posts_full:
                    # skip current message
                    if post.message_id == message.id:
                        continue
                    # add to duplicates
                    duplicates[post] = 0
                    await self.console.debug(message, "Full dhash match")
                    break

                # move on to the next hash
                continue

            # full match not found, iterate over whole database
            if posts_all is None:
                posts_all = repo_i.getAll()

            hamming_min = 128
            duplicate = None
            for post in posts_all:
                # skip current message
                if post.message_id == message.id:
                    continue
                # do the comparison
                post_hash = int(post.dhash, 16)
                hamming = dhash.get_num_bits_different(image_hash, post_hash)
                if hamming < hamming_min:
                    duplicate = post
                    hamming_min = hamming

            duplicates[duplicate] = hamming_min

            await self.console.debug(message, f"Closest Hamming distance: {hamming_min}/128 bits")

        for image_hash, hamming_distance in duplicates.items():
            if hamming_distance <= self.limit_soft:
                await self._announceDuplicate(message, image_hash, hamming_distance)

    async def _announceDuplicate(self, message: discord.Message, original: object, hamming: int):
        """Send message that a post is a original

        original: object
        hamming: Hamming distance between the image and closest database entry
        """
        if hamming <= self.limit_full:
            t = "**♻️ To je repost!**"
            await message.add_reaction("♻️")
        elif hamming <= self.limit_hard:
            t = "**♻️ To je asi repost**"
            await message.add_reaction("🤔")
        else:
            t = "To je možná repost"
            await message.add_reaction("🤷🏻")

        prob = "{:.1f} %".format((1 - hamming / 128) * 100)
        timestamp = utils.id_to_datetime(original.attachment_id).strftime("%Y-%m-%d %H:%M:%S")

        src_chan = self.getGuild().get_channel(original.channel_id)
        try:
            src_post = await src_chan.fetch_message(original.message_id)
            link = src_post.jump_url
            author = discord.utils.escape_markdown(src_post.author.display_name)
        except discord.errors.NotFound:
            link = "404 " + emote.sad
            author = "_??? (404)_"

        d = self.text.get(
            "repost description",
            name=discord.utils.escape_markdown(message.author.display_name),
            value=prob,
        )
        embed = discord.Embed(title=t, color=config.color, description=d, url=message.jump_url)
        embed.add_field(name=f"**{author}**, {timestamp}", value=link, inline=False)

        embed.add_field(
            name=self.text.get("repost title"),
            value="_"
            + self.text.get("repost content", limit=self.config.get("not duplicate limit"))
            + "_",
        )
        embed.set_footer(text=f"{message.author.id} | {message.id}")
        m = await message.channel.send(embed=embed)
        await m.add_reaction("❎")
        await m.add_reaction("🆗")
Exemplo n.º 3
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)
Exemplo n.º 4
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)
Exemplo n.º 5
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
Exemplo n.º 6
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
Exemplo n.º 7
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"))
Exemplo n.º 8
0
class Anonsend(rubbercog.Rubbercog):
    """Send files anonymously"""
    def __init__(self, bot):
        super().__init__(bot)

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

    @commands.group()
    async def anonsend(self, ctx):
        """Send an image anonymously.

        Run `anonsend link` to get the URL to the website.
        """
        await utils.send_help(ctx)

    @commands.guild_only()
    @commands.check(acl.check)
    @anonsend.command(name="add")
    async def anonsend_add(self, ctx, name: str):
        """Add channel used for anonymous posts"""
        try:
            repo_a.add(guild_id=ctx.guild.id,
                       channel_id=ctx.channel.id,
                       name=name)
        except ValueError:
            return await ctx.send(self.text.get("name_exists"))

        await self.event.sudo(
            ctx, f"anonsend channel created: `{self.sanitise(name)}`.")
        await ctx.send(self.text.get("added"))

    @commands.check(acl.check)
    @anonsend.command(name="remove")
    async def anonsend_remove(self, ctx, name: str):
        """Remove channel used for anonymous posts"""
        try:
            repo_a.remove(name=name)
        except ValueError:
            return await ctx.send(self.text.get("bad_name"))

        await self.event.sudo(
            ctx, f"anonsend channel removed: `{self.sanitise(name)}`.")
        await ctx.send(self.text.get("removed"))

    @commands.check(acl.check)
    @anonsend.command(name="rename")
    async def anonsend_rename(self, ctx, old_name: str, new_name: str):
        """Rename anonymous post channel"""
        try:
            repo_a.rename(old_name=str, new_name=str)
        except ValueError as e:
            return await ctx.send("`" + str(e) + "`")

        await self.event.sudo(
            ctx,
            ("anonsend channel renamed: "
             f"`{self.sanitise(old_name)}` to "
             f"`{self.sanitise(new_name)}`."),
        )
        await ctx.send(self.text.get("renamed"))

    @commands.guild_only()
    @commands.check(acl.check)
    @anonsend.command(name="list")
    async def anonsend_list(self, ctx):
        """Get mappings for current guild"""
        channels = repo_a.get_all(ctx.guild.id)
        await ctx.send("```\n" + "\n".join([str(x)
                                            for x in channels]) + "\n```")

    @commands.check(acl.check)
    @anonsend.command(name="fetch")
    async def anonsend_fetch(self, ctx):
        """Get list of pending files"""
        url_base = self.config.get(
            "url") + "/api.php?apikey=" + self.config.get("apikey")
        response = requests.get(url_base + "&action=list")
        files = response.json()

        embed = self.embed(ctx=ctx, title=self.text.get("fetch_server"))
        for i, (file, timestamp) in enumerate(files.items()):
            uploaded = datetime.datetime.fromtimestamp(timestamp)
            embed.add_field(
                name=file,
                value=uploaded.strftime("%Y-%m-%d %H:%M:%S") + "\n" +
                self.text.get(
                    "fetch_age",
                    time=utils.seconds2str(
                        (datetime.datetime.now() - uploaded).seconds),
                ),
            )
            if i % 24 == 0 and i > 0:
                await ctx.send(embed=embed)
        await ctx.send(embed=embed)

    @commands.check(acl.check)
    @anonsend.command(name="link", aliases=["url"])
    async def anonsend_link(self, ctx):
        """Get link to the anonsend upload website"""
        await ctx.send(self.config.get("url"))

    @commands.dm_only()
    @anonsend.command(name="submit")
    async def anonsend_submit(self, ctx, name: str, filename: str):
        """Send a file"""

        # get channel
        target = repo_a.get(name=name)
        if target is None:
            return await ctx.send(self.text.get("no_channel"))

        channel = self.bot.get_channel(target.channel_id)
        if channel is None:
            return await ctx.send(self.text.get("channel_not_found"))

        guild = self.bot.get_guild(target.guild_id)
        guild_user = guild.get_member(ctx.author.id)
        if guild_user is None:
            return await ctx.send(self.text.get("not_in_guild"))

        url_base = self.config.get(
            "url") + "/api.php?apikey=" + self.config.get("apikey")
        url_base += "&file=" + filename

        # feedback
        message = await ctx.send(self.text.get("downloading"))

        # download image
        response = requests.get(url_base + "&action=download")
        if response.status_code == 404:
            return await ctx.send(self.text.get("bad_filename"))
        if response.status_code != 200:
            await ctx.send(
                self.text.get("download_error", message=response.content))
            await self.event.error(ctx, "anonsend error: " + response.content)
            return

        image_binary = tempfile.TemporaryFile()
        image_binary.write(response.content)
        image_binary.seek(0)

        # feedback
        await message.edit(content=message.content + " " +
                           self.text.get("uploading"))

        # send it
        await channel.send(file=discord.File(
            fp=image_binary,
            filename=filename,
        ))
        feedback = requests.get(url_base + "&action=delete")
        if feedback.status_code != 200:
            await self.event.error(
                ctx, "delete error: " + str(feedback.status_code))

        image_binary.close()

        # feedback
        await message.edit(content=message.content + " " +
                           self.text.get("done"))

        # increment log
        repo_a.increment(name)
        await self.event.user(
            "DMChannel", f"Anonymous image sent to **{self.sanitise(name)}**.")
Exemplo n.º 9
0
class Sync(rubbercog.Rubbercog):
    """Guild synchronisation"""
    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

    def get_slave_guild(self):
        if self.slave_guild is None:
            self.slave_guild = self.bot.get_guild(self.slave_guild_id)
        return self.slave_guild

    def get_slave_verify(self) -> discord.Role:
        if self.slave_verify is None:
            self.slave_verify = self.get_slave_guild().get_role(
                self.slave_verify_id)
        return self.slave_verify

    def get_master_member(self, user_id: int) -> discord.Member:
        return self.getGuild().get_member(user_id)

    def get_slave_member(self, user_id: int) -> discord.Member:
        return self.get_slave_guild().get_member(user_id)

    def get_slave_role(self, master_role_id: int) -> discord.Role:
        key = str(master_role_id)
        if key not in self.mapping_ids:
            return None

        if key not in self.mapping.keys():
            self.mapping[key] = self.get_slave_guild().get_role(
                self.mapping_ids[key])
        return self.mapping[key]

    ##
    ## Commands
    ##

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_member_update(self, before: discord.Member,
                               after: discord.Member):
        """Add member to slave guild if they add one of specified roles"""
        if before.roles == after.roles:
            return

        # roles are guild-specific, we do not need to check the guild ID
        before_ids = [role.id for role in before.roles]
        after_ids = [role.id for role in after.roles]

        for engineer_id in self.engineer_ids:
            if engineer_id not in before_ids and engineer_id in after_ids:
                await self.verify_member(after)
                return

            if engineer_id in before_ids and engineer_id not in after_ids:
                await self.unverify_member(after)
                return

    @commands.Cog.listener()
    async def on_member_join(self, member: discord.Member):
        """Verify user if they join slave server while having specified role on master"""
        if member.guild.id != self.slave_guild_id:
            return

        master_member = self.get_master_member(member.id)
        master_member_roles = [role.id for role in master_member.roles]

        for engineer_id in self.engineer_ids:
            if engineer_id in master_member_roles:
                await self.verify_member(master_member)
                return

    ##
    ## Logic
    ##

    async def verify_member(self, member: discord.Member):
        # get member object on slave guild
        slave_member = self.get_slave_member(member.id)
        if slave_member is not None:
            # map some of their roles to slave ones
            to_add = [self.get_slave_verify()]
            event_roles = []

            for role in member.roles:
                if str(role.id) in self.mapping_ids.keys():
                    mapped = self.get_slave_role(role.id)
                    to_add.append(mapped)
                    event_roles.append(mapped.name)
            roles = ", ".join(f"**{self.sanitise(name)}**"
                              for name in event_roles)

            # add the roles
            await slave_member.add_roles(*to_add, reason="Sync: verify")
            await self.event.user(slave_member,
                                  f"Verified on slave server with {roles}.")
        else:
            # send invitation
            await member.send(
                self.text.get("invite",
                              invite_link=self.config.get("slave_invite_link"))
            )
            await self.event.user(member, "Not on slave server: invite sent.")

    async def unverify_member(self, member: discord.Member):
        # get member object on slave guild
        slave_member = self.get_slave_member(member.id)
        if slave_member is not None:
            roles = slave_member.roles[1:]  # the first is @everyone
            await slave_member.remove_roles(*roles, reason="Sync: unverify")
            await self.event.user(slave_member, "Unverified on slave server.")
        else:
            await self.event.user(member,
                                  "Not on slave server: skipping unverify.")
Exemplo n.º 10
0
class Actress(rubbercog.Rubbercog):
    """Be a human"""
    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 = {}

    ##
    ## Commands
    ##
    @commands.check(acl.check)
    @commands.group(name="send")
    async def send(self, ctx):
        """Send message to given channel"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @send.command(name="text")
    async def send_text(self, ctx, channel: discord.TextChannel, *,
                        content: str):
        """Send a text to text channel

        channel: Target text channel
        content: Text
        """
        message = await channel.send(content)
        await self.event.sudo(
            ctx,
            f"Text sent to {channel.mention}:\n<{message.jump_url}>\n>>> _{content}_",
        )
        await self.output.info(ctx, self.text.get("send_text"))

    @commands.check(acl.check)
    @send.command(name="dm", aliases=["text-dm"])
    async def send_dm(self, ctx, user: discord.User, *, content: str):
        """Send a DM to a user"""
        try:
            message = await user.send(content)
        except discord.Forbidden:
            return await ctx.send(self.text.get("dm_forbidden"))

        await self.event.sudo(ctx, f"DM sent to {user}:\n>>> _{content}_")
        await self.output.info(ctx, self.text.get("send_text"))

    @commands.check(acl.check)
    @send.command(name="image", aliases=["file"])
    async def send_image(self, ctx, channel: discord.TextChannel, filename):
        """Send an image as a bot

        channel: Target text channel
        filename: A filename
        """
        now = time.monotonic()
        try:
            async with ctx.typing():
                message = await channel.send(file=discord.File(self.path +
                                                               filename))
                delta = str(int(time.monotonic() - now))
                await self.output.info(ctx,
                                       self.text.get("send_file", delta=delta))

                await self.event.sudo(
                    ctx,
                    f"Media file sent to {channel.mention}:\n"
                    f"> _{filename}_\n> <{message.jump_url}>",
                )
        except Exception as e:
            await self.output.error(ctx, self.text.get("FileSendError"), e)

    @commands.check(acl.check)
    @commands.group(name="react", aliases=["reaction", "reactions"])
    async def react(self, ctx):
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @react.command(name="overview")
    async def react_overview(self, ctx):
        """List registered reactions"""
        embed = self.embed(ctx=ctx,
                           description=self.text.get("embed",
                                                     "total",
                                                     count=len(
                                                         self.reactions)))
        value = []
        for name in self.reactions.keys():
            if len(value) + len(name) > 1024:
                embed.add_field(name="\u200b",
                                value="\n".join(f"`{v}`" for v in value),
                                inline=False)
                value = []
            value.append(name)

        embed.add_field(name="\u200b",
                        value="\n".join(f"`{v}`" for v in value),
                        inline=False)
        await ctx.send(embed=embed)

    @commands.check(acl.check)
    @react.command(name="list")
    async def react_list(self, ctx):
        """See details for reactions"""
        try:
            name = next(iter(self.reactions))
            reaction = self.reactions[name]
            embed = self.embed(ctx=ctx, page=(1, len(self.reactions)))
        except StopIteration:
            reaction = None
            embed = self.embed(ctx=ctx)

        if reaction is not None:
            embed = self.fill_reaction_embed(embed, name, reaction)

        message = await ctx.send(embed=embed)

        if len(self.reactions) > 1:
            await message.add_reaction("◀")
            await message.add_reaction("▶")

    @commands.check(acl.check)
    @react.command(name="usage", aliases=["stat", "stats"])
    async def react_usage(self, ctx):
        """See reactions usage since start"""
        items = {
            k: v
            for k, v in sorted(
                self.usage.items(), key=lambda item: item[1], reverse=True)
        }

        embed = self.embed(ctx=ctx)
        content = []
        total = 0
        template = "`{count:>2}` … **{reaction}**"
        for reaction, count in items.items():
            content.append(template.format(count=count, reaction=reaction))
            total += count
        if len(content) == 0:
            content.append(self.text.get("embed", "nothing"))

        embed.add_field(name=self.text.get("embed", "total", count=total),
                        value="\n".join(content))
        await ctx.send(embed=embed, delete_after=config.delay_embed)

        await utils.delete(ctx)

    @commands.check(acl.check)
    @react.command(name="add")
    async def react_add(self, ctx, name: str = None, *, parameters=None):
        """Add new reaction

        ```
        ?react add <reaction name>
        type <image | text>
        match <full | start | end | any>
        sensitive <true | false>
        triggers "a b c" "d e" f
        responses "abc def"
        enabled <true | false>

        users 0 1 2
        channels 0 1 2
        counter 10
        ```
        """
        if name is None:
            return await utils.send_help(ctx)
        elif name in self.reactions.keys():
            raise ReactionNameExists()

        reaction = await self.parse_react_message(ctx.message, strict=True)
        self.reactions[name] = reaction
        self._save_reactions()

        await self.output.info(ctx, self.text.get("reaction_add", name=name))
        await self.event.sudo(ctx, f"Reaction **{name}** added.")

    @commands.check(acl.check)
    @react.command(name="edit")
    async def react_edit(self, ctx, name: str = None, *, parameters=None):
        """Edit reaction

        ```
        ?react edit <reaction name>
        type <image | text>
        match <full | start | end | any>
        sensitive <true | false>
        triggers "a b c" "d e" f
        responses "abc def"
        enabled <true | false>

        users 0 1 2
        channels 0 1 2
        counter 10
        ```
        """
        if name is None:
            return await utils.send_help(ctx)
        elif name not in self.reactions.keys():
            raise ReactionNotFound()

        new_reaction = await self.parse_react_message(ctx.message,
                                                      strict=False)
        reaction = self.reactions[name]

        for key, value in new_reaction.items():
            reaction[key] = new_reaction[key]

        self.reactions[name] = reaction
        self._save_reactions()

        await self.output.info(ctx, self.text.get("reaction_edit", name=name))
        await self.event.sudo(ctx, f"Reaction **{name}** updated.")

    @commands.check(acl.check)
    @react.command(name="remove")
    async def react_remove(self, ctx, name: str = None):
        """Remove reaction"""
        if name is None:
            return await utils.send_help(ctx)
        elif name not in self.reactions.keys():
            raise ReactionNotFound()

        del self.reactions[name]
        self._save_reactions()

        await self.output.info(ctx, self.text.get("reaction_remove",
                                                  name=name))
        await self.event.sudo(ctx, f"Reaction **{name}** removed.")

    @commands.check(acl.check)
    @commands.group(name="image", aliases=["img", "images"])
    async def image(self, ctx):
        """Manage images available to the bot"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @image.command(name="list")
    async def image_list(self, ctx):
        """List available commands"""
        files = os.listdir(self.path)
        template = "`{size:>5} kB` … {filename}"
        content = []
        for file in files:
            if file.split(".")[-1] not in self.supported_formats:
                continue
            size = int(os.path.getsize(self.path + file) / 1024)
            content.append(template.format(size=size, filename=file))
        if len(content) == 0:
            content.append(self.text.get("image", "no_files"))

        embed = self.embed(ctx=ctx)
        embed.add_field(name="\u200b", value="\n".join(content))
        await utils.send(ctx, embed=embed)

        await utils.delete(ctx)

    @commands.check(acl.check)
    @image.command(name="download", aliases=["dl"])
    async def image_download(self, ctx, url: str, filename: str):
        """Download new image

        url: URL of the image
        filename: Target filename
        """
        if filename.split(".")[-1] not in self.supported_formats:
            return self.output.error(ctx, self.text.get("bad_extension"))
        if "/" in filename or "\\" in filename or ".." in filename:
            return self.output.error(ctx, self.text.get("bad_character"))

        with open(self.path + filename, "wb") as f:
            response = get(url)
            f.write(response.content)

        await self.output.info(ctx, self.text.get("image", "downloaded"))

        await utils.delete(ctx)

    @commands.check(acl.check)
    @image.command(name="remove", aliases=["delete", "rm", "del"])
    async def image_remove(self, ctx, filename: str):
        """Remove image

        filename: An image filename
        """
        if "/" in filename or "\\" in filename or ".." in filename:
            return self.output.error(ctx,
                                     self.text.get("image", "bad_character"))

        os.remove(self.path + filename)
        await self.output.info(self.text.get("image", "deleted"))

        await utils.delete(ctx)

    @commands.check(acl.check)
    @image.command(name="show")
    async def image_show(self, ctx, filename: str):
        """Show an image

        filename: An image filename
        """
        await self.send_image(ctx, ctx.channel, filename)

        await utils.delete(ctx)

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_message(self, message):
        if message.author.bot:
            return

        # fmt: off
        if hasattr(message.channel, "id") \
        and message.channel.id in self.config.get("ignored_channels"):
            return
        # fmt: on

        for name, reaction in self.reactions.items():
            # test
            if not self._reaction_matches(message, reaction):
                continue

            # send
            response = random.choice(reaction["responses"])
            if reaction["type"] == "text":
                response = response.replace("((mention))",
                                            message.author.mention)
                response = response.replace(
                    "((name))", self.sanitise(message.author.display_name))
                await message.channel.send(response)
            elif reaction["type"] == "image":
                await message.channel.send(file=discord.File(self.path +
                                                             response))

            # log
            if name in self.usage:
                self.usage[name] += 1
            else:
                self.usage[name] = 1

            # counter
            if "counter" in reaction:
                if reaction["counter"] > 1:
                    self.reactions[name]["counter"] -= 1
                else:
                    # last usage, disable
                    del self.reactions[name]["counter"]
                    self.reactions[name]["enabled"] = False
                    await self.event.user(message,
                                          "Reaction disabled: **{name}**.")
                self._save_reactions()

            break

    @commands.Cog.listener()
    async def on_reaction_add(self, reaction: discord.Reaction,
                              user: discord.User):
        # do we care?
        if (user.bot or len(reaction.message.embeds) != 1
                or reaction.message.embeds[0].title !=
                f"{config.prefix}react list"):
            return

        if hasattr(reaction, "emoji"):
            if str(reaction.emoji) == "◀":
                page_delta = -1
            elif str(reaction.emoji) == "▶":
                page_delta = 1
            else:
                # invalid reaction
                return await utils.remove_reaction(reaction, user)
        else:
            # invalid reaction
            return await utils.remove_reaction(reaction, user)

        embed = reaction.message.embeds[0]
        if embed.footer == discord.Embed.Empty or " | " not in embed.footer.text:
            return await utils.remove_reaction(reaction, user)

        # allow only the author
        if embed.footer.text.split(" | ")[0] != str(user):
            return await utils.remove_reaction(reaction, user)

        # get page
        footer_text = embed.footer.text
        pages = footer_text.split(" | ")[-1]
        page_current = int(pages.split("/")[0]) - 1

        page = (page_current + page_delta) % len(self.reactions)
        footer_text = footer_text.replace(pages,
                                          f"{page+1}/{len(self.reactions)}")

        # update embed
        bot_reaction_name = list(self.reactions.keys())[page]
        bot_reaction = self.reactions[bot_reaction_name]

        embed = self.fill_reaction_embed(embed, bot_reaction_name,
                                         bot_reaction)
        embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url)
        await reaction.message.edit(embed=embed)

        await utils.remove_reaction(reaction, user)

    ##
    ## Helper functions
    ##
    def _save_reactions(self):
        with open(self.path + "reactions.hjson", "w", encoding="utf-8") as f:
            hjson.dump(self.reactions, f, ensure_ascii=False, indent="\t")

    async def _remove_reaction(self, reaction, user):
        try:
            await reaction.remove(user)
        except:
            pass

    def _reaction_matches(self, message, reaction) -> bool:
        # check if it is enabled
        if not reaction["enabled"]:
            return False

        # normalise
        if reaction["sensitive"]:
            text = message.content
            triggers = reaction["triggers"]
        else:
            text = message.content.lower()
            triggers = [x.lower() for x in reaction["triggers"]]

        # check the type
        if reaction["match"] == "full" and text not in triggers:
            return False
        if reaction["match"] == "any":
            for trigger in triggers:
                if trigger in text:
                    break
            else:
                return False
        if reaction["match"] == "start":
            for trigger in triggers:
                if text.startswith(trigger):
                    break
            else:
                return False
        if reaction["match"] == "end":
            for trigger in triggers:
                if text.endswith(trigger):
                    break
            else:
                return False

        # conditions
        if "users" in reaction and message.author.id not in reaction["users"]:
            return False
        if "channels" in reaction and message.channel.id not in reaction[
                "channels"]:
            return False

        return True

    ##
    ## Logic
    ##
    async def parse_react_message(self, message: discord.Message,
                                  strict: bool) -> dict:
        content = message.content.replace("`", "").split("\n")[1:]
        result = {}

        # fill values
        for line in content:
            line = line.split(" ", 1)
            key = line[0]
            value = line[1]

            if key not in (
                    "type",
                    "match",
                    "sensitive",
                    "triggers",
                    "responses",
                    "enabled",
                    "users",
                    "channels",
                    "counter",
            ):
                raise InvalidReactionKey(key=key)

            # check
            invalid = False
            # fmt: off
            if key == "type" and value not in ("text", "image") \
            or key == "match" and value not in ("full", "start", "end", "any") \
            or key == "sensitive" and value not in ("true", "false") \
            or key == "enabled" and value not in ("true", "false") \
            or key == "triggers" and len(value) < 1 \
            or key == "responses" and len(value) < 1:
                invalid = True

            # fmt: on
            if invalid:
                raise ReactionParsingException(key, value)

            # parse
            if key in ("sensitive", "enabled"):
                value = value == "true"
            elif key in ("triggers", "responses"):
                # convert to list
                value = shlex.split(value)
            elif key in ("users", "channels"):
                # convert to list of ints
                try:
                    value = [int(x) for x in shlex.split(value)]
                except:
                    raise ReactionParsingException(key, value)
            elif key == "counter":
                try:
                    value = int(value)
                except:
                    raise ReactionParsingException(key, value)

            result[key] = value

        if strict:
            # check if all required values are present
            for key in ("type", "match", "triggers", "response"):
                if key is None:
                    raise discord.MissingRequiredArgument(param=key)

        return result

    def fill_reaction_embed(self, embed: discord.Embed, name: str,
                            reaction: dict) -> discord.Embed:
        # reset any previous
        embed.clear_fields()

        embed.add_field(name="name", value=f"**{name}**")
        for key in ("triggers", "responses"):
            value = "\n".join(reaction[key])
            embed.add_field(name=key, value=value, inline=False)
        for key in ("type", "match", "sensitive", "enabled"):
            embed.add_field(name=key, value=reaction[key])
        if "users" in reaction.keys() and reaction["users"] is not None:
            users = [self.bot.get_user(x) for x in reaction["users"]]
            value = "\n".join(
                f"`{user.id}` {user.name if hasattr(user, 'name') else '_unknown_'}"
                for user in users)
            embed.add_field(name="users", value=value, inline=False)
        if "channels" in reaction.keys() and reaction["channels"] is not None:
            channels = [self.bot.get_channel(x) for x in reaction["channels"]]
            value = "\n".join(f"`{channel.id}` {channel.mention}"
                              for channel in channels)
            embed.add_field(name="channels", value=value, inline=False)
        if "counter" in reaction.keys() and reaction["counter"] is not None:
            embed.add_field(name="counter", value=str(reaction["counter"]))

        return embed

    ##
    ## 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, InvalidReactionKey):
            await self.output.error(
                ctx, self.text.get("InvalidReactionKey", key=error.key))
        elif isinstance(error, ReactionParsingException):
            await self.output.error(
                ctx,
                self.text.get("ReactionParsingException",
                              key=error.key,
                              value=error.value))
        # exceptions without parameters
        elif isinstance(error, ActressException):
            await self.output.error(ctx, self.text.get(type(error).__name__))
Exemplo n.º 11
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.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))
        if option is not None:
            await ctx.reply(option)

        await utils.room_check(ctx)

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

    @commands.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @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.reply(option)
        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel)
    @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.reply(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 Exception:
            image_url = discord.Embed.Empty

        footer = "picsum.photos"
        if seed:
            footer += f" ({seed})"
        embed = self.embed(ctx=ctx, title=discord.Embed.Empty, description=image_url, footer=footer)
        embed.set_image(url=image.url)
        await ctx.reply(embed=embed)

        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel)
    @commands.command()
    async def cat(self, ctx):
        """Get random image of a cat"""
        data = requests.get("https://api.thecatapi.com/v1/images/search")
        if data.status_code != 200:
            return await ctx.reply(f"E{data.status_code}")

        embed = self.embed(ctx=ctx, title=discord.Embed.Empty, footer="thecatapi.com")
        embed.set_image(url=data.json()[0]["url"])
        await ctx.reply(embed=embed)

        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=20, type=commands.BucketType.channel)
    @commands.command()
    async def dog(self, ctx):
        """Get random image of a dog"""
        data = requests.get("https://api.thedogapi.com/v1/images/search")
        if data.status_code != 200:
            return await ctx.reply(f"E{data.status_code}")

        embed = self.embed(ctx=ctx, title=discord.Embed.Empty, footer="thedogapi.com")
        embed.set_image(url=data.json()[0]["url"])
        await ctx.reply(embed=embed)

        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=60, type=commands.BucketType.channel)
    @commands.command()
    async def xkcd(self, ctx, number: int = None):
        """Get random xkcd comics

        Arguments
        ---------
        number: Comics number. Omit to get random one.
        """
        # get maximal
        fetched = await utils.fetch_json("https://xkcd.com/info.0.json")
        # get random
        if number is None or number < 1 or number > fetched["num"]:
            number = random.randint(1, fetched["num"])
        # fetch requested
        if number != fetched["num"]:
            fetched = await utils.fetch_json(f"https://xkcd.com/{number}/info.0.json")

        embed = self.embed(
            ctx=ctx,
            title=fetched["title"],
            description="_" + fetched["alt"][:2046] + "_",
            footer="xkcd.com",
        )
        embed.add_field(
            name=(
                f"{fetched['year']}"
                f"-{str(fetched['month']).zfill(2)}"
                f"-{str(fetched['day']).zfill(2)}"
            ),
            value=(
                f"https://xkcd.com/{number}\n"
                + f"https://www.explainxkcd.com/wiki/index.php/{number}"
            ),
            inline=False,
        )
        embed.set_image(url=fetched["img"])
        await ctx.reply(embed=embed)

        await utils.room_check(ctx)

    @commands.cooldown(rate=5, per=60, type=commands.BucketType.channel)
    @commands.command()
    async def dadjoke(self, ctx, *, keyword: str = None):
        """Get random dad joke

        Arguments
        ---------
        keyword: search for a certain keyword in a joke
        """
        if keyword is not None and ("&" in keyword or "?" in keyword):
            await ctx.reply(self.text.get("joke_notfound"))
            return await utils.room_check(ctx)

        param = {"limit": "30"}
        url = "https://icanhazdadjoke.com"
        if keyword != None:
            param["term"] = keyword
            url += "/search"

        fetched = requests.get(url, headers={"Accept": "application/json"}, params=param)

        if keyword != None:
            res = fetched.json()["results"]
            if len(res) == 0:
                await ctx.reply(self.text.get("joke_notfound"))
                return await utils.room_check(ctx)
            result = random.choice(res)
        else:
            result = fetched.json()

        embed = self.embed(
            ctx=ctx,
            description=result["joke"],
            footer="icanhazdadjoke.com",
            url="https://icanhazdadjoke.com/j/" + result["id"],
        )
        await ctx.reply(embed=embed)
        await utils.room_check(ctx)
Exemplo n.º 12
0
class Janitor(rubbercog.Rubbercog):
    """Manage users, roles and channels"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("janitor")

    @commands.cooldown(rate=2, per=20.0, type=commands.BucketType.user)
    @commands.check(acl.check)
    @commands.command()
    async def hoarders(self, ctx: commands.Context, warn: str = None):
        """Check for users with multiple programme roles

        warn: Optional. Use "warn" string to send warnings, else just list the users
        """
        warn = warn == "warn"

        hoarders = []
        limit_top = discord.utils.get(self.getGuild().roles,
                                      name="---PROGRAMMES")
        limit_bottom = discord.utils.get(self.getGuild().roles,
                                         name="---INTERESTS")

        for member in self.getGuild().members:
            prog = []
            for role in member.roles:
                if role < limit_top and role > limit_bottom:
                    prog.append(role.name)
            if len(prog) > 1:
                hoarders.append([member, prog])

        if len(hoarders) == 0:
            return await ctx.send(self.text.get("no_hoarders"))

        embed = self.embed(ctx=ctx, title=self.text.get("embed", "title"))
        if warn:
            msg = await ctx.send(
                self.text.get("sending", num=1, all=len(hoarders)))

        for num, (hoarder, progs) in enumerate(hoarders):
            # fmt: off
            embed.add_field(
                name=self.text.get("embed", "user"),
                value=f"**{self.sanitise(hoarder.name)}** ({hoarder.id})",
            )
            embed.add_field(name=self.text.get("embed", "status"),
                            value=hoarder.status)
            embed.add_field(
                name=self.text.get("embed", "programmes"),
                value=", ".join(progs),
                inline=False,
            )
            if warn:
                if num % 5 == 0:
                    await msg.edit(content=self.text.get(
                        "sending", num=num, all=len(hoarders)))
                await hoarder.send(
                    self.text.get("warning", guild=self.getGuild().name))
            if num % 8 == 0:
                # There's a limit of 25 fields per embed
                await ctx.send(embed=embed)
                embed = embed.clear_fields()
            # fmt: on
        if warn:
            await msg.edit(content=self.text.get("sent", num=len(hoarders)))
        await ctx.send(embed=embed)

    @commands.guild_only()
    @commands.check(acl.check)
    @commands.bot_has_permissions(manage_messages=True)
    @commands.command()
    async def purge(self, ctx, limit: int, pinMode: str = "pinSkip"):
        """Delete messages from current text channel

        limit: how many messages should be deleted
        mode: pinSkip (default) | pinStop | pinIgnore
        """
        if pinMode not in ("pinSkip", "pinStop", "pinIgnore"):
            return await ctx.send_help(ctx.invoked_with)

        now = time.monotonic()
        messages = await ctx.channel.history(limit=limit).flatten()

        total = 0
        for message in messages:
            if message.pinned and pinMode == "pinStop":
                break
            elif message.pinned and pinMode == "pinSkip":
                continue

            try:
                await message.delete()
                total += 1
            except discord.errors.HTTPException:
                pass

        delta = str(int(time.monotonic() - now))
        await self.event.sudo(ctx, f"Purged {total} posts in {delta}s.")

    @commands.check(acl.check)
    @commands.bot_has_permissions(manage_channels=True)
    @commands.command(name="teacher_channel", aliases=["teacher-channel"])
    async def teacher_channel(self, ctx, channel: discord.TextChannel):
        """Create subject channel will be visible for the subject's teacher, too

        channel: Subject channel to be duplicated
        """
        if channel.name not in config.subjects:
            return await self.output.error(
                ctx, self.text.get("not_subject", channel=channel.mention))

        ch = await channel.clone(name=channel.name +
                                 config.get("channels", "teacher suffix"))
        await ch.edit(position=channel.position + 1)
        await ctx.send(self.text.get("teacher_channel", channel=ch.mention))

        await self.event.sudo(ctx, f"Teacher channel {ch.name}")
Exemplo n.º 13
0
class Karma(rubbercog.Rubbercog):
    """Karma"""
    def __init__(self, bot):
        super().__init__(bot)

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

    ##
    ## Commands
    ##
    @commands.group(name="karma")
    async def karma(self, ctx):
        """Karma"""
        if ctx.invoked_subcommand is None:
            await self.karma_stalk(ctx, member=ctx.author)

    @commands.cooldown(rate=2, per=30, type=commands.BucketType.user)
    @karma.command(name="stalk")
    async def karma_stalk(self, ctx, member: discord.Member):
        """See someone's karma"""
        k = repo_k.get_karma(member.id)
        embed = self.embed(
            ctx=ctx,
            description=self.text.get("stalk_user",
                                      user=self.sanitise(member.display_name)),
        )
        embed.add_field(
            name=self.text.get("stalk_karma"),
            value=f"**{k.karma.value}** ({k.karma.position}.)",
            inline=False,
        )
        embed.add_field(
            name=self.text.get("stalk_positive"),
            value=f"**{k.positive.value}** ({k.positive.position}.)",
        )
        embed.add_field(
            name=self.text.get("stalk_negative"),
            value=f"**{k.negative.value}** ({k.negative.position}.)",
        )
        await ctx.send(embed=embed)
        await utils.room_check(ctx)

    @commands.cooldown(rate=2, per=30, type=commands.BucketType.user)
    @karma.command(name="emote", aliases=["emoji"])
    async def karma_emote(self, ctx, emote: str):
        """See emote's karma"""
        if not self._isUnicode(emote):
            try:
                emote_id = int(self._emoteToID(emote))
                emote = await ctx.guild.fetch_emoji(emote_id)
            except (ValueError, IndexError):
                return await utils.send_help(ctx)
            except discord.NotFound:
                return await ctx.send(self.text.get("emoji_not_found"))

        value = repo_k.emoji_value_raw(emote)
        if value is None:
            return await ctx.send(self.text.get("emoji_not_voted"))

        await ctx.send(
            self.text.get("emoji", emoji=str(emote), value=str(value)))
        await utils.room_check(ctx)

    @commands.guild_only()
    @commands.cooldown(rate=2, per=30, type=commands.BucketType.user)
    @karma.command(name="emotes", aliases=["emojis"])
    async def karma_emotes(self, ctx):
        """See karma for all emotes"""
        emotes = await ctx.guild.fetch_emojis()
        emotes = [e for e in emotes if not e.animated]
        content = []

        emotes_positive = self._getEmoteList(emotes, "1")
        if len(emotes_positive) > 0:
            content.append(self.text.get("emojis_positive"))
            content += self._emoteListToMessage(emotes_positive)

        emotes_neutral = self._getEmoteList(emotes, "0")
        if len(emotes_neutral) > 0:
            content.append(self.text.get("emojis_neutral"))
            content += self._emoteListToMessage(emotes_neutral)

        emotes_negative = self._getEmoteList(emotes, "-1")
        if len(emotes_negative) > 0:
            content.append(self.text.get("emojis_negative"))
            content += self._emoteListToMessage(emotes_negative)

        emotes_nonvoted = self._getNonvotedEmoteList(emotes)
        if len(emotes_nonvoted) > 0:
            content.append(self.text.get("emojis_not_voted"))
            content += self._emoteListToMessage(emotes_nonvoted)

        if len(content) == 0:
            content.append(self.text.get("no_emojis"))

        line = ""
        for items in [x for x in content]:
            if items[0] != "<":
                # description
                if len(line):
                    await ctx.send(line)
                    line = ""
                await ctx.send(items)
                continue

            if line.count("\n") >= 3:
                await ctx.send(line)
                line = ""

            line += "\n" + items
        await ctx.send(line)

        await utils.room_check(ctx)

    @commands.guild_only()
    @commands.check(acl.check)
    @karma.command(name="vote")
    async def karma_vote(self, ctx, emote: str = None):
        """Vote for emote's karma value"""
        if emote is None:
            emojis = await ctx.guild.fetch_emojis()
            emojis = [e for e in emojis if not e.animated]
            nonvoted = self._getNonvotedEmoteList(emojis)

            if len(nonvoted) == 0:
                return await ctx.author.send(
                    self.text.get("all_emojis_voted", guild=ctx.guild.name))
            emote = nonvoted[0]

        message = await ctx.send(
            self.text.get(
                "vote_info",
                emoji=emote,
                time=self.config.get("vote time"),
                limit=self.config.get("vote limit"),
            ))
        # set default of zero, so we can run the command multiple times
        repo_k.set_emoji_value(str(self._emoteToID(emote)), 0)

        # add options and vote
        await message.add_reaction("☑️")
        await message.add_reaction("0⃣")
        await message.add_reaction("❎")

        await self.event.sudo(ctx, f"Vote over value of {emote} started.")
        await asyncio.sleep(self.config.get("vote time") * 60)

        # update cached message
        message = await ctx.channel.fetch_message(message.id)

        positive = 0
        negative = 0
        neutral = 0
        for reaction in message.reactions:
            if reaction.emoji == "☑️":
                positive = reaction.count - 1
            elif reaction.emoji == "❎":
                negative = reaction.count - 1
            elif reaction.emoji == "0⃣":
                neutral = reaction.count - 1

        if positive + negative + neutral < self.config.get("vote limit"):
            await self.event.sudo(ctx, f"Vote for {emote} failed.")
            return await ctx.send(self.text.get("vote_failed", emoji=emote))

        result = 0
        if positive > negative + neutral:
            result = 1
        elif negative > positive + neutral:
            result = -1

        repo_k.set_emoji_value(str(self._emoteToID(emote)), result)
        await ctx.send(self.text.get("vote_result", emoji=emote, value=result))
        await self.event.sudo(ctx, f"{emote} karma value voted as {result}.")

    @commands.check(acl.check)
    @karma.command(name="set")
    async def karma_set(self, ctx, emoji: discord.Emoji, value: int):
        """Set karma value without public vote"""
        repo_k.set_emoji_value(str(self._emoteToID(emoji)), value)
        await ctx.send(self.text.get("emoji", emoji=emoji, value=value))
        await self.event.sudo(ctx, f"Karma of {emoji} set to {value}.")

        await utils.delete(ctx)

    @commands.cooldown(rate=2, per=30, type=commands.BucketType.user)
    @karma.command(name="message")
    async def karma_message(self, ctx, link: str):
        """Get karma for given message"""
        converter = commands.MessageConverter()
        try:
            message = await converter.convert(ctx=ctx, argument=link)
        except Exception as error:
            return await self.output.error(ctx, "Message not found", error)

        embed = self.embed(ctx=ctx, description=f"{message.author}")

        # fmt: off
        count = True
        if message.channel.id in self.config.get("banned channels") \
        or (
            not self.config.get("count subjects")
            and repo_s.get(message.channel.name) is not None
        ):
            count = False
        for word in self.config.get("banned words"):
            if word in message.content:
                count = False
                break
        # fmt: on

        output = {"negative": [], "neutral": [], "positive": []}
        karma = 0

        for reaction in message.reactions:
            emote = reaction.emoji
            value = repo_k.emoji_value_raw(emote)
            if value == 1:
                output["positive"].append(emote)
                karma += reaction.count
                async for user in reaction.users():
                    if user.id == message.author.id:
                        karma -= 1
                        break
            elif value == -1:
                output["negative"].append(emote)
                karma -= reaction.count
                async for user in reaction.users():
                    if user.id == message.author.id:
                        karma += 1
                        break
            else:
                output["neutral"].append(emote)

        embed.add_field(name="Link", value=message.jump_url, inline=False)

        if count:
            for key, value in output.items():
                if len(value) == 0:
                    continue
                emotes = " ".join(str(emote) for emote in value)
                embed.add_field(name=self.text.get("embed_" + key),
                                value=emotes)
            # fmt: off
            embed.add_field(
                name=self.text.get("embed_total"),
                value=f"**{karma}**",
                inline=False,
            )
            # fmt: on
        else:
            embed.add_field(name="\u200b",
                            value=self.text.get("embed_disabled"),
                            inline=False)
        await ctx.send(embed=embed)

        await utils.room_check(ctx)

    @commands.check(acl.check)
    @karma.command(name="give")
    async def karma_give(self, ctx, member: discord.Member, value: int):
        """Give karma points to someone"""
        repo_k.update_karma(member=member, giver=ctx.author, emoji_value=value)
        await ctx.send(self.text.get("give",
                                     "given" if value > 0 else "taken"))
        await self.event.sudo(ctx, f"{member} got {value} karma points.")

    @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel)
    @commands.command(aliases=["karmaboard"])
    async def leaderboard(self, ctx, offset: int = 0):
        """Karma leaderboard"""
        await self.sendBoard(ctx, "desc", offset)

    @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel)
    @commands.command()
    async def loserboard(self, ctx, offset: int = 0):
        """Karma leaderboard, from the worst"""
        await self.sendBoard(ctx, "asc", offset)

    @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel)
    @commands.command()
    async def givingboard(self, ctx, offset: int = 0):
        """Karma leaderboard"""
        await self.sendBoard(ctx, "give", offset)

    @commands.cooldown(rate=3, per=30, type=commands.BucketType.channel)
    @commands.command(aliases=["stealingboard"])
    async def takingboard(self, ctx, offset: int = 0):
        """Karma leaderboard"""
        await self.sendBoard(ctx, "take", offset)

    ##
    ## Listeners
    ##
    @commands.Cog.listener()
    async def on_raw_reaction_add(self, payload):
        """Karma add"""
        parsed_payload = await self._payloadToReaction(payload)
        if parsed_payload is None:
            return
        channel, member, message, emote = parsed_payload

        count = self.doCountKarma(member=member, message=message)
        if not count:
            return

        repo_k.karma_emoji(message.author, member, emote)

    @commands.Cog.listener()
    async def on_raw_reaction_remove(self, payload):
        """Karma remove"""
        parsed_payload = await self._payloadToReaction(payload)
        if parsed_payload is None:
            return
        channel, member, message, emote = parsed_payload

        count = self.doCountKarma(member=member, message=message)
        if not count:
            return

        repo_k.karma_emoji_remove(message.author, member, emote)

    @commands.Cog.listener()
    async def on_reaction_add(self, reaction, user):
        """Scrolling, vote"""
        if user.bot:
            return

        if reaction.message.channel.id == config.get("channels", "vote"):
            await self.checkVoteEmote(reaction, user)

        if str(reaction) in ("⏪", "◀", "▶"):
            await self.checkBoardEmoji(reaction, user)

    ##
    ## Helper functions
    ##
    def _isUnicode(self, text):
        demojized = demojize(text)
        if demojized.count(":") != 2:
            return False
        if demojized.split(":")[2] != "":
            return False
        return demojized != text

    def _getEmoteList(self, guild_emotes: list, value: int) -> list:
        db_emotes = repo_k.getEmotesByValue(value)

        result = []
        for guild_emote in guild_emotes:
            if str(guild_emote.id) in db_emotes:
                result.append(guild_emote)
        return result

    def _getNonvotedEmoteList(self, guild_emotes: list) -> list:
        db_emotes = [x.emoji_ID for x in repo_k.get_all_emojis()]

        result = []
        for guild_emote in guild_emotes:
            if str(guild_emote.id) not in db_emotes:
                result.append(guild_emote)
        return result

    def _emoteListToMessage(self, emotes: list) -> List[str]:
        line = ""
        result = []
        for i, emote in enumerate(emotes):
            if i % 8 == 0:
                result.append(line)
                line = ""
            line += f"{emote} "
        result.append(line)

        return [r for r in result if len(r) > 0]

    def _emoteToID(self, emote: str):
        if ":" in str(emote):
            return int(str(emote).split(":")[2][:-1])
        return emote

    async def _payloadToReaction(
            self, payload: discord.RawReactionActionEvent) -> tuple:
        """Return (channel, member, message, emote)"""
        channel = self.bot.get_channel(payload.channel_id)
        if channel is None or not isinstance(channel, discord.TextChannel):
            return

        member = channel.guild.get_member(payload.user_id)
        if member is None or member.bot:
            return

        try:
            message = await channel.fetch_message(payload.message_id)
        except discord.NotFound:
            return
        if message is None:
            return

        if payload.emoji.is_custom_emoji():
            emote = payload.emoji.id
        else:
            emote = payload.emoji.name

        return channel, member, message, emote

    async def _remove_reaction(self, reaction, user):
        try:
            await reaction.remove(user)
        except:
            pass

    ##
    ## Logic
    ##
    def doCountKarma(self, *, member: discord.Member,
                     message: discord.Message) -> bool:
        """Return True only if the message should be counted"""
        # do not count author's reactions
        if member.id == message.author.id:
            return False

        # only count master and slave guilds
        if message.guild.id not in (config.guild_id, config.slave_id):
            return False

        # do not count banned channels
        if message.channel.id in self.config.get("banned channels"):
            return False

        # do not count banned roles
        if self.config.get("banned roles") in map(lambda x: x.id,
                                                  member.roles):
            return False

        # do not count banned strings
        for word in self.config.get("banned words"):
            if word in message.content:
                return False

        # optionally, do not count subject channels
        if not self.config.get("count subjects"):
            if repo_s.get(message.channel.name) is not None:
                return False

        return True

    async def sendBoard(self, ctx: commands.Context, parameter: str,
                        offset: int):
        """Send karma board

        parameter: desc | asc | give | take
        """
        # convert offset to be zero-base
        offset -= 1

        max_offset = repo_k.getMemberCount() - self.config.get(
            "leaderboard limit")
        if offset < 0:
            offset = 0
        elif offset > max_offset:
            offset = max_offset

        # fmt: off
        title = "{title}  {ordering}".format(
            title=self.text.get("board_title"),
            ordering=self.text.get("board_" + parameter + "_t"),
        )
        # fmt: on
        description = self.text.get("board_" + parameter + "_d")
        embed = self.embed(ctx=ctx, title=title, description=description)
        embed = self.fillBoard(embed,
                               member=ctx.author,
                               order=parameter,
                               offset=offset)

        message = await ctx.send(embed=embed)
        await message.add_reaction("⏪")
        await message.add_reaction("◀")
        await message.add_reaction("▶")

        await utils.room_check(ctx)

    def fillBoard(self, embed, *, member, order: str,
                  offset: int) -> discord.Embed:
        limit = self.config.get("leaderboard limit")
        template = "`{position:>2}` … `{karma:>5}` {username}"

        # get repository parameters
        column = "karma"
        if order == "give":
            column = "positive"
        elif order == "take":
            column = "negative"

        if order == "desc":
            attr = DB_Karma.karma.desc()
        elif order == "asc":
            attr = DB_Karma.karma
        elif order == "give":
            attr = DB_Karma.positive.desc()
        elif order == "take":
            attr = DB_Karma.negative.desc()

        # construct first field
        value = []
        board = repo_k.getLeaderboard(attr, offset, limit)

        if not board or not board.count():
            return None

        user_in_list = False
        for i, db_user in enumerate(board, start=offset):
            # fmt: off
            user = self.getGuild().get_member(int(db_user.discord_id))
            username = (self.sanitise(user.display_name) if hasattr(
                user, "display_name") else "_unknown_")

            if int(db_user.discord_id) == member.id:
                username = f"**{username}**"
                user_in_list = True

            value.append(
                template.format(
                    position=i + 1,
                    karma=getattr(db_user, column),
                    username=username,
                ))
            # fmt: on

        embed.clear_fields()

        if offset == 0:
            name = self.text.get("board_1", num=limit)
        else:
            name = self.text.get("board_x", num=limit, offset=offset + 1)
        embed.add_field(name=name, value="\n".join(value), inline=False)

        # construct user field, if they are not included in first one
        if not user_in_list:
            k = repo_k.get_karma(member.id)
            # get right values
            if order in ("desc", "asc"):
                value, position = k.karma.value, k.karma.position
            elif order == "give":
                value, position = k.positive.value, k.positive.position
            else:
                value, position = k.negative.value, k.negative.position
            username = "******" + self.sanitise(member.display_name) + "**"

            embed.add_field(
                name=self.text.get("board_user"),
                value=template.format(position=position,
                                      karma=value,
                                      username=username),
            )

        return embed

    async def checkVoteEmote(self, reaction, user):
        """Check if the emote is vote emote"""
        if not hasattr(reaction, "emoji"):
            return await self._remove_reaction(reaction, user)

        if not reaction.message.content.startswith(
                self.text.get("vote_info")[:25]):
            return

        if str(reaction.emoji) not in ("☑️", "0⃣", "❎"):
            await self._remove_reaction(reaction, user)

    async def checkBoardEmoji(self, reaction, user):
        """Check if the leaderboard should be scrolled"""
        if user.bot:
            return

        if str(reaction) not in ("⏪", "◀", "▶"):
            return

        # fmt: off
        if len(reaction.message.embeds) != 1 \
        or type(reaction.message.embeds[0].title) != str \
        or not reaction.message.embeds[0].title.startswith(self.text.get("board_title")):
            return
        # fmt: on

        embed = reaction.message.embeds[0]

        # get ordering
        for o in ("desc", "asc", "give", "take"):
            if embed.title.endswith(self.text.get("board_" + o + "_t")):
                order = o
                break

        # get current offset
        if "," in embed.fields[0].name:
            offset = int(embed.fields[0].name.split(" ")[-1]) - 1
        else:
            offset = 0

        # get new offset
        if str(reaction) == "⏪":
            offset = 0
        elif str(reaction) == "◀":
            offset -= self.config.get("leaderboard limit")
        elif str(reaction) == "▶":
            offset += self.config.get("leaderboard limit")

        if offset < 0:
            offset = 0

        # apply
        embed = self.fillBoard(embed, member=user, order=order, offset=offset)
        if embed:
            await reaction.message.edit(embed=embed)
        await utils.remove_reaction(reaction, user)
Exemplo n.º 14
0
class Base(rubbercog.Rubbercog):
    """About"""
    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 cog_unload(self):
        self.status_loop.cancel()

    ##
    ## Loops
    ##

    @tasks.loop(minutes=1)
    async def status_loop(self):
        """Observe latency to the Discord API. If it goes below 0.25s, "online"
        will be switched to "idle", over 0.50s is "dnd".
        """
        if self.bot.latency <= 0.25:
            status = "online"
        elif self.bot.latency <= 0.5:
            status = "idle"
        else:
            status = "dnd"

        if self.status != status:
            self.status = status
            await self.console.debug(
                "latency",
                f"Updating status to {status} (latency {self.bot.latency:.2f})."
            )
            await utils.set_presence(self.bot,
                                     status=getattr(discord.Status, status))

    @status_loop.before_loop
    async def before_status_loop(self):
        if not self.bot.is_ready():
            await self.bot.wait_until_ready()

    ##
    ## Commands
    ##

    @commands.cooldown(rate=1, per=10.0, type=commands.BucketType.channel)
    @commands.command()
    async def uptime(self, ctx):
        """Bot uptime"""
        now = datetime.datetime.now().replace(microsecond=0)
        delta = now - boottime

        embed = self.embed(ctx=ctx)
        embed.add_field(name="Boot", value=str(boottime), inline=False)
        embed.add_field(name="Uptime", value=str(delta), inline=False)
        await ctx.send(embed=embed)
        await utils.delete(ctx.message)

    @commands.cooldown(rate=1, per=10.0, type=commands.BucketType.channel)
    @commands.command()
    async def ping(self, ctx):
        """Bot latency"""
        await ctx.reply("pong: **{:.2f} s**".format(self.bot.latency))

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_raw_reaction_add(self,
                                  payload: discord.RawReactionActionEvent):
        """Pinning functionality"""
        channel = self.bot.get_channel(payload.channel_id)
        if channel is None or not isinstance(channel, discord.TextChannel):
            return
        if payload.emoji.name not in ("📌", "📍", "🔖"):
            return

        try:
            message = await channel.fetch_message(payload.message_id)
        except discord.errors.NotFound:
            return
        if payload.emoji.is_custom_emoji():
            return

        reaction_author: discord.User = self.bot.get_user(payload.user_id)
        if reaction_author.bot:
            return

        if message.type != discord.MessageType.default:
            await message.remove_reaction(payload.emoji, reaction_author)
            return

        if payload.emoji.name == "📍" and not reaction_author.bot:
            await reaction_author.send(self.text.get("bad pin"))
            await message.remove_reaction(payload.emoji, reaction_author)
            return

        if payload.emoji.name == "🔖":
            await self.bookmark_message(message, reaction_author)
            await message.remove_reaction(payload.emoji, reaction_author)
            return

        for reaction in message.reactions:
            if reaction.emoji != "📌":
                continue

            if message.pinned:
                return await reaction.clear()

            if channel.id in self.config.get("unpinnable"):
                return await reaction.clear()

            if reaction.count < self.config.get("pins"):
                return

            users = await reaction.users().flatten()
            user_names = ", ".join([str(user) for user in users])
            log_embed = self.embed(title=self.text.get("pinned"),
                                   description=user_names)
            if len(message.content):
                value = utils.id_to_datetime(
                    message.id).strftime("%Y-%m-%d %H:%M:%S")
                log_embed.add_field(name=str(message.author), value=value)
            url_text = self.text.get(
                "link text",
                channel=channel.name,
                guild=channel.guild.name,
            )
            if len(message.content):
                log_embed.add_field(
                    name=self.text.get("content"),
                    value=message.content[:1024],
                    inline=False,
                )
            if len(message.content) >= 1024:
                log_embed.add_field(
                    name="\u200b",
                    value=message.content[1024:],
                    inline=False,
                )
            if len(message.attachments):
                log_embed.add_field(
                    name=self.text.get("content"),
                    value=self.text.get("attachments",
                                        count=len(message.attachments)),
                    inline=False,
                )
            log_embed.add_field(
                name=self.text.get("link"),
                value=f"[{url_text}]({message.jump_url})",
                inline=False,
            )

            try:
                await message.pin()
            except discord.errors.HTTPException as e:
                await self.event.user(channel, "Could not pin message.", e)
                error_embed = self.embed(
                    title=self.text.get("pin error"),
                    description=user_names,
                    url=message.jump_url,
                )
                await message.channel.send(embed=error_embed)
                return

            event_channel = self.bot.get_channel(
                config.get("channels", "events"))
            await event_channel.send(embed=log_embed)

            await reaction.clear()
            await message.add_reaction("📍")

    async def bookmark_message(
        self,
        message: discord.Message,
        user: Union[discord.Member, discord.User],
    ):
        embed = self.embed(
            title=self.text.get("bookmark", "title"),
            description=message.content,
            author=message.author,
        )
        timestamp = utils.id_to_datetime(
            message.id).strftime("%Y-%m-%d %H:%M:%S")
        embed.add_field(
            name=self.sanitise(message.author.display_name),
            value=self.text.get(
                "bookmark",
                "info",
                timestamp=timestamp,
                channel=message.channel.name,
                link=message.jump_url,
            ),
            inline=False,
        )
        info = set()
        if len(message.attachments):
            embed.add_field(
                name=self.text.get("bookmark", "files"),
                value=self.text.get("bookmark",
                                    "total",
                                    count=len(message.attachments)),
            )
        if len(message.embeds):
            embed.add_field(
                name=self.text.get("bookmark", "embeds"),
                value=self.text.get("bookmark",
                                    "total",
                                    count=len(message.embeds)),
            )
        await user.send(embed=embed)
        await self.event.user(
            user,
            f"Bookmarked message in #{message.channel.name}\n> {message.jump_url}",
            escape_markdown=False,
        )
Exemplo n.º 15
0
class Stalker(rubbercog.Rubbercog):
    """A cog for database lookups"""

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

        self.text = CogText("stalker")

    def dbobj2email(self, user):
        if user is not None:
            if user.group == "FEKT" and "@" not in user.login:
                email = user.login + "@stud.feec.vutbr.cz"
            elif user.group == "VUT" and "@" not in user.login:
                email = user.login + "@vutbr.cz"
            else:
                email = user.login
            return email
        return

    @commands.check(acl.check)
    @commands.group(name="whois", aliases=["gdo"])
    async def whois(self, ctx: commands.Context):
        """Get information about user"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @whois.command(name="member", aliases=["tag", "user", "id"])
    async def whois_member(self, ctx: commands.Context, member: discord.Member):
        """Get information about guild member

        member: A guild member
        """
        db_member = repository.get(member.id)

        embed = self.whois_embed(ctx, member, db_member)

        await ctx.send(embed=embed)
        await self.event.sudo(ctx, f"Database lookup for member **{member}**.")

    @commands.check(acl.check)
    @whois.command(name="email", aliases=["login", "xlogin"])
    async def whois_email(self, ctx: commands.Context, email: str = None):
        """Get information about xlogin

        email: An e-mail
        """
        db_member = repository.getByLogin(email)
        if db_member is None:
            return self.output.info(ctx, self.text.get("not_found"))
        member = self.getGuild().get_member(db_member.discord_id)
        if member is None:
            return self.output.info(ctx, self.text.get("not_in_guild"))

        embed = self.whois_embed(ctx, member, db_member)

        await ctx.send(embed=embed)
        await self.event.sudo(ctx, f"Database lookup for e-mail **{email}**.")

    @commands.check(acl.check)
    @whois.command(name="logins", aliases=["emails"])
    async def whois_logins(self, ctx, prefix: str):
        """Filter database by login"""
        users = repository.getByPrefix(prefix=prefix)

        # parse data
        items = []
        template = "`{name:<10}` … {email}"
        for user in users:
            member = self.bot.get_user(user.discord_id)
            name = member.name if member is not None else ""
            email = self.dbobj2email(user)
            items.append(template.format(name=name, email=email))

        # construct embed fields
        fields = []
        field = ""
        for item in items:
            if len(field + item) > 1000:
                fields.append(field)
                field = ""
            field = field + "\n" + item
        fields.append(field)

        # create embed
        embed = self.embed(ctx=ctx, description=self.text.get("prefix", "result", num=len(users)))
        for field in fields[:5]:  # there is a limit of 6000 characters in total
            embed.add_field(name="\u200b", value=field)
        if len(fields) > 5:
            embed.add_field(
                name=self.text.get("prefix", "too_many"),
                value=self.text.get("prefix", "omitted"),
                inline=False,
            )

        await ctx.send(embed=embed)
        await self.event.sudo(ctx, f"Database lookup for e-mail prefix **{prefix}**.")

    @commands.check(acl.check)
    @commands.group(aliases=["db"])
    async def database(self, ctx: commands.Context):
        """Manage users"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @database.command(name="add")
    async def database_add(
        self,
        ctx: commands.Context,
        member: discord.Member = None,
        login: str = None,
        group: discord.Role = None,
    ):
        """Add user to database

        member: A server member
        login: e-mail
        group: A role from `roles_native` or `roles_guest` in config file
        """
        if member is None or login is None or group is None:
            return await utils.send_help(ctx)

        # define variables
        guild = self.bot.get_guild(config.guild_id)
        verify = discord.utils.get(guild.roles, name="VERIFY")

        if repository.get(member.id) is not None:
            return await self.output.error(ctx, self.text.get("db", "duplicate"))

        try:
            repository.add(
                discord_id=member.id,
                login=login,
                group=group.name,
                status="verified",
                code="MANUAL",
            )
        except Exception as e:
            return await self.output.error(ctx, self.text.get("db", "write_error"), e)

        # assign roles, if neccesary
        if verify not in member.roles:
            await member.add_roles(verify)
        if group not in member.roles:
            await member.add_roles(group)

        # display the result
        embed = self.whois_embed(ctx, member, repository.get(member.id))
        await ctx.send(embed=embed)
        await self.event.sudo(ctx, f"New user {member} ({group.name})")

    @commands.check(acl.check)
    @database.command(name="remove", aliases=["delete"])
    async def database_remove(self, ctx: commands.Context, member: discord.Member):
        """Remove user from database"""
        result = repository.deleteId(discord_id=member.id)

        if result < 1:
            return await self.output.error(ctx, self.text.get("db", "delete_error"))

        await ctx.send(self.text.get("db", "delete_success", num=result))
        await self.event.sudo(ctx, f"Member {member} ({member.id}) removed from database.")

    @commands.check(acl.check)
    @database.command(name="update")
    async def database_update(self, ctx, member: discord.Member, key: str, *, value):
        """Update user entry in database

        key: value
        - login: e-mail
        - group: one of the groups defined in gatekeeper mapping
        - status: [unknown, pending, verified, kicked, banned]
        - comment: commentary on user
        """
        if key not in ("login", "group", "status", "comment"):
            return await self.output.error(ctx, self.text.get("db", "invalid_key"))

        if key == "login":
            repository.update(member.id, login=value)

        elif key == "group":
            # get list of role names, defined in
            role_ids = config.get("roles", "native") + config.get("roles", "guests")
            role_names = [
                x.name
                for x in [self.getGuild().get_role(x) for x in role_ids]
                if hasattr(x, "name")
            ]
            value = value.upper()
            if value not in role_names:
                return await self.output.error(ctx, self.text.get("db", "invalid_value"))
            repository.update(member.id, group=value)

        elif key == "status":
            if value not in ("unknown", "pending", "verified", "kicked", "banned"):
                return await self.output.error(ctx, self.text.get("db", "invalid_value"))
            repository.update(member.id, status=value)

        elif key == "comment":
            repository.update(member.id, comment=value)

        await self.event.sudo(ctx, f"Updated {member}: {key} = {value}.")
        await ctx.send(self.text.get("db", "update_success"))

    @commands.check(acl.check)
    @database.command(name="show")
    async def database_show(self, ctx, param: str):
        """Filter users by parameter

        param: [unverified, pending, kicked, banned]
        """
        if param not in ("unverified", "pending", "kicked", "banned"):
            return await utils.send_help(ctx)

        await self._database_show_filter(ctx, param)

    @commands.check(acl.check)
    @commands.command(name="guild", aliases=["server"])
    async def guild(self, ctx: commands.Context):
        """Display general about guild"""
        embed = self.embed(ctx=ctx)
        g = self.getGuild()

        # guild
        embed.add_field(
            name=f"Guild **{g.name}**",
            inline=False,
            value=f"Created {g.created_at.strftime('%Y-%m-%d')}," f" owned by **{g.owner.name}**",
        )

        # verification
        states = ", ".join(
            "**{}** {}".format(repository.countStatus(state), state) for state in config.db_states
        )
        embed.add_field(name="Verification states", value=states, inline=False)

        # roles
        role_ids = config.get("roles", "native") + config.get("roles", "guests")
        roles = []
        for role_id in role_ids:
            role = self.getGuild().get_role(role_id)
            if role is not None:
                roles.append(f"**{role}** {repository.countGroup(role.name)}")
            else:
                roles.append(f"**{role_id}** {repository.countGroup(role_id)}")
        roles = ", ".join(roles)
        embed.add_field(name="Roles", value=f"Total count {len(g.roles)}\n{roles}", inline=False)

        # channels
        embed.add_field(
            name=f"{len(g.categories)} categories",
            value=f"{len(g.text_channels)} text channels, {len(g.voice_channels)} voice channels",
        )

        # users
        embed.add_field(
            name="Users",
            value=f"Total count **{g.member_count}**, {g.premium_subscription_count} boosters",
        )

        await ctx.send(embed=embed)

    ##
    ## Logic
    ##

    async def _database_show_filter(self, ctx: commands.Context, status: str = None):
        """Helper function for all databas_show_* functions"""
        if status is None or status not in config.db_states:
            return await utils.send_help(ctx)

        users = repository.filterStatus(status=status)

        embed = self.embed(ctx=ctx)
        embed.add_field(name="Result", value="{} users found".format(len(users)), inline=False)
        if users:
            embed.add_field(name="-" * 60, value="LIST:", inline=False)
        for user in users:
            member = discord.utils.get(self.getGuild().members, id=user.discord_id)
            if member:
                name = "**{}**, {}".format(member.name, member.id)
            else:
                name = "**{}**, {} _(not on server)_".format(user.discord_id, user.group)
            d = user.changed
            date = (d[:4] + "-" + d[4:6] + "-" + d[6:]) if (d and len(d) == 8) else "_(none)_"
            embed.add_field(
                name=name, value="{}\nLast action on {}".format(self.dbobj2email(user), date)
            )

        await ctx.send(embed=embed, delete_after=config.delay_embed)

        await utils.delete(ctx)

    def whois_embed(self, ctx, member: discord.Member, db_member: object) -> discord.Embed:
        """Construct the whois embed"""
        embed = self.embed(ctx=ctx, title="Whois", description=member.mention)

        embed.add_field(
            name=self.text.get("whois", "information"),
            value=self.text.get(
                "whois",
                "account_information",
                name=self.sanitise(member.display_name),
                account_since=utils.id_to_datetime(member.id).strftime("%Y-%m-%d"),
                member_since=member.joined_at.strftime("%Y-%m-%d"),
            ),
            inline=False,
        )

        if db_member is not None:
            embed.add_field(
                name=self.text.get("whois", "email"),
                value=self.dbobj2email(db_member)
                if db_member.login
                else self.text.get("whois", "missing"),
            )

            embed.add_field(
                name=self.text.get("whois", "code"),
                value=db_member.code if db_member.code else self.text.get("whois", "missing"),
            )

            embed.add_field(
                name=self.text.get("whois", "status"),
                value=db_member.status if db_member.status else self.text.get("whois", "missing"),
            )

            embed.add_field(
                name=self.text.get("whois", "group"),
                value=db_member.group if db_member.group else self.text.get("whois", "missing"),
            )

            embed.add_field(
                name=self.text.get("whois", "changed"),
                value=db_member.changed if db_member.changed else self.text.get("whois", "missing"),
            )

            if db_member.comment and len(db_member.comment):
                embed.add_field(
                    name=self.text.get("whois", "comment"), value=db_member.comment, inline=False
                )

        role_list = ", ".join(list((r.name) for r in member.roles[::-1])[:-1])
        embed.add_field(
            name=self.text.get("whois", "roles"),
            value=role_list if len(role_list) else self.text.get("whois", "missing"),
        )

        return embed

    @commands.check(acl.check)
    @commands.command(name="channelinfo", aliases=['ci'])
    async def channelinfo(self, ctx: commands.Context, channel: discord.TextChannel = None):
        if channel is None:
            channel = ctx.channel

        webhooks = await channel.webhooks()

        channel_embed = discord.Embed(
            title=f"Information about `#{str(channel)}`",
            description="```css\nRole overwrites```",
            colour = discord.Color.green()
        )
        channel_embed.set_footer(text=f"Channel ID: {channel.id}")

        channel_embed.add_field(name="Channel topic", value=channel.topic)
        channel_embed.add_field(name="Number of people in channel", value=len(channel.members))
        channel_embed.add_field(name="Webhooks", value=len(webhooks))

        roles = []
        users = []
        for overwrite in channel.overwrites:
            if isinstance(overwrite, discord.Role):
                roles.append(overwrite)
            else:
                users.append(overwrite)

        if roles:
            channel_embed.description += '\n'.join(
            [f"{count}) {role.mention} (Permissions value: {role.permissions.value})"
            for count, role in enumerate(roles, start=1)]
        )

        if users:
            channel_embed.description += '\n\n```css\nUser overwrites```' + '\n'.join(
            [f"{count}) {user.mention} (Permissions value: {channel.permissions_for(user).value})"
            for count, user in enumerate(users, start=1)]
        )

        await ctx.channel.send(embed=channel_embed)
Exemplo n.º 16
0
class Review(rubbercog.Rubbercog):
    """Subject reviews"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("review")

    ##
    ## Commands
    ##

    @commands.cooldown(rate=5, per=60, type=commands.BucketType.user)
    @commands.check(acl.check)
    @commands.group(name="review")
    async def review(self, ctx):
        """Manage your subject reviews"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @review.command(name="subject", aliases=["see"])
    async def review_subject(self, ctx, subject: str):
        """See subject's reviews"""
        db_subject = repo_s.get(subject)
        if db_subject is None:
            return await ctx.send(self.text.get("no_subject"))

        title = self.text.get("embed", "title") + subject
        name = db_subject.name if db_subject.name is not None else discord.Embed.Empty
        if name is not discord.Embed.Empty and db_subject.category is not None:
            name += f" ({db_subject.category})"

        db_reviews = repo_r.get_subject_reviews(subject)
        if db_reviews.count() == 0:
            return await ctx.send(
                self.text.get("no_reviews", mention=ctx.author.mention))

        _total = 0
        for db_review in db_reviews:
            _total += db_review.Review.tier
        average = _total / db_reviews.count()

        review = db_reviews[0].Review
        embed = self.embed(ctx=ctx,
                           title=title,
                           description=name,
                           page=(1, db_reviews.count()))
        embed = self.fill_subject_embed(embed, review, average)

        message = await ctx.send(embed=embed)
        if db_reviews.count() > 1:
            await message.add_reaction("◀")
            await message.add_reaction("▶")
        if db_reviews.count() > 0:
            await message.add_reaction("👍")
            await message.add_reaction("🛑")
            await message.add_reaction("👎")

    @commands.check(acl.check)
    @review.command(name="list", aliases=["available"])
    async def review_list(self, ctx):
        """Get list of reviewed subjects"""
        subjects = set()
        for r in repo_r.get_all_reviews():
            subjects.add(r.subject)

        if not len(subjects):
            return await ctx.send(self.text.get("empty"),
                                  mention=ctx.author.mention)

        await ctx.send(">>> " + ", ".join(f"`{s}`" for s in sorted(subjects)))

    @commands.check(acl.check)
    @review.command(name="my-list")
    async def review_mylist(self, ctx):
        """Get list of your reviewed subjects"""
        subjects = set()
        for r in repo_r.get_all_reviews():
            if r.discord_id == ctx.author.id:
                subjects.add(r.subject)

        if not len(subjects):
            return await ctx.send(self.text.get("empty"),
                                  mention=ctx.author.mention)

        await ctx.send(">>> " + ", ".join(f"`{s}`" for s in sorted(subjects)))

    @commands.check(acl.check)
    @review.command(name="add", aliases=["update"])
    async def review_add(self, ctx, subject: str, mark: int, *, text: str):
        """Add a review

        subject: Subject code
        mark: 1-5 (one being best)
        text: Your review
        """
        result = await self.add_review(ctx,
                                       subject,
                                       mark,
                                       text,
                                       anonymous=False)

        if result is not None:
            await self.event.user(
                ctx, f"Review **#{result.id}** for **{subject}**.")
            await ctx.send(self.text.get("added"))

    @commands.check(acl.check)
    @review.command(name="add-anonymous", aliases=["anonymous", "anon"])
    async def review_add_anonymous(self, ctx, subject: str, mark: int, *,
                                   text: str):
        """Add anonymous review

        subject: Subject code
        mark: 1-5 (one being best)
        text: Your review
        """
        result = await self.add_review(ctx,
                                       subject,
                                       mark,
                                       text,
                                       anonymous=True)

        if result is not None:
            await utils.delete(ctx.message)
            await self.event.user(
                ctx, f"Anonymous review **#{result.id}** for **{subject}**.")
            await ctx.send(self.text.get("added"))

    @commands.check(acl.check)
    @review.command(name="remove")
    async def review_remove(self, ctx, subject: str):
        """Remove your review

        subject: Subject code
        """
        review = repo_r.get_review_by_author_subject(ctx.author.id, subject)
        if review is None:
            return await ctx.send(
                self.text.get("no_review", mention=ctx.author.mention))

        repo_r.remove(review.id)
        await self.event.user(ctx, f"Removed review for **{subject}**.")
        return await ctx.send(self.text.get("removed"))

    @commands.check(acl.check)
    @commands.group(name="sudo_review")
    async def sudo_review(self, ctx):
        """Manage other user's reviews"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @sudo_review.command(name="remove")
    async def sudo_review_remove(self, ctx, id: int):
        """Remove someone's review"""
        db_review = repo_r.get(id)
        if db_review is None:
            return await ctx.send(
                self.text.get("no_review", mention=ctx.author.mention))

        repo_r.remove(id)
        await self.event.sudo(ctx, f"Review **#{id}** removed")
        return await ctx.send(self.text.get("removed"))

    @commands.check(acl.check)
    @commands.group(name="subject")
    async def subject(self, ctx):
        """Manage subjects"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @subject.command(name="info")
    async def subject_info(self, ctx, subject: str):
        """Get information about subject

        subject: Subject code
        """
        db_subject = repo_s.get(subject)
        if db_subject is None:
            return await ctx.send(self.text.get("no_subject"))

        embed = self.embed(ctx=ctx, title=db_subject.shortcut)
        if db_subject.name or db_subject.category:
            embed.add_field(
                name=db_subject.name or "\u200b",
                value=db_subject.category or "\u200b",
                inline=False,
            )
        embed.add_field(
            name=self.text.get("info", "reviews"),
            value=self.text.get("info",
                                "count",
                                count=len(db_subject.reviews),
                                subject=subject)
            if len(db_subject.reviews) else self.text.get("info", "none"),
            inline=False,
        )

        await ctx.send(embed=embed)

    @commands.check(acl.check)
    @subject.command(name="add")
    async def subject_add(self, ctx, subject: str, name: str, category: str):
        """Add subject

        subject: Subject code
        name: Subject name
        category: Subject faculty or other assignment
        """
        db_subject = repo_s.get(subject)
        if db_subject is not None:
            return await ctx.send(self.text.get("subject_exists"))

        repo_s.add(subject, name, category)
        await self.event.sudo(ctx, f"Subject **{subject}** added.")
        await ctx.send(self.text.get("subject_added"))

    @commands.check(acl.check)
    @subject.command(name="update")
    async def subject_update(self, ctx, subject: str, name: str,
                             category: str):
        """Update subject

        subject: Subject code
        name: Subject name
        category: Subject faculty or other assignment
        """
        db_subject = repo_s.get(subject)
        if db_subject is None:
            return await ctx.send(self.text.get("no_subject"))

        repo_s.update(subject, name=name, category=category)
        await self.event.sudo(ctx, f"Subject **{subject}** updated.")
        await ctx.send(self.text.get("subject_updated"))

    @commands.check(acl.check)
    @subject.command(name="remove")
    async def subject_remove(self, ctx, subject: str):
        """Remove subject

        subject: Subject code
        """
        db_subject = repo_s.get(subject)
        if db_subject is None:
            return await ctx.send(self.text.get("no_subject"))

        repo_s.remove(subject)
        await self.event.sudo(ctx, f"Subject **{subject}** removed.")
        await ctx.send(self.text.get("subject_removed"))

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_reaction_add(self, reaction: discord.Reaction,
                              user: discord.User):
        # check for wanted embed
        if (user.bot or len(reaction.message.embeds) != 1
                or not isinstance(reaction.message.embeds[0].title, str)
                or not reaction.message.embeds[0].title.startswith(
                    self.text.get("embed", "title"))):
            return

        scroll = False
        vote = False
        scroll_delta = 0
        vote_value = 0

        # check for scroll availability
        if hasattr(reaction, "emoji"):
            # scrolling
            if str(reaction.emoji) == "◀":
                scroll = True
                scroll_delta = -1
            elif str(reaction.emoji) == "▶":
                scroll = True
                scroll_delta = 1
            # voting
            elif str(reaction.emoji) == "👍":
                vote = True
                vote_value = 1
            elif str(reaction.emoji) == "🛑":
                vote = True
                vote_value = 0
            elif str(reaction.emoji) == "👎":
                vote = True
                vote_value = -1
            # invalid
            else:
                # invalid reaction
                return await self._remove_reaction(reaction, user)
        else:
            # invalid reaction
            return await self._remove_reaction(reaction, user)

        embed = reaction.message.embeds[0]
        if embed.footer == discord.Embed.Empty or " | " not in embed.footer.text:
            return await self._remove_reaction(reaction, user)

        # get reviews for given subject
        subject = embed.title.replace(self.text.get("embed", "title"), "")
        reviews = repo_r.get_subject_reviews(subject)

        _total = 0
        for review in reviews:
            _total += review.Review.tier
        average = _total / reviews.count()

        # get page
        footer_text = embed.footer.text
        if scroll:
            pages = footer_text.split(" | ")[-1]
            page_current = int(pages.split("/")[0]) - 1

            page = (page_current + scroll_delta) % reviews.count()
            footer_text = footer_text.replace(pages,
                                              f"{page+1}/{reviews.count()}")
        else:
            page = 0

        # get new review
        review = reviews[page].Review

        if vote:
            # apply vote
            if user.id == review.discord_id:
                return await self._remove_reaction(reaction, user)
            if vote_value == 0:
                repo_r.remove_vote(review.id, str(user.id))
            else:
                repo_r.add_vote(review.id, vote_value == 1, str(user.id))

        # update embed
        embed = self.fill_subject_embed(embed, review, average)
        embed.set_footer(text=footer_text, icon_url=embed.footer.icon_url)
        await reaction.message.edit(embed=embed)

        await self._remove_reaction(reaction, user)

    ##
    ## Helper functions
    ##
    async def _remove_reaction(self, reaction, user):
        try:
            await reaction.remove(user)
        except Exception:
            pass

    ##
    ## Logic
    ##

    def fill_subject_embed(self, embed: discord.Embed, review: object,
                           average: float) -> discord.Embed:
        # reset any previous
        embed.clear_fields()

        # add content
        # fmt: off
        name = self.bot.get_user(int(review.discord_id)) or self.text.get(
            "embed", "no_user")
        if review.anonym:
            name = self.text.get("embed", "anonymous")

        embed.add_field(
            inline=False,
            name=self.text.get("embed", "num", num=str(review.id)),
            value=self.text.get("embed", "average", num=f"{average:.1f}"),
        )
        embed.add_field(name=name, value=review.date)
        embed.add_field(name=self.text.get("embed", "mark"), value=review.tier)
        embed.add_field(
            inline=False,
            name=self.text.get("embed", "text"),
            value=review.text_review[:1024],
        )
        if len(review.text_review) > 1024:
            embed.add_field(
                inline=False,
                name="\u200b",
                value=review.text_review[1024:],
            )
        # fmt: on

        embed.add_field(name="👍",
                        value=f"{repo_r.get_votes_count(review.id, True)}")
        embed.add_field(name="👎",
                        value=f"{repo_r.get_votes_count(review.id, False)}")

        return embed

    async def add_review(self, ctx, subject: str, mark: int, text: str,
                         anonymous: bool):
        """Add and return review"""
        if mark < 1 or mark > 5:
            return await ctx.send(self.text.get("wrong_mark"))

        # check if subject is in database
        db_subject = repo_s.get(subject)
        if db_subject is None:
            await ctx.send(self.text.get("no_subject"))
            return

        if text is None or not len(text):
            await ctx.send(self.text.get("no_text"))
            return

        past_review = repo_r.get_review_by_author_subject(
            ctx.author.id, subject)

        if past_review is None:
            # add
            result = repo_r.add_review(ctx.author.id, subject, mark, anonymous,
                                       text)
        else:
            # update
            result = repo_r.update_review(past_review.id, mark, anonymous,
                                          text)

        return result
Exemplo n.º 17
0
class Warden(rubbercog.Rubbercog):
    """Repost detector"""

    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

    @commands.check(acl.check)
    @commands.group()
    async def scan(self, ctx):
        """Scan for reposts"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
    @commands.bot_has_permissions(read_message_history=True)
    @scan.command(name="history")
    async def scan_history(self, ctx, limit: int):
        """Scan current channel for images and save them as hashes.

        limit: How many messages should be scanned. Negative to scan all.
        """
        if limit < 0:
            limit = None

        async with ctx.typing():
            messages = await ctx.channel.history(limit=limit).flatten()

        status = await ctx.send(self.text.get("scan_history", "title", count=len(messages)))

        await asyncio.sleep(1)

        ctr_nofile: int = 0
        ctr_hashes: int = 0
        now = time.time()
        for i, message in enumerate(messages, 1):
            if i % 20 == 0:
                await status.edit(
                    content=self.text.get(
                        "scan_history",
                        "scanning",
                        count=i,
                        total=len(messages),
                        percent="{:.1f}".format(i / len(messages) * 100),
                        hashes=ctr_hashes,
                    )
                )

            if not len(message.attachments):
                ctr_nofile += 1
                continue

            hashes = [x async for x in self.save_hashes(message)]
            ctr_hashes += len(hashes)

        await status.edit(
            content=self.text.get(
                "scan_history",
                "complete",
                messages=len(messages),
                hashes=ctr_hashes,
                seconds="{:.1f}".format(time.time() - now),
            )
        )

    @commands.check(acl.check)
    @scan.command(name="compare", aliases=["messages"])
    async def scan_compare(self, ctx, messages: commands.Greedy[discord.Message]):
        """Display hashes of given messages.

        messages: Space separated list of messages.
        """
        text = []

        for message in messages:
            db_images = repo_i.get_by_message(message.id)
            if not len(db_images):
                continue

            text.append(self.text.get("compare", "header", message_id=message.id))
            for db_image in db_images:
                text.append(self.text.get("compare", "line", hash=db_image.dhash[2:]))
            text.append("")

        if not len(text):
            return await ctx.send("compare", "not_found")

        await ctx.send("\n".join(text))

    #

    def _in_repost_channel(self, message: discord.Message) -> bool:
        if message.channel.id not in self.config.get("deduplication channels"):
            return False
        if message.attachments is None or not len(message.attachments):
            return False
        if message.author.bot:
            return False
        return True

    @commands.Cog.listener()
    async def on_message(self, message: discord.Message):
        if self._in_repost_channel(message):
            await self.check_message(message)

    @commands.Cog.listener()
    async def on_raw_message_delete(self, payload: discord.RawMessageDeleteEvent):
        repo_i.delete_by_message(payload.message_id)

    @commands.Cog.listener()
    async def on_message_delete(self, message: discord.Message):
        if not self._in_repost_channel(message):
            return

        # try to detect and delete repost embed
        messages = await message.channel.history(
            after=message, limit=3, oldest_first=True
        ).flatten()
        for report in messages:
            if not report.author.bot:
                continue
            if len(report.embeds) != 1 or type(report.embeds[0].footer.text) != str:
                continue
            if str(message.id) != report.embeds[0].footer.text.split(" | ")[1]:
                continue

            try:
                await report.delete()
            except discord.errors.HTTPException as e:
                await self.console.error(message, "Could not delete repost report.", e)
            break

    @commands.Cog.listener()
    async def on_reaction_add(self, reaction, user):
        """Handle 'This is a repost' report.

        The footer contains reposter's user ID and repost message id.
        """
        if reaction.message.channel.id not in self.config.get("deduplication channels"):
            return
        if user.bot:
            return
        if not reaction.message.author.bot:
            return
        emoji = str(reaction.emoji)
        if emoji != "❎":
            return

        try:
            repost_message_id = int(reaction.message.embeds[0].footer.text.split(" | ")[1])
            repost_message = await reaction.message.channel.fetch_message(repost_message_id)
        except discord.errors.HTTPException:
            return await self.console.error(reaction.message, "Could not find the repost message.")

        for report_reaction in reaction.message.reactions:
            if str(report_reaction) != "❎":
                continue

            if (
                emoji == "❎"
                and str(report_reaction) == "❎"
                and report_reaction.count > self.config.get("not duplicate limit")
            ):
                # remove bot's reaction, it is not a repost
                try:
                    await repost_message.remove_reaction("♻️", self.bot.user)
                    await repost_message.remove_reaction("🤷🏻", self.bot.user)
                    await repost_message.remove_reaction("🤔", self.bot.user)
                except discord.errors.HTTPException as exc:
                    return await self.console.error(
                        reaction.message, "Could not remove bot's reaction.", exc
                    )
                return await utils.delete(reaction.message)

    #

    async def save_hashes(self, message: discord.Message):
        for attachment in message.attachments:
            if attachment.size > self.config.get("max_size") * 1024:
                continue

            extension = attachment.filename.split(".")[-1].lower()
            if extension not in ("jpg", "jpeg", "png", "webp", "gif"):
                continue

            fp = BytesIO()

            await attachment.save(fp)
            try:
                image = Image.open(fp)
            except OSError:
                continue

            h = dhash.dhash_int(image)
            repo_i.add_image(
                channel_id=message.channel.id,
                message_id=message.id,
                attachment_id=attachment.id,
                dhash=str(hex(h)),
            )
            yield h

    async def check_message(self, message: discord.Message):
        """Check if message contains duplicate image."""
        image_hashes = [x async for x in self.save_hashes(message)]

        if len(message.attachments) > len(image_hashes):
            await message.add_reaction("▶")
            await asyncio.sleep(2)
            await message.remove_reaction("▶", self.bot.user)

        duplicates = {}
        all_images = None

        for image_hash in image_hashes:
            # try to look up hash directly
            images = repo_i.get_hash(str(hex(image_hash)))
            for image in images:
                # skip current message
                if image.message_id == message.id:
                    continue
                # add to duplicates
                duplicates[image] = 0
                await self.console.debug(message, "Full dhash match found.")
                break

            # move on to the next hash
            continue

            # full match not found, iterate over whole database
            if all_images is None:
                all_images = repo_i.get_all()

            minimal_distance = 128
            duplicate = None
            for image in all_images:
                # skip current image
                if image.message_id == message.id:
                    continue

                # do the comparison
                db_image_hash = int(image.dhash, 16)
                distance = dhash.get_num_bits_different(db_image_hash, image_hash)
                if distance < minimal_distance:
                    duplicate = image
                    minimal_distance = distance

            if minimal_distance < self.limit_soft:
                duplicates[duplicate] = minimal_distance

        for image_hash, distance in duplicates.items():
            await self.report_duplicate(message, image_hash, distance)

    async def report_duplicate(self, message: discord.Message, original: object, distance: int):
        """Send report.

        message: The new message containing attachment repost.
        original: The original attachment.
        distance: Hamming distance between the original and repost.
        """
        if distance <= self.limit_full:
            level = "title_full"
            await message.add_reaction("♻️")
        elif distance <= self.limit_hard:
            level = "title_hard"
            await message.add_reaction("🤔")
        else:
            level = "title_soft"
            await message.add_reaction("🤷🏻")

        similarity = "{:.1f} %".format((1 - distance / 128) * 100)
        timestamp = utils.id_to_datetime(original.attachment_id).strftime("%Y-%m-%d %H:%M:%S")

        try:
            original_channel = message.guild.get_channel(original.channel_id)
            original_message = await original_channel.fetch_message(original.message_id)
            author = discord.utils.escape_markdown(original_message.author.display_name)
            link = f"[**{author}**, {timestamp}]({original_message.jump_url})"
        except discord.errors.NotFound:
            link = "404 " + emote.sad

        description = self.text.get(
            "report",
            "description",
            name=discord.utils.escape_markdown(message.author.display_name),
            similarity=similarity,
        )
        embed = discord.Embed(
            title=self.text.get("report", level),
            color=config.color,
            description=description,
        )
        embed.add_field(
            name=self.text.get("report", "original"),
            value=link,
            inline=False,
        )
        embed.add_field(
            name=self.text.get("report", "help"),
            value=self.text.get(
                "report",
                "help_content",
                limit=self.config.get("not duplicate limit"),
            ),
            inline=False,
        )
        embed.set_footer(text=f"{message.author.id} | {message.id}")
        report = await message.reply(embed=embed)
        await report.add_reaction("❎")
Exemplo n.º 18
0
class Howto(rubbercog.Rubbercog):
    """See information about school related topics"""
    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()

    ##
    ## Commands
    ##

    @commands.check(check.is_verified)
    @commands.command()
    async def howto(self, ctx, *args):
        """See information about school related topics"""
        args = [x for x in args if len(x) > 0]

        name = "\u200b"
        content = self.data

        # get the requested item
        for directory in args:
            if directory not in content.keys():
                await utils.delete(ctx)
                raise HowtoException()
            name = directory
            content = content.get(name)

        title = "{prefix}{command} {args}".format(
            prefix=config.prefix,
            command=ctx.command.qualified_name,
            args=self.sanitise(" ".join(args), limit=30),
        )
        embed = self.embed(ctx=ctx, title=title)

        # fill the embed
        if isinstance(content, OrderedDict):
            embed = self._add_listing(embed, name, content)
        elif isinstance(content, list):
            embed = self._add_list_content(embed, name, content)
        else:
            embed = self._add_str_content(embed, name, str(content))

        # done
        await ctx.send(embed=embed)
        await utils.delete(ctx)
        await utils.room_check(ctx)

    ##
    ## Helper functions
    ##

    def _add_listing(self, embed: discord.Embed, name: str,
                     directory: OrderedDict):
        """List items in howto directory

        name: directory name
        directory: howto category (HJSON {})
        """
        value = "\n".join(["→ " + x for x in directory.keys()])
        embed.add_field(name=name, value=value)
        return embed

    def _add_list_content(self, embed: discord.Embed, name: str, item: list):
        """Add item content to embed

        name: item name
        item: list
        """
        for i, step in enumerate(item):
            embed.add_field(name=f"#{i+1}", value=step, inline=False)
        return embed

    def _add_str_content(self, embed: discord.Embed, name: str, item: str):
        """Add item content to embed

        name: item name
        item: string
        """
        embed.add_field(name="\u200b", value=item)
        return embed

    ##
    ## 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
        elif isinstance(error, HowtoException):
            await self.output.warning(
                ctx, self.text.get("HowtoException",
                                   mention=ctx.author.mention))
Exemplo n.º 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.")
Exemplo n.º 20
0
class ACL(rubbercog.Rubbercog):
    """Permission control"""
    def __init__(self, bot):
        super().__init__(bot)

        self.text = CogText("acl")

    ##
    ## Commands
    ##

    @commands.guild_only()
    @commands.check(acl.check)
    @commands.group(name="acl")
    async def acl_(self, ctx):
        """Permission control"""
        await utils.send_help(ctx)

    ## Groups

    @commands.check(acl.check)
    @acl_.group(name="group", aliases=["g"])
    async def acl_group(self, ctx):
        """ACL group control"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @acl_group.command(name="list", aliases=["l"])
    async def acl_group_list(self, ctx):
        """List ACL groups"""
        groups = repo_a.get_groups(ctx.guild.id)

        if not len(groups):
            return await ctx.send(self.text.get("group_list", "nothing"))

        # compute relationships between groups
        relationships = {}
        for group in groups:
            if group.name not in relationships:
                relationships[group.name] = []
            if group.parent is not None:
                # add to parent's list
                if group.parent not in relationships:
                    relationships[group.parent] = []
                relationships[group.parent].append(group)

        # add relationships to group objects
        for group in groups:
            group.children = relationships[group.name]
            group.level = 0

        def bfs(queue) -> list:
            visited = []
            while queue:
                group = queue.pop(0)
                if group not in visited:
                    visited.append(group)
                    # build levels for intendation
                    for child in group.children:
                        child.level = group.level + 1
                    queue = group.children + queue
            return visited

        result = ""
        template = "{group_id:<2} {name:<20} {role:<18}"
        for group in bfs(groups):
            result += "\n" + template.format(
                group_id=group.id,
                name="  " * group.level + group.name,
                role=group.role_id,
            )

        await ctx.send("```" + result + "```")

    @commands.check(acl.check)
    @acl_group.command(name="get", aliases=["info"])
    async def acl_group_get(self, ctx, name: str):
        """Get ACL group"""
        result = repo_a.get_group(ctx.guild.id, name)
        if result is None:
            return await ctx.send(self.text.get("group_get", "nothing"))

        await ctx.send(embed=self.get_group_embed(ctx, result))

    @commands.check(acl.check)
    @acl_group.command(name="add", aliases=["a"])
    async def acl_group_add(self, ctx, name: str, parent: str, role_id: int):
        """Add ACL group

        name: string matching `[a-zA-Z-]+`
        parent: parent group name
        role_id: Discord role ID

        To unlink the group from any parents, set parent to "".
        To set up virtual group with no link to discord roles, set role_id to 0.
        """
        regex = r"[a-zA-Z-]+"
        if re.fullmatch(regex, name) is None:
            return await ctx.send(self.text.get("group_regex", regex=regex))

        if len(parent) == 0:
            parent = None
        result = repo_a.add_group(ctx.guild.id, name, parent, role_id)
        await ctx.send(embed=self.get_group_embed(ctx, result))
        await self.event.sudo(ctx, f"ACL group added: **{result.name}**.")

    @commands.check(acl.check)
    @acl_group.command(name="edit", aliases=["e"])
    async def acl_group_edit(self, ctx, name: str, param: str, value):
        """Edit ACL group

        name: string matching `[a-zA-Z-]+`

        Options:
        name, string matching `[a-zA-Z-]+`
        parent, parent group name
        role_id, Discord role ID

        To unlink the group from any parents, set parent to "".
        To set up virtual group with no link to discord roles, set role_id to 0.
        """
        if param == "name":
            regex = r"[a-zA-Z-]+"
            if re.fullmatch(regex, value) is None:
                return await ctx.send(self.text.get("group_regex",
                                                    regex=regex))
            result = repo_a.edit_group(ctx.guild.id, name=name, new_name=value)
        elif param == "parent":
            result = repo_a.edit_group(ctx.guild.id, name=name, parent=value)
        elif param == "role_id":
            result = repo_a.edit_group(ctx.guild.id,
                                       name=name,
                                       role_id=int(value))
        else:
            raise discord.BadArgument()

        await ctx.send(embed=self.get_group_embed(ctx, result))
        await self.event.sudo(
            ctx, f"ACL group **{result.name}** updated: **{param}={value}**.")

    @commands.check(acl.check)
    @acl_group.command(name="remove", aliases=["delete", "r", "d"])
    async def acl_group_remove(self, ctx, name: str):
        """Remove ACL group"""
        result = repo_a.delete_group(ctx.guild.id, name)
        await ctx.send(embed=self.get_group_embed(ctx, result))
        await self.event.sudo(
            ctx,
            f"ACL group removed: **{result.get('name')}** (#{result.get('id')})."
        )

    ## Rules

    @commands.guild_only()
    @commands.check(acl.check)
    @acl_.group(name="rule")
    async def acl_rule(self, ctx):
        """Command control"""
        await utils.send_help(ctx)

    @commands.guild_only()
    @commands.check(acl.check)
    @acl_rule.command(name="get", aliases=["info"])
    async def acl_rule_get(self, ctx, command: str):
        """See command's policy"""
        rule = repo_a.get_rule(ctx.guild.id, command)
        if rule is None:
            return await ctx.send(self.text.get("rule_get", "nothing"))

        embed = self.get_rule_embed(ctx, rule)
        await ctx.send(embed=embed)

    @commands.guild_only()
    @commands.check(acl.check)
    @acl_rule.command(name="import")
    async def acl_rule_import(self, ctx, mode: str):
        """Import command rules

        mode: `append` or `replace`
        """
        if len(ctx.message.attachments) != 1:
            return await ctx.send(self.text.get("import", "wrong_file"))
        if not ctx.message.attachments[0].filename.endswith("json"):
            return await ctx.send(self.text.get("import", "wrong_json"))

        if mode not in ("append", "replace"):
            return await ctx.send(self.text.get("import", "wrong_mode"))

        # Download
        data_file = tempfile.TemporaryFile()
        await ctx.message.attachments[0].save(data_file)
        data_file.seek(0)
        try:
            data = json.load(data_file)
        except json.decoder.JSONDecodeError as exc:
            return await ctx.send(
                self.text.get("import", "wrong_json") + f"\n> `{str(exc)}`")

        new, edited, rejected = await self._import_json(ctx, data, mode=mode)
        await ctx.send(
            self.text.get("import",
                          "imported",
                          new=len(new),
                          edited=len(edited)))

        result = ""
        for (msg, command, reason) in rejected:
            result += "\n> " + self.text.get(
                "import", msg, command=command, reason=reason)
            if len(result) > 1900:
                await ctx.send(result)
                result = ""
        if len(result):
            await ctx.send(result)

        data_file.close()
        await self.event.sudo(
            ctx, f"Added {len(new)} and edited {len(edited)} ACL rules.")

    @commands.guild_only()
    @commands.check(acl.check)
    @acl_rule.command(name="export")
    async def acl_rule_export(self, ctx):
        """Export command rules"""
        filename = f"acl_{ctx.guild.id}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"

        rules = repo_a.get_rules(ctx.guild.id)
        export = dict()

        for rule in rules:
            export[rule.command] = {
                "default": rule.default,
                "group allow": [g.group.name for g in rule.groups if g.allow],
                "group deny":
                [g.group.name for g in rule.groups if not g.allow],
                "user allow": [u.user_id for u in rule.users if u.allow],
                "user deny": [u.user_id for u in rule.users if not u.allow],
            }

        file = tempfile.TemporaryFile(mode="w+")
        json.dump(export, file, indent="\t", sort_keys=True)
        file.seek(0)

        await ctx.send(file=discord.File(fp=file, filename=filename))
        file.close()
        await self.event.sudo(ctx, "ACL rules exported.")

    @commands.check(acl.check)
    @acl_rule.command(name="flush", ignore_extra=False)
    async def acl_rule_flush(self, ctx):
        """Flush all the command rules."""
        count = repo_a.delete_rules(ctx.guild.id)
        await ctx.send(self.text.get("rule_flush", count=count))
        await self.event.sudo(ctx, "ACL rules flushed.")

    ##
    ## Logic
    ##

    def get_rule_embed(self, ctx, rule: ACL_rule) -> discord.Embed:
        embed = discord.Embed(
            ctx=ctx,
            title=self.text.get("rule_get", "title", command=rule.command),
        )
        embed.add_field(
            name=self.text.get("rule_get", "default"),
            value=str(rule.default),
            inline=False,
        )
        if len([g for g in rule.groups if g.allow is True]):
            embed.add_field(
                name=self.text.get("rule_get", "group_allow"),
                value=", ".join(
                    [g.group.name for g in rule.groups if g.allow is True]),
            )
        if len([g for g in rule.groups if g.allow is False]):
            embed.add_field(
                name=self.text.get("rule_get", "group_deny"),
                value=", ".join(
                    [g.group.name for g in rule.groups if g.allow is False]),
            )
        if len([u for u in rule.users if u.allow is True]):
            embed.add_field(
                name=self.text.get("rule_get", "user_allow"),
                value=", ".join(
                    [(self.bot.get_user(u.user_id) or str(u.user_id))
                     for u in rule.users if u.allow is True], ),
            )
        if len([u for u in rule.users if u.allow is False]):
            embed.add_field(
                name=self.text.get("rule_get", "user_deny"),
                value=", ".join([(self.bot.get_user(u.user_id)
                                  or str(u.user_id)) for u in rule.users
                                 if u.allow is False]),
            )

        return embed

    def get_group_embed(self, ctx: commands.Context,
                        group: Tuple[ACL_group, dict]) -> discord.Embed:
        if type(group) == ACL_group:
            group = group.mirror()

        role = ctx.guild.get_role(group["role_id"])

        embed = discord.Embed(
            ctx=ctx,
            title=self.text.get("group_get", "title", name=group["name"]),
        )
        if role is not None:
            embed.add_field(
                name=self.text.get("group_get", "discord"),
                value=f"{role.name} ({role.id})",
                inline=False,
            )
        if group["parent"] is not None:
            embed.add_field(
                name=self.text.get("group_get", "parent"),
                value=group["parent"],
                inline=False,
            )

        return embed

    async def _import_json(
            self, ctx: commands.Context, data: dict,
            mode: str) -> Tuple[List[str], List[str], List[Tuple[str, str]]]:
        """Import JSON rules

        Returns
        -------
        list: New commands
        list: Altered commands
        list: Rejected commands as (skip/warn, command, reason) tuple
        """
        result_new = list()
        result_alt = list()
        result_rej = list()

        # check data
        for command, attributes in data.items():
            # CHECK
            bad: bool = False

            # bool
            if "default" in attributes:
                if attributes["default"] not in (True, False):
                    result_rej.append((
                        "skip",
                        command,
                        self.text.get("import", "bad_bool", key="default"),
                    ))
                    bad = True
            else:
                attributes["default"] = False

            if "direct" in attributes:
                if attributes["direct"] not in (True, False):
                    result_rej.append((
                        "skip",
                        command,
                        self.text.get("import", "bad_bool", key="direct"),
                    ))
                    bad = True
            else:
                attributes["direct"] = False

            # lists
            for keyword in ("group allow", "group deny", "user allow",
                            "user deny"):
                if keyword in attributes:
                    if type(attributes[keyword]) != list:
                        result_rej.append((
                            "skip",
                            command,
                            self.text.get("import", "bad_list", key=keyword),
                        ))
                        bad = True
                else:
                    attributes[keyword] = list()

            # groups
            for keyword in ("group allow", "group deny"):
                for group in attributes[keyword]:
                    if type(group) != str:
                        result_rej.append((
                            "skip",
                            command,
                            self.text.get("import", "bad_text", key=group),
                        ))
                        bad = True

            # users
            for keyword in ("user allow", "user deny"):
                for user in attributes[keyword]:
                    if type(user) != int:
                        result_rej.append((
                            "skip",
                            command,
                            self.text.get("import", "bad_int", key=user),
                        ))
                        bad = True

            if bad:
                # do not proceed to adding
                continue

            # ADD
            try:
                repo_a.add_rule(ctx.guild.id, command, attributes["default"])
                result_new.append(command)
            except acl_repo.Duplicate:
                if mode == "replace":
                    repo_a.delete_rule(ctx.guild.id, command)
                    repo_a.add_rule(ctx.guild.id, command,
                                    attributes["default"])
                    result_alt.append(command)
                else:
                    result_rej.append(
                        ("skip", command, self.text.get("import",
                                                        "duplicate")))
                    continue

            for group in attributes["group allow"]:
                try:
                    repo_a.add_group_constraint(ctx.guild.id,
                                                command,
                                                group,
                                                allow=True)
                except acl_repo.NotFound:
                    result_rej.append((
                        "warn",
                        command,
                        self.text.get(
                            "import",
                            "no_group",
                            name=group,
                        ),
                    ))
            for group in attributes["group deny"]:
                try:
                    repo_a.add_group_constraint(ctx.guild.id,
                                                command,
                                                group,
                                                allow=False)
                except acl_repo.NotFound:
                    result_rej.append((
                        "warn",
                        command,
                        self.text.get(
                            "import",
                            "no_group",
                            name=group,
                        ),
                    ))
            for user in attributes["user allow"]:
                repo_a.add_user_constraint(ctx.guild.id,
                                           command,
                                           user,
                                           allow=True)
            for user in attributes["user deny"]:
                repo_a.add_user_constraint(ctx.guild.id,
                                           command,
                                           user,
                                           allow=False)

        return result_new, result_alt, result_rej
Exemplo n.º 21
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()
Exemplo n.º 22
0
class Points(rubbercog.Rubbercog):
    """Get points by having conversations"""
    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()

    ##
    ## Commands
    ##

    @commands.group(name="points", aliases=["body"])
    async def points(self, ctx):
        """Points? Points!"""
        await utils.send_help(ctx)

    @points.command(name="get", aliases=["gde", "me", "stalk"])
    async def points_get(self, ctx, member: discord.Member = None):
        """Get user points"""
        if member is None:
            member = ctx.author

        result = repo_p.get(member.id)
        position = repo_p.getPosition(result.points)

        if member.id == ctx.author.id:
            text = self.text.get("me", points=result.points, position=position)
        else:
            text = self.text.get(
                "stalk",
                display_name=self.sanitise(member.display_name),
                points=result.points,
                position=position,
            )

        await ctx.send(text)
        await utils.room_check(ctx)

    @points.command(name="leaderboard", aliases=["🏆"])
    async def points_leaderboard(self, ctx):
        """Points leaderboard"""
        embed = self.embed(
            ctx=ctx,
            title=self.text.get("embed", "title") +
            self.text.get("embed", "desc_suffix"),
            description=self.text.get("embed", "desc_description"),
        )
        users = repo_p.getUsers("desc",
                                limit=self.config.get("board"),
                                offset=0)
        value = self._getBoard(ctx.author, users)
        embed.add_field(
            name=self.text.get("embed", "desc_0",
                               num=self.config.get("board")),
            value=value,
            inline=False,
        )

        # if the user is not present, add them to second field
        if ctx.author.id not in [u.user_id for u in users]:
            author = repo_p.get(ctx.author.id)

            embed.add_field(
                name=self.text.get("embed", "user"),
                value="`{points:>8}` … {name}".format(
                    points=author.points,
                    name="**" + self.sanitise(ctx.author.display_name) + "**"),
                inline=False,
            )

        message = await ctx.send(embed=embed)
        await message.add_reaction("⏪")
        await message.add_reaction("◀")
        await message.add_reaction("▶")
        await utils.room_check(ctx)

    @points.command(name="loserboard", aliases=["💩"])
    async def points_loserboard(self, ctx):
        """Points loserboard"""
        embed = self.embed(
            ctx=ctx,
            title=self.text.get("embed", "title") +
            self.text.get("embed", "asc_suffix"),
            description=self.text.get("embed", "asc_description"),
        )
        users = repo_p.getUsers("asc",
                                limit=self.config.get("board"),
                                offset=0)
        value = self._getBoard(ctx.author, users)
        embed.add_field(name=self.text.get("embed",
                                           "asc_0",
                                           num=self.config.get("board")),
                        value=value)

        # if the user is not present, add them to second field
        if ctx.author.id not in [u.user_id for u in users]:
            author = repo_p.get(ctx.author.id)

            embed.add_field(
                name=self.text.get("embed", "user"),
                value="`{points:>8}` … {name}".format(
                    points=author.points,
                    name="**" + self.sanitise(ctx.author.display_name) + "**"),
                inline=False,
            )

        message = await ctx.send(embed=embed)
        await message.add_reaction("⏪")
        await message.add_reaction("◀")
        await message.add_reaction("▶")
        await utils.room_check(ctx)

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_message(self, message):
        """Add points on message"""
        if message.author.bot:
            return

        # Before the database is updated, only count primary guild
        if message.guild.id not in (config.guild_id, config.slave_id):
            return

        now = datetime.datetime.now()

        if (str(message.author.id) in self.stats_message and
            (now - self.stats_message[str(message.author.id)]).total_seconds()
                < self.timer_message):
            return

        value = random.randint(self.limits_message[0], self.limits_message[1])
        self.stats_message[str(message.author.id)] = now
        repo_p.increment(message.author.id, value)

    @commands.Cog.listener()
    async def on_reaction_add(self, reaction, user):
        """Handle board scrolling"""
        if user.bot:
            return

        # add points
        now = datetime.datetime.now()
        if (str(user.id) not in self.stats_reaction
                or (now - self.stats_reaction[str(user.id)]).total_seconds() >=
                self.timer_reaction):
            value = random.randint(self.limits_reaction[0],
                                   self.limits_reaction[1])
            self.stats_reaction[str(user.id)] = now
            repo_p.increment(user.id, value)

        if str(reaction) not in ("⏪", "◀", "▶"):
            return

        # fmt: off
        if len(reaction.message.embeds) != 1 \
        or type(reaction.message.embeds[0].title) != str \
        or not reaction.message.embeds[0].title.startswith(self.text.get("embed", "title")):
            return
        # fmt: on

        embed = reaction.message.embeds[0]

        # get ordering
        if embed.title.endswith(self.text.get("embed", "desc_suffix")):
            order = "desc"
        else:
            order = "asc"

        # get current offset
        if ", " in embed.fields[0].name:
            offset = int(embed.fields[0].name.split(" ")[-1]) - 1
        else:
            offset = 0

        # get new offset
        if str(reaction) == "⏪":
            offset = 0
        elif str(reaction) == "◀":
            offset -= self.config.get("board")
        elif str(reaction) == "▶":
            offset += self.config.get("board")

        if offset < 0:
            return await utils.remove_reaction(reaction, user)

        users = repo_p.getUsers(order,
                                limit=self.config.get("board"),
                                offset=offset)
        value = self._getBoard(user, users)
        if not value:
            # offset too big
            return await utils.remove_reaction(reaction, user)

        if offset:
            name = self.text.get("embed",
                                 order + "_n",
                                 num=self.config.get("board"),
                                 offset=offset + 1)
        else:
            name = self.text.get("embed",
                                 order + "_0",
                                 num=self.config.get("board"))
        embed.clear_fields()
        embed.add_field(name=name, value=value, inline=False)

        # if the user is not present, add them to second field
        if user.id not in [u.user_id for u in users]:
            author = repo_p.get(user.id)

            embed.add_field(
                name=self.text.get("embed", "user"),
                value="`{points:>8}` … {name}".format(
                    points=author.points,
                    name="**" + self.sanitise(user.display_name) + "**"),
                inline=False,
            )

        await reaction.message.edit(embed=embed)
        await utils.remove_reaction(reaction, user)

    ##
    ## Helper functions
    ##

    def _getBoard(self,
                  author: Union[discord.User, discord.Member],
                  users: list,
                  offset: int = 0) -> str:
        result = []
        template = "`{points:>8}` … {name}"
        for db_user in users:
            user = self.bot.get_user(db_user.user_id)
            if user and user.display_name:
                name = discord.utils.escape_markdown(user.display_name)
            else:
                name = self.text.get("unknown")

            if db_user.user_id == author.id:
                name = "**" + name + "**"

            result.append(template.format(points=db_user.points, name=name))
        return "\n".join(result)

    ##
    ## Tasks
    ##

    @tasks.loop(seconds=120.0)
    async def cleanup(self):
        delete = []
        for uid, time in self.stats_message.items():
            if (datetime.datetime.now() -
                    time).total_seconds() >= self.timer_message:
                delete.append(uid)
        for uid in delete:
            self.stats_message.pop(uid)
        delete = []
        for uid, time in self.stats_reaction.items():
            if (datetime.datetime.now() -
                    time).total_seconds() >= self.timer_reaction:
                delete.append(uid)
        for uid in delete:
            self.stats_reaction.pop(uid)
Exemplo n.º 23
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)
Exemplo n.º 24
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

    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:
            await self.event.user(f"{after} updated",
                                  "Not an animal (default avatar).")
            return

        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.event.user(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.event.user(f"{after} reverified",
                                      "Skipping (unverify).")
                return

        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

        message = await self.getChannel().fetch_message(payload.message_id)
        # fmt: off
        if not message or len(message.embeds) != 1 \
        or message.embeds[0].title != self.text.get("title"):
            return
        # fmt: on

        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)
        animal = self.getChannel().guild.get_member(animal_id)

        if animal is None:
            await self.console.error(
                "animals",
                f"Could not find member with ID {animal_id}. Vote aborted.")
            await self.event.user(
                "animals", f"Could not find user {animal_id}, vote aborted.")
            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.info(animal,
                                    "Avatar has changed since. Vote aborted.")
            return await utils.delete(message)

        for r in message.reactions:
            if r.emoji == "☑️" and r.count > self.config.get("limit"):
                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),
                        ))
                else:
                    # member is an animal and has not been before
                    try:
                        await animal.add_roles(self.getRole())
                        await self.event.user(animal, "New animal!")
                        await self.getChannel().send(
                            self.text.get("result",
                                          "no_yes",
                                          mention=animal.mention))
                    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"):
                if self.getRole() in animal.roles:
                    # member is not an animal and has been before
                    try:
                        await animal.remove_roles(self.getRole())
                        await self.event.user(animal, "Animal left.")
                        await self.getChannel().send(
                            self.text.get("result",
                                          "yes_no",
                                          mention=animal.mention))
                    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))
                break
        else:
            return
        await utils.delete(message)

    ##
    ## 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.event.user(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
Exemplo n.º 25
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
Exemplo n.º 26
0
class Voice(rubbercog.Rubbercog):
    """Manage voice channels"""
    def __init__(self, bot):
        super().__init__(bot)

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

        self.locked = []

    ##
    ## Commands
    ##

    @commands.guild_only()
    @commands.check(check.is_in_voice)
    @commands.bot_has_permissions(manage_channels=True, manage_messages=True)
    @commands.group(name="voice")
    async def voice(self, ctx):
        """Manage voice channels"""
        await utils.send_help(ctx)

    @voice.command(name="lock", aliases=["close"])
    async def voice_lock(self, ctx):
        """Make current voice channel invisible"""
        voice = self.users_voice_channel(ctx)
        # Lock
        await voice.set_permissions(self.getVerifyRole(), overwrite=None)
        if ctx.channel.id not in self.locked:
            self.locked.append(ctx.channel.id)
        # Report
        await ctx.send(self.text.get("locked", name=self.sanitise(voice.name)),
                       delete_after=10)
        await utils.delete(ctx.message)
        await self.event.user(ctx, f"Channel locked: **{voice.name}**.")

    @voice.command(name="unlock", aliases=["open"])
    async def voice_unlock(self, ctx):
        """Make current voice channel visible"""
        voice = self.users_voice_channel(ctx)
        # Unlock
        await voice.set_permissions(self.getVerifyRole(), view_channel=True)
        if ctx.channel.id in self.locked:
            self.locked.remove(ctx.channel.id)
        # Report
        await ctx.send(self.text.get("unlocked",
                                     name=self.sanitise(voice.name)),
                       delete_after=10)
        await utils.delete(ctx.message)
        await self.event.user(ctx, f"Channel unlocked: **{voice.name}**.")

    ##
    ## Listeners
    ##

    @commands.Cog.listener()
    async def on_voice_state_update(self, member, beforeState, afterState):
        """Detect changes in voice channels"""

        # get voice objects
        before = beforeState.channel
        after = afterState.channel

        # Do not act if no one has left or joined
        if before == after or (before is None and after is None):
            return
        # Do not act if the action is not on main guild
        if before not in self.getGuild(
        ).channels and after not in self.getGuild().channels:
            return

        # Get voice-no-mic channel
        nomic = self.getGuild().get_channel(config.channel_nomic)

        # Manage channel overwrites
        if before is None:
            # User joined
            await nomic.set_permissions(member, view_channel=True)
            try:
                await after.set_permissions(member, view_channel=True)
            except Exception as e:
                await self.console.warning(
                    member, f"Could not add overwrite to {after}.", e)

            # Send welcome message
            await nomic.send(
                self.text.get("welcome",
                              nickname=self.sanitise(member.display_name),
                              prefix=config.prefix),
                delete_after=30,
            )

        elif after is None:
            # User left
            await nomic.set_permissions(member, overwrite=None)
            try:
                await before.set_permissions(member, overwrite=None)
            except Exception as e:
                await self.console.warning(
                    member, f"Could not remove overwrite from {before}.", e)

        else:
            # User moved
            try:
                await before.set_permissions(member, overwrite=None)
            except Exception as e:
                await self.console.warning(
                    member, f"Could not remove overwrite from {before}.", e)
            try:
                await after.set_permissions(member, view_channel=True)
            except Exception as e:
                await self.console.warning(
                    member, f"Could not add overwrite to {before}.", e)

        # Do cleanup
        await self.cleanup_channels()

    ##
    ## Logic
    ##

    async def cleanup_channels(self):
        """Remove empty voice channels"""
        category = self.getGuild().get_channel(config.channel_voices)
        nomic = self.getGuild().get_channel(config.channel_nomic)

        empty = []
        for voice in category.voice_channels:
            if len(voice.members) == 0:
                empty.append(voice)

        if len(empty) == 0:
            # Need to add one
            voice = await self.getGuild().create_voice_channel(
                name=self.gen_channel_name(), category=category)
            await voice.set_permissions(self.getVerifyRole(),
                                        view_channel=True)
            return

        # Delete all except the last one
        for voice in empty[:-1]:
            try:
                await voice.delete()
            except discord.NotFound as e:
                await self.console.warning("Voice cleanup",
                                           f"Channel {voice} not found.", e)

        # Make sure the empty is writable
        await empty[-1].set_permissions(self.getVerifyRole(),
                                        view_channel=True)

        if len(category.voice_channels) == 1:
            # No one inside, can safely wipe no-mic channel
            await nomic.purge()
            # Just to make sure
            self.locked = []

    ##
    ## Helper functions
    ##

    def users_voice_channel(self, ctx: commands.Context):
        return ctx.author.voice.channel

    def gen_channel_name(self) -> str:
        adjectives = self.config.get("adjectives")
        nouns = self.config.get("nouns")
        return "{} {}".format(random.choice(adjectives), random.choice(nouns))
Exemplo n.º 27
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__))
Exemplo n.º 28
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.cooldown(rate=5, per=20.0, type=commands.BucketType.user)
    @commands.command()
    async def hug(self, ctx, user: discord.Member = None):
        """Hug someone!

        user: Discord user. If none, the bot will hug yourself.
        """
        if user is None:
            user = ctx.author
        elif user == self.bot.user:
            await ctx.send(emote.hug_left)
            return

        await ctx.send(emote.hug_right +
                       f" **{self.sanitise(user.display_name)}**")

    @hug.error
    async def hugError(self, ctx, error):
        if isinstance(error, commands.BadArgument):
            await ctx.send(self.text.get("cannot_hug"))

    @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"**{ctx.author.display_name}**\n>>> " + text)

    @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
        await ctx.send(f"**{ctx.author.display_name}**\n>>> " + text[:1900])

    @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:
                # 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 result[-1] == "?":
            result += " UwU"
        if result[-1] == "!":
            result += " OwO"
        if result[-1] == ".":
            result = result[:-1] + "," * random.randint(2, 4)

        return result
Exemplo n.º 29
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)
Exemplo n.º 30
0
class Backup(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

        self.text = CogText("backup")

    @commands.check(acl.check)
    @commands.max_concurrency(1, per=commands.BucketType.default, wait=False)
    @commands.group(name="backup")
    async def backup(self, ctx):
        """Backup various kinds of data"""
        await utils.send_help(ctx)

    @commands.check(acl.check)
    @backup.command(name="channel")
    async def backup_channel(self,
                             ctx,
                             channel: discord.TextChannel,
                             limit: int = None):
        """Backup contents of text channel.

        channel: Text channel to be backed up.
        limit: Optional. Number of messages to back up.

        This can only be run once at a time.
        """
        now = datetime.datetime.now()
        database.Information.add(
            id=channel.guild.id,
            channel_id=channel.id,
            author_id=ctx.author.id,
        )
        users = set()

        # download messages
        total = str(limit) if limit is not None else "???"
        status = await ctx.send(
            self.text.get("channel", "messages", count=0, total=total))

        i = 0
        async for message in channel.history(limit=limit, oldest_first=False):
            try:
                attachments = (",".join([a.url for a in message.attachments])
                               if len(message.attachments) else None)
                embeds = (json.dumps([e.to_dict() for e in message.embeds])
                          if len(message.embeds) else None)
                database.Message.add(
                    id=message.id,
                    author_id=message.author.id,
                    text=message.content,
                    attachments=attachments,
                    embeds=embeds,
                )
                users.add(message.author)
                i += 1
            except Exception as exc:
                await ctx.send(exc)

            if i > 0 and i % 500 == 0:
                await status.edit(content=self.text.get(
                    "channel", "messages", count=i, total=total))

        # download user information
        await status.edit(content=status.content + " " +
                          self.text.get("channel", "users"))
        for user in users:
            try:
                database.User.add(
                    id=user.id,
                    name=user.name,
                    avatar=await user.avatar_url_as(format="png",
                                                    size=256).read(),
                )
            except Exception as exc:
                await ctx.send(f"`{exc}`")

        # done
        await status.edit(content=status.content + " " +
                          self.text.get("channel", "done"))

        # get file size in bytes
        dbsize = os.path.getsize("data/backup/default.db")
        if ctx.guild is not None:
            limit = ctx.guild.filesize_limit
        else:
            # exactly 8MB is refused, this is ok
            limit = 8 * 1024**2 - 1024

        # copy
        fname = now.strftime(f"backup_%Y%m%d-%H%M%S_channel-{channel.name}.db")
        fpath = "data/backup/" + fname
        shutil.copy("data/backup/default.db", fpath)

        text = self.text.get("saved_as", file=fpath)
        if dbsize > limit:
            await ctx.send(text + " " +
                           self.text.get("too_big", size=int(dbsize / 1000)))
        else:
            await ctx.send(
                text,
                file=discord.File(fp=fpath, filename=fname),
            )
        database.wipe()

        delta = datetime.datetime.now() - now
        await ctx.send(
            self.text.get("time", seconds=int(delta.total_seconds())))

    @commands.guild_only()
    @commands.check(acl.check)
    @backup.command(name="emojis")
    async def backup_emojis(self, ctx):
        """Backup guild emojis.

        This can only be run once at a time.
        """
        now = datetime.datetime.now()
        database.Information.add(
            id=ctx.guild.id,
            channel_id=ctx.channel.id,
            author_id=ctx.author.id,
        )
        database.User.add(
            id=ctx.author.id,
            name=ctx.author.name,
            avatar=await ctx.author.avatar_url_as(format="png",
                                                  size=256).read(),
        )

        status = await ctx.send(
            self.text.get("emojis", "downloading",
                          count=len(ctx.guild.emojis)))
        for emoji in ctx.guild.emojis:
            try:
                database.Emoji.add(
                    id=emoji.id,
                    name=emoji.name,
                    data=await emoji.url.read(),
                )
            except Exception as exc:
                await ctx.send(f"`{exc}`")
        await status.edit(content=status.content + " " +
                          self.text.get("emojis", "done"))

        # get file size in bytes
        dbsize = os.path.getsize("data/backup/default.db")

        # copy
        fname = now.strftime(
            f"backup_%Y%m%d-%H%M%S_emojis-{ctx.guild.name}.db")
        fpath = "data/backup/" + fname
        shutil.copy("data/backup/default.db", fpath)

        text = self.text.get("saved_as", file=fpath)
        if dbsize > ctx.guild.filesize_limit:
            await ctx.send(text + " " +
                           self.text.get("too_big", size=int(dbsize / 1000)))
        else:
            await ctx.send(
                text,
                file=discord.File(fp=fpath, filename=fname),
            )
        database.wipe()

        delta = datetime.datetime.now() - now
        await ctx.send(
            self.text.get("time", seconds=int(delta.total_seconds())))