aboutsummaryrefslogtreecommitdiffstats
path: root/bot/exts/utilities/twemoji.py
blob: 6552aa561bd221009df71d3ca604216d9c94bd8b (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
import re
from typing import Literal

import discord
from discord.ext import commands
from emoji import EMOJI_DATA, is_emoji
from pydis_core.utils.logging import get_logger

from bot.bot import Bot
from bot.constants import Colours, Roles
from bot.utils.decorators import whitelist_override

log = get_logger(__name__)
BASE_URLS = {
    "png": "https://raw.githubusercontent.com/twitter/twemoji/master/assets/72x72/",
    "svg": "https://raw.githubusercontent.com/twitter/twemoji/master/assets/svg/",
}
CODEPOINT_REGEX = re.compile(r"[a-f1-9][a-f0-9]{3,5}$")


class Twemoji(commands.Cog):
    """Utilities for working with Twemojis."""

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

    @staticmethod
    def get_url(codepoint: str, format: Literal["png", "svg"]) -> str:
        """Returns a source file URL for the specified Twemoji, in the corresponding format."""
        return f"{BASE_URLS[format]}{codepoint}.{format}"

    @staticmethod
    def alias_to_name(alias: str) -> str:
        """
        Transform a unicode alias to an emoji name.

        Example usages:
        >>> alias_to_name(":falling_leaf:")
        "Falling leaf"
        >>> alias_to_name(":family_man_girl_boy:")
        "Family man girl boy"
        """
        name = alias.strip(":").replace("_", " ")
        return name.capitalize()

    @staticmethod
    def build_embed(codepoint: str) -> discord.Embed:
        """Returns the main embed for the `twemoji` commmand."""
        emoji = "".join(Twemoji.emoji(e) or "" for e in codepoint.split("-"))

        embed = discord.Embed(
            title=Twemoji.alias_to_name(EMOJI_DATA[emoji]["en"]),
            description=f"{codepoint.replace('-', ' ')}\n[Download svg]({Twemoji.get_url(codepoint, 'svg')})",
            colour=Colours.twitter_blue,
        )
        embed.set_thumbnail(url=Twemoji.get_url(codepoint, "png"))
        return embed

    @staticmethod
    def emoji(codepoint: str | None) -> str | None:
        """
        Returns the emoji corresponding to a given `codepoint`, or `None` if no emoji was found.

        The return value is an emoji character, such as "πŸ‚". The `codepoint`
        argument can be of any format, since it will be trimmed automatically.
        """
        if code := Twemoji.trim_code(codepoint):
            return chr(int(code, 16))
        return None

    @staticmethod
    def codepoint(emoji: str | None) -> str | None:
        """
        Returns the codepoint, in a trimmed format, of a single emoji.

        `emoji` should be an emoji character, such as "🐍" and "πŸ₯°", and
        not a codepoint like "1f1f8". When working with combined emojis,
        such as "πŸ‡ΈπŸ‡ͺ" and "πŸ‘¨β€πŸ‘©β€πŸ‘¦", send the component emojis through the method
        one at a time.
        """
        if emoji is None:
            return None
        return hex(ord(emoji)).removeprefix("0x")

    @staticmethod
    def trim_code(codepoint: str | None) -> str | None:
        """
        Returns the meaningful information from the given `codepoint`.

        If no codepoint is found, `None` is returned.

        Example usages:
        >>> trim_code("U+1f1f8")
        "1f1f8"
        >>> trim_code("\u0001f1f8")
        "1f1f8"
        >>> trim_code("1f466")
        "1f466"
        """
        if code := CODEPOINT_REGEX.search(codepoint or ""):
            return code.group()
        return None

    @staticmethod
    def codepoint_from_input(raw_emoji: tuple[str, ...]) -> str:
        """
        Returns the codepoint corresponding to the passed tuple, separated by "-".

        The return format matches the format used in URLs for Twemoji source files.

        Example usages:
        >>> codepoint_from_input(("🐍",))
        "1f40d"
        >>> codepoint_from_input(("1f1f8", "1f1ea"))
        "1f1f8-1f1ea"
        >>> codepoint_from_input(("πŸ‘¨β€πŸ‘§β€πŸ‘¦",))
        "1f468-200d-1f467-200d-1f466"
        """
        raw_emoji = [emoji.lower() for emoji in raw_emoji]
        if is_emoji(raw_emoji[0]):
            emojis = (Twemoji.codepoint(emoji) or "" for emoji in raw_emoji[0])
            return "-".join(emojis)

        emoji = "".join(
            Twemoji.emoji(Twemoji.trim_code(code)) or "" for code in raw_emoji
        )
        if is_emoji(emoji):
            return "-".join(Twemoji.codepoint(e) or "" for e in emoji)

        raise ValueError("No codepoint could be obtained from the given input")

    @commands.command(aliases=("tw",))
    @whitelist_override(roles=(Roles.everyone,))
    async def twemoji(self, ctx: commands.Context, *raw_emoji: str) -> None:
        """Sends a preview of a given Twemoji, specified by codepoint or emoji."""
        if len(raw_emoji) == 0:
            await self.bot.invoke_help_command(ctx)
            return
        try:
            codepoint = self.codepoint_from_input(raw_emoji)
        except ValueError:
            raise commands.BadArgument(
                "please include a valid emoji or emoji codepoint."
            )

        await ctx.send(embed=self.build_embed(codepoint))


async def setup(bot: Bot) -> None:
    """Load the Twemoji cog."""
    await bot.add_cog(Twemoji(bot))