| 
 | 1 | +import base64  | 
1 | 2 | import logging  | 
2 | 3 | import os  | 
3 | 4 | import re  | 
4 | 5 | import sys  | 
5 | 6 | from pathlib import Path  | 
6 |  | -from typing import Any, cast  | 
 | 7 | +from typing import Any, Final, cast  | 
7 | 8 | 
 
  | 
8 | 9 | from dotenv import load_dotenv  | 
9 | 10 | from httpx import Client, HTTPStatusError, Response  | 
 | 
15 | 16 |     Webhooks,  | 
16 | 17 |     _Categories,  # pyright: ignore[reportPrivateUsage]  | 
17 | 18 |     _Channels,  # pyright: ignore[reportPrivateUsage]  | 
 | 19 | +    _Emojis,  # pyright: ignore[reportPrivateUsage]  | 
18 | 20 |     _Roles,  # pyright: ignore[reportPrivateUsage]  | 
19 | 21 | )  | 
20 | 22 | from bot.log import get_logger  # noqa: E402  | 
 | 
35 | 37 | RULES_CHANNEL_NAME = "rules"  | 
36 | 38 | GUILD_CATEGORY_TYPE = 4  | 
37 | 39 | GUILD_FORUM_TYPE = 15  | 
 | 40 | +EMOJI_REGEX = re.compile(r"<:(\w+):(\d+)>")  | 
38 | 41 | 
 
  | 
39 | 42 | if not BOT_TOKEN:  | 
40 | 43 |     message = (  | 
@@ -76,6 +79,8 @@ def __getitem__(self, item: str):  | 
76 | 79 | class DiscordClient(Client):  | 
77 | 80 |     """An HTTP client to communicate with Discord's APIs."""  | 
78 | 81 | 
 
  | 
 | 82 | +    CDN_BASE_URL: Final[str] = "https://cdn.discordapp.com"  | 
 | 83 | + | 
79 | 84 |     def __init__(self, guild_id: int | str):  | 
80 | 85 |         super().__init__(  | 
81 | 86 |             base_url="https://discord.com/api/v10",  | 
@@ -238,6 +243,37 @@ def create_webhook(self, name: str, channel_id_: int) -> str:  | 
238 | 243 |         new_webhook = response.json()  | 
239 | 244 |         return new_webhook["id"]  | 
240 | 245 | 
 
  | 
 | 246 | +    def list_emojis(self) -> list[dict[str, Any]]:  | 
 | 247 | +        """Lists all the emojis for the guild."""  | 
 | 248 | +        response = self.get(f"/guilds/{self.guild_id}/emojis")  | 
 | 249 | +        return response.json()  | 
 | 250 | + | 
 | 251 | +    def get_emoji_contents(self, emoji_id: str | int) -> bytes | None:  | 
 | 252 | +        """Fetches the image data for an emoji by ID."""  | 
 | 253 | +        # emojis are located at https://cdn.discordapp.com/emojis/821611231663751168.png?size=4096  | 
 | 254 | +        response = self.get(f"{self.CDN_BASE_URL}/emojis/{emoji_id!s}.webp")  | 
 | 255 | +        return response.content  | 
 | 256 | + | 
 | 257 | +    def clone_emoji(self, *, name: str, id: str | int) -> str:  | 
 | 258 | +        """Creates a new emoji in the guild, cloned from another emoji by ID."""  | 
 | 259 | +        emoji_data = self.get_emoji_contents(id)  | 
 | 260 | +        if not emoji_data:  | 
 | 261 | +            log.warning(f"Couldn't find emoji with ID {id}.")  | 
 | 262 | +            return ""  | 
 | 263 | + | 
 | 264 | +        payload = {  | 
 | 265 | +            "name": name,  | 
 | 266 | +            "image": f"data:image/png;base64,{base64.b64encode(emoji_data).decode('utf-8')}",  | 
 | 267 | +        }  | 
 | 268 | + | 
 | 269 | +        response = self.post(  | 
 | 270 | +            f"/guilds/{self.guild_id}/emojis",  | 
 | 271 | +            json=payload,  | 
 | 272 | +            headers={"X-Audit-Log-Reason": f"Creating {name} emoji as part of PyDis botstrap"},  | 
 | 273 | +        )  | 
 | 274 | +        new_emoji = response.json()  | 
 | 275 | +        return new_emoji["id"]  | 
 | 276 | + | 
241 | 277 | 
 
  | 
242 | 278 | with DiscordClient(guild_id=GUILD_ID) as discord_client:  | 
243 | 279 |     if discord_client.upgrade_application_flags_if_necessary():  | 
@@ -329,7 +365,24 @@ def create_webhook(self, name: str, channel_id_: int) -> str:  | 
329 | 365 |         config_str += f"webhooks_{webhook_name}__id={webhook_id}\n"  | 
330 | 366 | 
 
  | 
331 | 367 |     config_str += "\n#Emojis\n"  | 
332 |  | -    config_str += "emojis_trashcan=🗑️"  | 
 | 368 | + | 
 | 369 | +    existing_emojis = discord_client.list_emojis()  | 
 | 370 | +    log.debug("Syncing emojis with bot configuration.")  | 
 | 371 | +    for emoji_config_name, emoji_config in _Emojis.model_fields.items():  | 
 | 372 | +        if not (match := EMOJI_REGEX.match(emoji_config.default)):  | 
 | 373 | +            continue  | 
 | 374 | +        emoji_name = match.group(1)  | 
 | 375 | +        emoji_id = match.group(2)  | 
 | 376 | + | 
 | 377 | +        for emoji in existing_emojis:  | 
 | 378 | +            if emoji["name"] == emoji_name:  | 
 | 379 | +                emoji_id = emoji["id"]  | 
 | 380 | +                break  | 
 | 381 | +        else:  | 
 | 382 | +            log.info("Creating emoji %s", emoji_name)  | 
 | 383 | +            emoji_id = discord_client.clone_emoji(name=emoji_name, id=emoji_id)  | 
 | 384 | + | 
 | 385 | +        config_str += f"emojis_{emoji_config_name}=<:{emoji_name}:{emoji_id}>\n"  | 
333 | 386 | 
 
  | 
334 | 387 |     with env_file_path.open("wb") as file:  | 
335 | 388 |         file.write(config_str.encode("utf-8"))  | 
 | 
0 commit comments