[
  {
    "path": ".gitignore",
    "content": ".idea/\nvenv/\n__pycache__/\n*.log\n/.env\nmy-id-bot.service\ndocker-compose.yml\n"
  },
  {
    "path": "Dockerfile",
    "content": "# Separate \"build\" image\nFROM python:3.11-slim as builder\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY bot /app/bot\n\n# Final stage\nFROM gcr.io/distroless/python3-debian12:nonroot\nCOPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages\nCOPY --from=builder /app /app\nWORKDIR /app\nENV PYTHONPATH=/usr/local/lib/python3.11/site-packages\nCMD [\"-m\", \"bot\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019-present Aleksandr K. (also known as MasterGroosha on GitHub)\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Bot to get users/chats IDs in Telegram\n\nThis is a simple bot written with [aiogram 3.x](https://github.com/aiogram/aiogram) framework to show some IDs, like:\n\n* Your user ID (when asked in inline mode or in private chat with any message);  \n* Group/supergroup ID (when added to that group or with /id command);  \n* Channel ID (when message forwarded from channel to one-to-one chat with bot);  \n* Supergroup ID (when message forwarded from anonymous group admin);  \n* Topic ID for [forum supergroups](https://telegram.org/blog/topics-in-groups-collectible-usernames#topics-in-groups);  \n* Sticker ID (they can be re-used with any bot);\n* Group to supergroup migrate information (both old and new ID).\n\n## Requirements:\n* Python 3.11 and newer;  \n* Linux (should work on Windows, but not tested);   \n* Systemd init system (optional).  \n* Docker (optional).\n\n## Installation:\n\n### Just to test (not recommended)\n1. Clone this repo;\n2. `cd` to cloned directory and initialize Python virtual environment (venv);\n3. Activate the venv and install all dependencies from `requirements.txt` file;\n4. Copy `env_example` to `.env` (with the leading dot), open `.env` and edit the variables;\n5. In the activated venv: `python -m bot`\n\n### Systemd \n1. Perform steps 1-4 from \"just to test\" option above;\n2. Copy `my-id-bot.example.service` to `my-id-bot.service` (or whatever your prefer), open it and edit `WorkingDirectory` \nand `ExecStart` directives;\n3. Copy (or symlink) that service file to `/etc/systemd/system/` directory;\n4. Enable your service `sudo systemctl enable my-id-bot --now`;\n5. Check that service is running: `systemctcl status my-id-bot` (can be used without root privileges).\n\n### Docker + Docker Compose\n1. Get `docker-compose.example.yml` file and rename it as `docker-compose.yml`;\n2. Get `env_example` file, rename it as `.env` (with the leading dot), open it and edit the variables;\n3. Run the bot: `docker compose up -d`;\n4. Check that container is up and running: `docker compose ps`\n"
  },
  {
    "path": "bot/__init__.py",
    "content": ""
  },
  {
    "path": "bot/__main__.py",
    "content": "import asyncio\nfrom pathlib import Path\n\nimport structlog\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.client.default import DefaultBotProperties\nfrom aiogram.enums import ParseMode\nfrom structlog.typing import FilteringBoundLogger\n\nfrom bot.config_reader import bot_config, log_config\nfrom bot.fluent_helper import FluentDispenser\nfrom bot.handlers import commands, pm, add_or_migrate, inline_mode, errors\nfrom bot.logs import get_structlog_config\nfrom bot.middlewares import L10nMiddleware, UnhandledUpdatesLoggerMiddleware\nfrom bot.ui_commands import set_bot_commands\n\nlogger: FilteringBoundLogger = structlog.get_logger()\n\n\nasync def main():\n    structlog.configure(**get_structlog_config(log_config))\n\n    bot = Bot(\n        token=bot_config.bot_token.get_secret_value(),\n        default=DefaultBotProperties(\n            parse_mode=ParseMode.HTML,\n        )\n    )\n\n    # Setup dispatcher\n    dp = Dispatcher()\n\n    dispenser = FluentDispenser(\n        locales_dir=Path(__file__).parent.joinpath(\"locales\"),\n        default_language=\"en\"\n    )\n    dp.update.middleware(L10nMiddleware(dispenser))\n\n    if log_config.log_unhandled:\n        dp.update.outer_middleware(UnhandledUpdatesLoggerMiddleware())\n\n    # Register handlers\n    dp.include_routers(\n        commands.router,\n        pm.router,\n        add_or_migrate.router,\n        inline_mode.router,\n        errors.router\n    )\n\n    # Set bot commands in UI\n    await set_bot_commands(bot, dispenser)\n\n    # Run bot\n    await logger.awarning(\n        \"Important! This version is the last one to use environment variables for configuration. \"\n        \"The next version is going to use TOML file. Be careful when upgrading bot version in the future.\"\n    )\n    await logger.ainfo(\"Starting bot\")\n    await dp.start_polling(bot, allowed_updates=dp.resolve_used_update_types())\n    await logger.ainfo(\"Bot stopped\")\n\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n"
  },
  {
    "path": "bot/config_reader.py",
    "content": "from enum import Enum\n\nfrom pydantic import SecretStr\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\n\n\nclass LoggingRenderer(str, Enum):\n    JSON = \"json\"\n    CONSOLE = \"console\"\n\n\nclass LoggingSettings(BaseSettings):\n    level: str = \"INFO\"\n    format: str = \"%Y-%m-%d %H:%M:%S\"\n    is_utc: bool = False\n    renderer: LoggingRenderer = LoggingRenderer.JSON\n    log_unhandled: bool = False\n\n    model_config = SettingsConfigDict(\n        env_file=\".env\",\n        env_file_encoding=\"utf-8\",\n        env_prefix=\"LOGGING_\",\n        extra=\"allow\",\n    )\n\n\nclass BotSettings(BaseSettings):\n    bot_token: SecretStr\n\n    model_config = SettingsConfigDict(\n        env_file=\".env\",\n        env_file_encoding=\"utf-8\",\n        extra=\"allow\",\n    )\n\n\nbot_config = BotSettings()\nlog_config = LoggingSettings()\n"
  },
  {
    "path": "bot/fluent_helper.py",
    "content": "from pathlib import Path\n\nfrom fluent.runtime import FluentLocalization, FluentResourceLoader\n\n\nclass FluentDispenser:\n    def __init__(self, locales_dir: Path, default_language: str = \"en\"):\n        self.__loader = FluentResourceLoader(str(locales_dir) + \"/{locale}\")\n        self.__default_language = default_language\n        self.languages = dict()\n\n        dirs_names = set()\n        default_language_dir = None\n        for item in locales_dir.iterdir():\n            dirs_names.add(item.name)\n            if item.name == self.__default_language:\n                default_language_dir = item\n\n        if not default_language_dir:\n            raise ValueError(\"FluentDispenser: default language directory not found\")\n\n        ftl_files_list = [item.name for item in default_language_dir.iterdir() if item.suffix == \".ftl\"]\n\n        for name in dirs_names:\n            if name == default_language:\n                self.languages[name] = FluentLocalization([self.__default_language], ftl_files_list, self.__loader)\n            else:\n                self.languages[name] = FluentLocalization([name, self.__default_language], ftl_files_list, self.__loader)\n\n    @property\n    def default_locale(self) -> FluentLocalization:\n        return self.languages[self.__default_language]\n\n    @property\n    def available_languages(self) -> list[str]:\n        return list(self.languages.keys())\n\n    def get_language(self, language_code: str):\n        return self.languages.get(language_code, self.default_locale)\n"
  },
  {
    "path": "bot/handlers/__init__.py",
    "content": ""
  },
  {
    "path": "bot/handlers/add_or_migrate.py",
    "content": "from asyncio import sleep\n\nfrom aiogram import Bot, html, Router, F\nfrom aiogram.enums import ChatType\nfrom aiogram.filters.chat_member_updated import \\\n    ChatMemberUpdatedFilter, JOIN_TRANSITION\nfrom aiogram.types import ChatMemberUpdated, Message\nfrom fluent.runtime import FluentLocalization\n\nfrom bot.migration_cache import cache\n\nrouter = Router()\n\n\n@router.my_chat_member(\n    ChatMemberUpdatedFilter(\n        member_status_changed=JOIN_TRANSITION\n    ),\n    F.chat.type.in_({ChatType.GROUP, ChatType.SUPERGROUP})\n)\nasync def bot_added_to_group(event: ChatMemberUpdated, bot: Bot, l10n: FluentLocalization):\n    \"\"\"\n    Bot was added to group.\n\n    :param event: an event from Telegram of type \"my_chat_member\"\n    :param bot: bot who message was addressed to\n    :param l10n: Fluent localization object\n    :return:\n    \"\"\"\n    await sleep(1.0)\n    if event.chat.id not in cache.keys():\n        await bot.send_message(\n            chat_id=event.chat.id,\n            text=l10n.format_value(\n                \"any-chat\",\n                args={\"type\": event.chat.type, \"id\": html.code(event.chat.id)}\n            )\n        )\n\n\n@router.message(F.migrate_to_chat_id)\nasync def group_to_supergroup_migration(message: Message, bot: Bot, l10n: FluentLocalization):\n    await bot.send_message(\n        message.migrate_to_chat_id,\n        l10n.format_value(\n            \"group-to-supergroup\",\n            args={\"old_id\": html.code(message.chat.id), \"new_id\": html.code(message.migrate_to_chat_id)}\n        )\n    )\n\n    cache[message.migrate_to_chat_id] = True\n"
  },
  {
    "path": "bot/handlers/commands.py",
    "content": "from aiogram import Router, html, F\nfrom aiogram.enums import ChatType\nfrom aiogram.filters import Command, CommandStart\nfrom aiogram.types import Message\nfrom aiogram.utils.keyboard import InlineKeyboardBuilder\nfrom fluent.runtime import FluentLocalization\n\nrouter = Router()\nrouter.message.filter(~F.forward_from & ~F.forward_from_chat)\n\n\n@router.message(F.chat.type == ChatType.PRIVATE, CommandStart())\nasync def cmd_start(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    /start command handler for private chats\n    :param message: Telegram message with \"/start\" command\n    :param l10n: Fluent localization object\n    \"\"\"\n    builder = InlineKeyboardBuilder()\n    builder.button(text=l10n.format_value(\"cmd-start-inline-try-here\"), switch_inline_query_current_chat=\"\")\n    builder.button(text=l10n.format_value(\"cmd-start-inline-try-other\"), switch_inline_query=\"\")\n    await message.answer(\n        l10n.format_value(\"cmd-start\", args={\"id\": html.code(message.chat.id)}),\n        reply_markup=builder.adjust(1).as_markup()\n    )\n\n\n@router.message(F.chat.type == ChatType.PRIVATE, Command(\"id\"))\nasync def cmd_id_pm(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    /id command handler for private messages\n    :param message: Telegram message with \"/id\" command\n    :param l10n: Fluent localization object\n    \"\"\"\n    await message.answer(\n        l10n.format_value(\"cmd-id-pm\", args={\"id\": html.code(message.from_user.id)})\n    )\n\n\n@router.message(\n    F.chat.type.in_({ChatType.GROUP, ChatType.SUPERGROUP}),\n    Command(\"id\")\n)\n@router.message(\n    F.chat.type.in_({ChatType.GROUP, ChatType.SUPERGROUP}),\n    CommandStart(deep_link=True, magic=F.args == \"id\")\n)\nasync def cmd_id_groups(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    /id command handler for (super)groups\n    :param message: Telegram message with \"/id\" command\n    :param l10n: Fluent localization object\n    \"\"\"\n    chat_type_str = l10n.format_value(message.chat.type)\n    msg = [l10n.format_value(\"any-chat\", args={\"type\": chat_type_str, \"id\": html.code(message.chat.id)})]\n\n    if message.is_topic_message:\n        msg.append(\n            l10n.format_value(\n                \"cmd-id-group-topic-id\",\n                args={\"type\": message.chat.type, \"id\": html.code(message.message_thread_id)}\n            )\n        )\n\n    if message.sender_chat is None:\n        msg.append(l10n.format_value(\"cmd-id-pm\", args={\"id\": html.code(message.from_user.id)}))\n    else:\n        msg.append(l10n.format_value(\"cmd-id-group-as-channel\", args={\"id\": html.code(message.sender_chat.id)}))\n\n    await message.reply(\"\\n\".join(msg))\n\n\n@router.message(Command(\"help\"))\nasync def cmd_help(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    /help command handler for all chats\n    :param message: Telegram message with \"/help\" command\n    :param l10n: Fluent localization object\n    \"\"\"\n    await message.answer(l10n.format_value(\"cmd-help\"), disable_web_page_preview=True)\n"
  },
  {
    "path": "bot/handlers/errors.py",
    "content": "import structlog\nfrom aiogram import Router\nfrom aiogram.exceptions import TelegramAPIError\nfrom aiogram.types.error_event import ErrorEvent\nfrom structlog.typing import FilteringBoundLogger\n\nrouter = Router(name=\"errors-router\")\nlogger: FilteringBoundLogger = structlog.get_logger()\n\n\n@router.errors()\nasync def handle_errors(event: ErrorEvent):\n    if isinstance(event.exception, TelegramAPIError):\n        error_message = event.exception.message\n        error_source = \"BotAPI\"\n    else:\n        error_message = str(event.exception)\n        error_source = \"Python\"\n\n    await logger.aerror(\n        \"Outgoing bot message error\",\n        exception_type=event.exception.__class__.__name__,\n        message=error_message,\n        update=event.update.dict(),\n        error_source=error_source\n    )\n"
  },
  {
    "path": "bot/handlers/inline_mode.py",
    "content": "from aiogram import html, Router\nfrom aiogram.enums import ChatType\nfrom aiogram.types import InlineQuery, InlineQueryResultArticle, InputTextMessageContent\nfrom fluent.runtime import FluentLocalization\n\nrouter = Router()\n\n\n@router.inline_query()\nasync def inline_mode_handler(query: InlineQuery, l10n: FluentLocalization):\n    result = InlineQueryResultArticle(\n        id=\".\",\n        title=l10n.format_value(\"inline-mode-title\", args={\"id\": query.from_user.id}),\n        description=l10n.format_value(\"inline-mode-description\"),\n        input_message_content=InputTextMessageContent(\n            message_text=l10n.format_value(\"inline-mode-text\", args={\"id\": html.code(query.from_user.id)})\n        )\n    )\n    # Do not forget about is_personal parameter! Otherwise, all people will see the same ID\n    switch_pm_text = l10n.format_value(\"inline-mode-tryme\") if query.chat_type != ChatType.SENDER else None\n    await query.answer(\n        results=[result], cache_time=3600, is_personal=True,\n        switch_pm_parameter=\"1\", switch_pm_text=switch_pm_text\n    )\n"
  },
  {
    "path": "bot/handlers/pm.py",
    "content": "from aiogram import Router, html, F\nfrom aiogram.enums import ChatType\nfrom aiogram.filters import MagicData\nfrom aiogram.types import Message\nfrom fluent.runtime import FluentLocalization\n\nrouter = Router()\nrouter.message.filter(F.chat.type == ChatType.PRIVATE)\n\n\n@router.message(F.forward_from_chat.type.as_(\"chat_type\"))\nasync def get_channel_or_supergroup_id(message: Message, chat_type: str, l10n: FluentLocalization):\n    \"\"\"\n    Handler for message forwarded from channel\n    or from anonymous admin writing on behalf\n    of a supergroup\n    :param message: Telegram message with \"forward_from_chat\" field not empty\n    :param chat_type: parsed chat_type (\"channel\" or \"supergroup\")\n    :param l10n: Fluent localization object\n    \"\"\"\n    chat_type_str = l10n.format_value(chat_type)\n    msg = l10n.format_value(\"any-chat\", args={\"type\": chat_type_str, \"id\": html.code(message.forward_from_chat.id)})\n    if message.sticker:\n        msg += \"\\n\" + l10n.format_value(\"sticker-id\", args={\"id\": html.code(message.sticker.file_id)})\n    await message.reply(msg)\n\n\n@router.message(F.forward_from)\nasync def get_user_id_no_privacy(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    Handler for message forwarded from other user who doesn't hide their ID\n    :param message: Telegram message with \"forward_from\" field not empty\n    :param l10n: Fluent localization object\n    \"\"\"\n    account_type = \"bot\" if message.forward_from.is_bot else \"user\"\n    chat_type_str = l10n.format_value(account_type)\n    msg = l10n.format_value(\"any-chat\", args={\"type\": chat_type_str, \"id\": html.code(message.forward_from.id)})\n    if message.sticker:\n        msg += \"\\n\" + l10n.format_value(\"sticker-id\", args={\"id\": html.code(message.sticker.file_id)})\n    await message.reply(msg)\n\n\n@router.message(F.forward_sender_name)\nasync def get_user_id_with_privacy(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    Handler for message forwarded from other user who hides their ID\n    :param message: Telegram message with \"forward_sender_name\" field not empty\n    :param l10n: Fluent localization object\n    \"\"\"\n    msg = l10n.format_value(\"user-id-hidden\")\n    if message.sticker:\n        msg += \"\\n\\n\" + l10n.format_value(\"sticker-id\", args={\"id\": html.code(message.sticker.file_id)})\n    await message.reply(msg)\n\n\n@router.message(F.sticker)\nasync def sticker_in_pm(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    /start command handler for private chats\n    :param message: Telegram message with \"/start\" command\n    :param l10n: Fluent localization object\n    \"\"\"\n    #\n    await message.reply(\n        l10n.format_value(\"sticker-id-extended\", args={\"id\": html.code(message.sticker.file_id)})\n    )\n\n\n@router.message(F.via_bot, MagicData(F.event.via_bot.id != F.bot.id))  # noqa\nasync def other_inline_bot_in_pm(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    Message via some other inline bot in PM\n    :param message: Any Telegram message\n    :param l10n: Fluent localization object\n    \"\"\"\n    bot_str = l10n.format_value(\"bot\")\n    await message.answer(\n        l10n.format_value(\"any-chat\", args={\"type\": bot_str, \"id\": html.code(message.via_bot.id)})\n    )\n\n\n@router.message(~F.via_bot)\nasync def other_in_pm(message: Message, l10n: FluentLocalization):\n    \"\"\"\n    Any other message in PM, not via inline bot\n    :param message: Any Telegram message\n    :param l10n: Fluent localization object\n    \"\"\"\n    await message.answer(l10n.format_value(\"cmd-id-pm\", args={\"id\": html.code(message.from_user.id)}))\n"
  },
  {
    "path": "bot/locales/en/strings.ftl",
    "content": "# without @ !\nbot-username = my_id_bot\n\n# do not translate!\nbot-group-deeplink = https://t.me/{bot-username}?startgroup=id\n\nsource-code-link = https://github.com/MasterGroosha/my-id-bot\n\ncmd-start =\n    Your Telegram ID is { $id }\n    Help and source code: /help\n\n    You can also use this bot in inline mode to share its ID! Try using one of the buttons below.\n    Please note, that bot uses your language in PM and English in any other chats.\n\ncmd-start-inline-try-here = Try here\ncmd-start-inline-try-other = Try in other chat\n\n# Used in strings like \"This channel id is xxx\"\nsupergroup = supergroup\ngroup = group\nchannel = channel\nuser = user\nbot = bot\n\nany-chat = This { $type } ID is { $id }\n\ncmd-id-pm = Your Telegram ID is { $id }\ncmd-id-group-topic-id = This forum topic ID is { $id }\ncmd-id-group-as-channel = And you've sent this message as channel with ID { $id }\n\ncmd-help =\n    Use this bot to get ID for different entities across Telegram:\n\n    • Forward message from channel to get channel ID;\n    • Forward message from anonymous supergroup admin to get supergroup ID;\n    • Forward message from user to get their ID (unless they restrict from doing so);\n    • Forward message from some other bot or use it via inline mode to get bot ID;\n    • Send a sticker to get its file_id (currently you can use the sticker's file_id with any bot);\n    • <a href=\"{bot-group-deeplink}\">Add bot to group</a> to get its ID (it will even tell you when you migrate from group to supergroup);\n    • Use inline mode to send your Telegram ID to any chat.\n\n    Source code: { source-code-link }\n\ngroup-to-supergroup =\n    Group upgraded to supergroup.\n    Old ID: { $old_id }\n    New ID: { $new_id }\n\ninline-mode-title = Your ID is { NUMBER($id, useGrouping: 0) }\ninline-mode-description = Tap to send your ID to current chat\ninline-mode-text = My Telegram ID is { $id }\ninline-mode-tryme = Or try me in PM >>>\n\nsticker-id = Also this sticker's ID is { $id }\nsticker-id-extended =\n    This sticker ID is\n    { $id }\n    Sticker is currently the only media type which file_ids can be used by any bot.\n\nuser-id-hidden =\n    This user decided to <b>hide</b> their ID.\n    Learn more about this feature <a href=\"https://telegram.org/blog/unsend-privacy-emoji#anonymous-forwarding\">here</a>.\n\n# Commands in UI (when you press \"/\" or \"Menu\" button)\ncmd-hint-id-pm = Print your Telegram ID\ncmd-hint-id-group = Print Telegram ID of this group chat\ncmd-hint-help = Help and source code\n"
  },
  {
    "path": "bot/locales/es/strings.ftl",
    "content": "# without @ !\nbot-username = my_id_bot\n\n# do not translate!\nbot-group-deeplink = https://t.me/{bot-username}?startgroup=id\n\nsource-code-link = https://github.com/MasterGroosha/my-id-bot\n\ncmd-start =\n    Tu ID de Telegram es { $id }.\n    Ayuda y código fuente: /help\n\n    También puedes usar este bot en modo inline para compartir la ID! Prueba usando lso botones de abajo.\n    Por favor, ten en cuenta que el bot usa tu idioma en privado y en inglés en cualquier otro chat.\n\ncmd-start-inline-try-here = Intenta aquí\ncmd-start-inline-try-other = Intenta en otro chat\n\n# Used in strings like \"This channel id is xxx\"\nsupergroup = supergrupo\ngroup = grupo\nchannel = canal\nuser = usuario\nbot = bot\n\nany-chat = Esta { $type } ID es { $id }\n\ncmd-id-pm = Tu ID de Telegram es { $id }.\ncmd-id-group-topic-id = La ID de este grupo es { $id }.\ncmd-id-group-as-channel = Y has envido este mensaje de  un canal con ID { $id }.\n\ncmd-help =\n    Usa este bot para obtener la ID de diferentes entidades en Telegram:\n\n    • Reenvía un mensaje de un canal para obtener la ID del canal;\n    • Reenvía un mensaje de un admin de supergrupo anónimo para obtener la ID del supergrupo;\n    • Reenvía un mensaje de un usuario para obtener su ID (Solo si no tiene activada la protección);\n    • Reenvía un mensaje de otro bot o usa el modo inline para obtener la ID de este;\n    • Envía un sticker para obtener su file_id (Puedes usar el file_id de sticker con cualquier bot);\n    • <a href=\"{bot-group-deeplink}\">Añade el bot a un grupo</a> para obtener su ID (incluso te dirá cuando pases de grupo a supergrupo);\n    • Usa el modo inline para enviar tu ID de Telegram a otros chats.\n\n    Source code: { source-code-link }\n\ngroup-to-supergroup =\n    El grupo se ha convertido en supergroup.\n    Antigua ID: { $old_id }\n    Nueva ID: { $new_id }\n\ninline-mode-title = Tu ID es { NUMBER($id, useGrouping: 0) }\ninline-mode-description = has click para mandar tu ID al chat actual.\ninline-mode-text = Mi ID de Telegram es { $id }.\ninline-mode-tryme = O prueba el bot en PM >>>\n\nsticker-id = Ademas, la ID del sticker es { $id }.\nsticker-id-extended =\n    La ID del sticker es\n    { $id }\n    El sticker es actualmente el único tipo de datos cuya file_ids pueden usar los bots.\n\nuser-id-hidden =\n    Este usuario ha decidido <b>ocultar</b> su ID.\n    Puedes leer más sobre este cambio <a href=\"https://telegram.org/blog/unsend-privacy-emoji#anonymous-forwarding\">aquí</a>.\n\n# Commands in UI (when you press \"/\" or \"Menu\" button)\ncmd-hint-id-pm = Muestra tu ID de Telegram.\ncmd-hint-id-group = Muestra la ID de Telegram de este chat de grupo.\ncmd-hint-help = Ayuda y código fuente.\n"
  },
  {
    "path": "bot/locales/ro/strings.ftl",
    "content": "# without @ !\nbot-username = my_id_bot\n\n# do not translate!\nbot-group-deeplink = https://t.me/{bot-username}?startgroup=id\n\nsource-code-link = https://github.com/MasterGroosha/my-id-bot\n\ncmd-start =\n    ID-ul tău de Telegram este { $id }\n    Ajutor si cod sursă: /help\n\n    Poți folosi și acest bot în modul inline pentru a partaja ID-ul său! Încearcă să folosești unul dintre butoanele de mai jos.\n    Te rugăm să reții că botul folosește limba ta în mesajele private și engleza în orice alte conversații.\n\ncmd-start-inline-try-here = Încearcă aici\ncmd-start-inline-try-other = Încearcă într-un alt chat\n\n# Used in strings like \"This channel id is xxx\"\nsupegroup = supegrup\ngroup = grup\nchannel = canal\nuser = utilizator\nbot = bot\n\nany-chat = Acest { $type } de ID este { $id }\n\ncmd-id-pm = ID-ul tău de Telegram este { $id }\ncmd-id-group-topic-id = ID-ul acestui subiect de forum este { $id }\ncmd-id-groupd-as-channel = Și ai trimis acest mesaj ca canal cu ID-ul { $id }\n\ncmd-help =\n    Folosește acest bot pentru a obține ID-ul pentru diferite entități din Telegram:\n\n    • Trimite mai departe mesajul din canal pentru a obține ID-ul canalului;\n    • Trimite mai departe mesajul de la un administrator anonim al supergrupului pentru a obține ID-ul supergrupului;\n    • Trimite mai departe mesajul de la utilizator pentru a obține ID-ul lor (cu excepția cazului în care aceștia restricționează acest lucru);\n    • Trimite mai departe mesajul de la un alt bot sau folosește-l prin modul inline pentru a obține ID-ul botului;\n    • Trimite un sticker pentru a obține file_id-ul său (în prezent, poți folosi file_id-ul sticker-ului cu orice bot);\n    • <a href=\"{bot-group-deeplink}\">Adaugă botul în grup</a> pentru a obține ID-ul său (îți va spune chiar și când migrezi de la grup la supergrup);\n    • Folosește modul inline pentru a trimite ID-ul tău de Telegram în orice conversație.\n\n    Cod sursă: { source-code-link }\n\ngroup-to-supegroup =\n    Grupul a fost actualizat la supergrup.\n    ID-ul vechi: { $old_id }\n    ID-ul nou: { $new_id }\n\ninline-mode-title = ID-ul tău este { NUMBER($id, useGrouping: 0) }\ninline-mode-description = Apasă pentru a trimite ID-ul tău în conversația curentă\ninline-mode-text = ID-ul meu de Telegram este { $id }\ninline-mode-tryme = Sau acceseaza-mă în mesaj privat >>>\n\nsticker-id = De asemenea, ID-ul acestui sticker este { $id }\nsticker-id-extended =\n    ID-ul acestui sticker este\n    { $id }\n    Stickerul este în prezent singurul tip de media al cărui file_id poate fi folosit de orice bot.\n\nuser-id-hidden =\n    Acest utilizator a decis să își <b>ascundă</b> ID-ul.\n    Află mai multe despre această funcție <a href=\"https://telegram.org/blog/unsend-privacy-emoji#anonymous-forwarding\">aici</a>.\n\n# Commands in UI (when you press \"/\" or \"Menu\" button)\ncmd-hint-id-pm = Printează ID-ul tău de Telegram\ncmd-hint-id-group = Printează Telegram ID-ul al acestui chat de grup\ncmd-hint-help = Suport și cod sursă\n"
  },
  {
    "path": "bot/locales/ru/strings.ftl",
    "content": "# without @ !\nbot-username = my_id_bot\n\n# do not translate!\nbot-group-deeplink = https://t.me/{bot-username}?startgroup=id\n\nsource-code-link = https://github.com/MasterGroosha/my-id-bot\n\ncmd-start =\n    Ваш Telegram ID: { $id }\n    Помощь и исходники: /help\n\n    Вы также можете использовать этого бота в инлайн-режиме! Попробуйте одну из кнопок ниже.\n    Учтите, что бот отвечает на вашем языке в личных сообщениях и на английском во всех других чатах.\n\ncmd-start-inline-try-here = Попробовать здесь\ncmd-start-inline-try-other = Попробовать в другом чате\n\n# Used in strings like \"This channel id is xxx\"\nsupergroup = супергруппа\ngroup = группа\nchannel = канал\nuser = пользователь\nbot = бот\n\nany-chat = Это { $type } с ID { $id }\n\ncmd-id-pm = Ваш Telegram ID: { $id }\ncmd-id-group-topic-id = Это топик с ID { $id }\ncmd-id-group-as-channel = И вы отправили это сообщение от имени канала с ID { $id }\n\ncmd-help =\n    Этот бот предназначен для получения ID разных сущностей в Telegram:\n\n    • Перешлите сообщение из канала, чтобы узнать его ID;\n    • Перешлите сообщение от анонимного администратора супергруппы, чтобы узнать ID этой супергруппы;\n    • Перешлите сообщение от юзера, чтобы узнать его/её ID (если они не запретили это);\n    • Перешлите сообщение от другого бота или используйте его в инлайн-режиме, чтобы узнать ID бота;\n    • Отправьте стикер, чтобы узнать его file_id (их можно использовать с любыми ботами);\n    • <a href=\"{bot-group-deeplink}\">Добавьте бота в группу</a>, чтобы узнать её ID (бот также сообщит о миграции группы в супергруппу);\n    • Попробуйте бота в инлайн-режиме, чтобы отправить свой Telegram ID в любой чат.\n\n    Исходники бота: { source-code-link }\n\ngroup-to-supergroup =\n    Группа обновлена до супергруппы.\n    Старый ID: { $old_id }\n    Новый ID: { $new_id }\n\ninline-mode-title = Ваш ID { NUMBER($id, useGrouping: 0) }\ninline-mode-description = Нажмите, чтобы отправить его в текущий чат\ninline-mode-text = Мой Telegram ID { $id }\ninline-mode-tryme = Попробуйте меня в ЛС >>>\n\nsticker-id = ID этого стикера: { $id }\nsticker-id-extended =\n    ID этого стикера\n    { $id }\n    В настоящий момент только айди стикеров можно использовать любыми ботами.\n\nuser-id-hidden =\n    Этот пользователь <b>скрыл</b> свой айди при пересылке.\n    <a href=\"https://telegram.org/blog/unsend-privacy-emoji#anonymous-forwarding\">Подробнее об этой фиче</a>.\n\n# Commands in UI (when you press \"/\" or \"Menu\" button)\ncmd-hint-id-pm = Узнать свой ID\ncmd-hint-id-group = Узнать ID этой группы\ncmd-hint-help = Справка и исходники\n"
  },
  {
    "path": "bot/locales/uk/strings.ftl",
    "content": "# without @ !\nbot-username = my_id_bot\n\n# do not translate!\nbot-group-deeplink = https://t.me/{bot-username}?startgroup=id\n\nsource-code-link = https://github.com/MasterGroosha/my-id-bot\n\ncmd-start =\n    Ваш Telegram ID: { $id }\n    Допомога та вихідний код: /help\n\n    Ви також можете використовувати цього бота в інлайн-режимі! Спробуйте одну з кнопок нижче.\n    Врахуйте, що бот відповідає на вашій мові лише в особистих повідомленнях і на англійській у всіх інших чатах.\n\ncmd-start-inline-try-here = Спробувати тут\ncmd-start-inline-try-other = Спробувати в іншому чаті\n\n# Used in strings like \"This channel id is xxx\"\nsupergroup = супергрупа\ngroup = група\nchannel = канал\nuser = користувач\nbot = бот\n\nany-chat = Це { $type } з ID { $id }\n\ncmd-id-pm = Ваш Telegram ID: { $id }\ncmd-id-group-topic-id = Це топік з ID { $id }\ncmd-id-group-as-channel = І ви відправили це повідомлення від імені каналу з ID { $id }\n\ncmd-help =\n    Цей бот призначений для отримання ID різних об'єктів у Telegram:\n\n    • Перешліть повідомлення з каналу, щоб дізнатися його ID;\n    • Перешліть повідомлення від анонімного адміністратора супергрупи, щоб дізнатися ID цієї супергрупи;\n    • Перешліть повідомлення від юзера, щоб дізнатися його/її ID (якщо вони не заборонили це);\n    • Перешліть повідомлення від іншого бота або використайте його в інлайн-режимі, щоб дізнатись ID бота;\n    • Відправте стікер, щоб дізнатись його file_id (надалі, цей file_id можна використовувати у будь-яких ботах);\n    • <a href=\"{bot-group-deeplink}\">Додайте бота до групи</a>, щоб дізнатись її ID (бот також повідомить про міграцію групи у супергрупу);\n    • Спробуйте бота в інлайн-режимі, щоб надіслати свій Telegram ID у будь-який чат.\n\n    Вихідний код бота: { source-code-link }\n\ngroup-to-supergroup =\n    Група перетворена у супергрупу.\n    Старий ID: { $old_id }\n    Новий ID: { $new_id }\n\ninline-mode-title = Ваш ID { NUMBER($id, useGrouping: 0) }\ninline-mode-description = Натисніть, щоб надіслати його у поточний чат\ninline-mode-text = Мій Telegram ID { $id }\ninline-mode-tryme = Спробуйте мене в особистих повідомленнях >>>\n\nsticker-id = ID этого стикера: { $id }\nsticker-id-extended =\n    ID цього стікера\n    { $id }\n    У даний момент тільки айді стікерів можна використовувати у будь-яких ботах.\n\nuser-id-hidden =\n    Цей користувач <b>приховав</b> свій айді при пересиланні.\n    <a href=\"https://telegram.org/blog/unsend-privacy-emoji#anonymous-forwarding\">Детальніше про цю фічу</a>.\n\n# Commands in UI (when you press \"/\" or \"Menu\" button)\ncmd-hint-id-pm = Дізнатись свій ID\ncmd-hint-id-group = Дізнатись ID цієї групи\ncmd-hint-help = Допомога ти вихідний код\n"
  },
  {
    "path": "bot/logs.py",
    "content": "import logging\nfrom json import dumps\n\nimport structlog\nfrom structlog import WriteLoggerFactory\n\nfrom bot.config_reader import LoggingSettings, LoggingRenderer\n\n\ndef get_structlog_config(config: LoggingSettings) -> dict:\n    return {\n        \"processors\": get_processors(config),\n        \"cache_logger_on_first_use\": True,\n        \"wrapper_class\": structlog.make_filtering_bound_logger(logging.getLevelName(config.level)),\n        \"logger_factory\": WriteLoggerFactory()\n    }\n\n\ndef get_processors(config: LoggingSettings) -> list:\n    def custom_json_serializer(data, *args, **kwargs):\n        result = dict()\n        # Set keys in specific order\n        for key in (\"timestamp\", \"level\", \"event\"):\n            if key in data:\n                result[key] = data.pop(key)\n\n        # Add all other fields\n        result.update(**data)\n        return dumps(result, default=str)\n\n    processors = [\n        structlog.processors.TimeStamper(fmt=config.format, utc=config.is_utc),\n        structlog.processors.add_log_level\n    ]\n\n    if config.renderer == LoggingRenderer.JSON:\n        processors.append(structlog.processors.JSONRenderer(serializer=custom_json_serializer))\n    else:\n        processors.append(structlog.dev.ConsoleRenderer())\n    return processors\n"
  },
  {
    "path": "bot/middlewares/__init__.py",
    "content": "from .l10n import L10nMiddleware\nfrom .log_unhandled import UnhandledUpdatesLoggerMiddleware\n\n__all__ = [\n    \"L10nMiddleware\",\n    \"UnhandledUpdatesLoggerMiddleware\"\n]\n"
  },
  {
    "path": "bot/middlewares/l10n.py",
    "content": "from typing import Any, Awaitable, Callable, Dict, Final\n\nfrom aiogram import BaseMiddleware\nfrom aiogram.enums import ChatType\nfrom aiogram.types import TelegramObject, User, Update\n\nfrom bot.fluent_helper import FluentDispenser\n\n\ndef is_pm(event: Update) -> bool:\n    return \\\n            (event.message and event.message.chat.type == ChatType.PRIVATE) or \\\n            (event.inline_query and event.inline_query.chat_type == ChatType.SENDER)\n\n\nclass L10nMiddleware(BaseMiddleware):\n    middleware_key: Final[str] = \"l10n\"\n\n    def __init__(self, dispenser: FluentDispenser):\n        self.dispenser = dispenser\n\n    async def __call__(\n            self,\n            handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],\n            event: TelegramObject,\n            data: Dict[str, Any],\n    ) -> Any:\n        event: Update\n        if is_pm(event):\n            user: User = data[\"event_from_user\"]\n            data[self.middleware_key] = self.dispenser.get_language(user.language_code)\n        else:\n            data[self.middleware_key] = self.dispenser.default_locale\n\n        return await handler(event, data)\n"
  },
  {
    "path": "bot/middlewares/log_unhandled.py",
    "content": "from typing import Any, Awaitable, Callable, Dict\n\nimport structlog\nfrom aiogram import BaseMiddleware\nfrom aiogram.dispatcher.event.bases import UNHANDLED\nfrom aiogram.types import TelegramObject\nfrom structlog.typing import FilteringBoundLogger\n\nlogger: FilteringBoundLogger = structlog.get_logger()\n\n\nclass UnhandledUpdatesLoggerMiddleware(BaseMiddleware):\n    async def __call__(\n            self,\n            handler: Callable[[TelegramObject, Dict[str, Any]], Awaitable[Any]],\n            event: TelegramObject,\n            data: Dict[str, Any],\n    ) -> Any:\n        result = await handler(event, data)\n        if result is UNHANDLED:\n            await logger.awarning(\n                \"Unhandled update\",\n                update=event.dict()\n            )\n"
  },
  {
    "path": "bot/migration_cache.py",
    "content": "from math import inf\nfrom cachetools import TTLCache\n\n\"\"\"\nThis is a simple TTL Cache, so that my_chat_member doesn't trigger on group to supergroup migration event\n\"\"\"\ncache = TTLCache(maxsize=inf, ttl=10.0)\n"
  },
  {
    "path": "bot/ui_commands.py",
    "content": "from aiogram import Bot\nfrom aiogram.types import BotCommand, BotCommandScopeAllPrivateChats, BotCommandScopeAllGroupChats\nfrom fluent.runtime import FluentLocalization\n\nfrom bot.fluent_helper import FluentDispenser\n\n\nasync def set_bot_commands(bot: Bot, dispenser: FluentDispenser) -> None:\n    \"\"\"\n    Set bot commands in UI (using Menu or \"/\" buttons)\n    :param bot: Bot object\n    :param dispenser: FluentDispenser object\n    \"\"\"\n    data = list()\n\n    for lang_key in dispenser.available_languages:\n        locale_object: FluentLocalization = dispenser.get_language(lang_key)\n\n        data.extend(\n            [\n                (\n                    [\n                        BotCommand(command=\"id\", description=locale_object.format_value(\"cmd-hint-id-pm\")),\n                        BotCommand(command=\"help\", description=locale_object.format_value(\"cmd-hint-help\")),\n                    ],\n                    BotCommandScopeAllPrivateChats(),\n                    lang_key\n                ),\n                (\n                    [BotCommand(command=\"id\", description=locale_object.format_value(\"cmd-hint-id-group\"))],\n                    BotCommandScopeAllGroupChats(),\n                    lang_key\n                ),\n            ]\n\n        )\n    for commands_list, commands_scope, language in data:\n        await bot.set_my_commands(commands=commands_list, scope=commands_scope, language_code=language)\n"
  },
  {
    "path": "docker-compose.example.yml",
    "content": "version: \"3.8\"\nservices:\n    bot:\n        image: groosha/my-id-bot:latest\n        restart: unless-stopped\n        env_file: .env\n        # uncomment and set your paths if you want to use your own locales\n#        volumes:\n#            -  \"./locales:/app/bot/locales\""
  },
  {
    "path": "env_example",
    "content": "# rename this file to .env (with the following dot)\n\n# Bot token, get one from https://t.me/botfather\nBOT_TOKEN=12345:abcxyz\n\n### Logging configuration ###\n\n# Render mode: pretty with \"console\", or more production-like with \"json\"\n# Warning: case-sensitive!\nLOGGING_RENDERER=console\n\n# Minimum logging level: DEBUG, INFO, WARNING, ERROR or CRITICAL\n# Warning: case-sensitive!\nLOGGING_LEVEL=INFO\n\n# Datetime format for logs\nLOGGING_FORMAT=%Y-%m-%d %H:%M:%S\n\n# true/yes/1 if you want to show time in UTC timezone instead of computer's local tz\nLOGGING_IS_UTC=no\n\n# true/yes/1 if you want to log unhandled updates (updates which your bot received but could not find handler to answer)\nLOGGING_LOG_UNHANDLED=no"
  },
  {
    "path": "my-id-bot.example.service",
    "content": "[Unit]\nDescription=Telegram My Id Bot\nAfter=network.target\n\n[Service]\nType=simple\nWorkingDirectory=/home/user/my-id-bot\nEnvironmentFile=/home/user/my-id-bot/.env\nExecStart=/home/user/my-id-bot/venv/bin/python -m bot\nKillMode=process\nRestart=always\nRestartSec=10\n\n[Install]\nWantedBy=multi-user.target"
  },
  {
    "path": "requirements.txt",
    "content": "# This file was autogenerated by uv via the following command:\n#    uv pip compile -o requirements.txt requirements.in\naiofiles==24.1.0\n    # via aiogram\naiogram==3.18.0\n    # via -r requirements.in\naiohappyeyeballs==2.4.6\n    # via aiohttp\naiohttp==3.11.13\n    # via aiogram\naiosignal==1.3.2\n    # via aiohttp\nannotated-types==0.7.0\n    # via pydantic\nattrs==25.1.0\n    # via\n    #   aiohttp\n    #   fluent-runtime\nbabel==2.17.0\n    # via fluent-runtime\ncachetools==5.5.2\n    # via -r requirements.in\ncertifi==2025.1.31\n    # via aiogram\nfluent-runtime==0.4.0\n    # via -r requirements.in\nfluent-syntax==0.19.0\n    # via fluent-runtime\nfrozenlist==1.5.0\n    # via\n    #   aiohttp\n    #   aiosignal\nidna==3.10\n    # via yarl\nmagic-filter==1.0.12\n    # via aiogram\nmultidict==6.1.0\n    # via\n    #   aiohttp\n    #   yarl\npropcache==0.3.0\n    # via\n    #   aiohttp\n    #   yarl\npydantic==2.10.6\n    # via\n    #   aiogram\n    #   pydantic-settings\npydantic-core==2.27.2\n    # via pydantic\npydantic-settings==2.4.0\n    # via -r requirements.in\npython-dotenv==1.0.1\n    # via pydantic-settings\npytz==2025.1\n    # via fluent-runtime\nstructlog==25.1.0\n    # via -r requirements.in\ntyping-extensions==4.12.2\n    # via\n    #   aiogram\n    #   fluent-runtime\n    #   fluent-syntax\n    #   pydantic\n    #   pydantic-core\nyarl==1.18.3\n    # via aiohttp\n"
  }
]