October 20, 2025

Отправитель Телеграмм

Простой Telegram-бот на Python3, который автоматически пересылает сообщения из одного чата в другой.

Миграция с версии V1

v2 использует другой формат файла конфигурации. Для получения дополнительной информации обратитесь к разделу Конфигурация. Бот не запустится, если файл конфигурации имеет неправильный формат.

Запуск бота

После того как вы настроите конфигурацию (см. ниже), просто запустите:

форвардер python -m

или с помощью поэзии (рекомендуется)

экспедитор поэтического пробега

Настройка бота (перед запуском бота ознакомьтесь с приведенными ниже инструкциями!):

Telegram Forwarder поддерживает только Python 3.9 и более поздние версии.

Конфигурация

Для работы бота необходимы два файла .env и chat_list.json.

.env

Шаблон окружения можно найти в sample.env. Переименуйте его в .env и заполните значениями:

  • BOT_TOKEN — Токен Telegram-бота. Вы можете получить его у @BotFather
  • OWNER_ID — Целое число, состоящее из идентификатора вашего владельца.
  • REMOVE_TAG — установите значение True , если хотите удалить тег ("Переслано с xxxxx") из пересланного сообщения.

chat_list.json

Шаблон chat_list можно найти в chat_list.sample.json. Переименуйте его в chat_list.json.

Этот файл содержит список чатов, в которые нужно пересылать сообщения и из которых нужно пересылать сообщения. Бот ожидает, что это будет массив объектов со следующей структурой:

[
 {
 "источник": -10012345678,
 "получатель": [-10011111111, "-10022222222#123456"]
 }, 
{ 
 "источник": "-10087654321#000000", // Тема/Группа форума
    "пункт назначения": ["-10033333333#654321"], 
 "фильтры": ["word1", "word2"] // сообщение, содержащее это слово, будет переадресовано
 },
 {
 "источник": -10087654321,
 "получатель": [-10033333333],
 "черный список": ["слово3", "слово4"] // сообщения, содержащие это слово, не будут пересылаться
 },
 {
 "источник": -10087654321,
 "получатель": [-10033333333],
 "фильтры": ["word5"],
 "чёрный список": ["word6"]
 // для пересылки сообщение должно содержать слово5 и не должно содержать слово6
 }
]
  • source — Идентификатор чата, из которого нужно пересылать сообщения. Это может быть группа или канал.Если исходный чат является тематической группой, вы ДОЛЖНЫ явно указать идентификатор темы. Если идентификатор темы не указан, бот будет игнорировать входящие сообщения из тематической группы.
  • destination — Массив идентификаторов чатов для пересылки сообщений. Это может быть группа или канал.Destenation поддерживает чат Topics. Вы можете использовать строку #topicID для переадресации на определённую тему. Пример: [-10011111111, "-10022222222#123456"]. С этой настройкой переадресация будет осуществляться в чат -10022222222 с темой 123456 и в чат -10011111111 .
  • filters (Необязательно) — массив строк для фильтрации слов. Если сообщение содержит какую-либо из строк в массиве, оно БУДЕТ переслано.
  • blacklist (Необязательно) — массив строк для внесения слов в чёрный список. Если сообщение содержит какую-либо строку из массива, оно НЕ БУДЕТ переслано.

Вы можете добавить столько объектов, сколько захотите. Бот будет пересылать сообщения из всех чатов в поле source во все чаты в поле destination . Дубликаты допускаются, так как бот уже обрабатывает их.

Зависимости от Python

Установите необходимые зависимости Python, перейдя в каталог проекта и выполнив команду:

поэтическая установка - только основная

или с помощью pip

pip3 install -r requirements.txt

Это позволит установить все необходимые пакеты Python.

Запуск в контейнере Docker

Требования

  • Докер
  • создание докера

Перед запуском убедитесь, что все настройки выполнены (.env и chat_list.json)!

Затем просто введите команду:

настройка docker compose up -d

Вы можете просмотреть журналы с помощью команды:

docker создает журналы -f

Сам код:

✉forwarder

✉modules

↪️__init__.py

from forwarder import LOGGER
def __list_all_modules():
    import glob
    from os.path import basename, dirname, isfile
    # This generates a list of modules in this folder for the * in __main__ to work.
    mod_paths = glob.glob(dirname(__file__) + "/*.py")
    all_modules = [
        basename(f)[:-3]
        for f in mod_paths
        if isfile(f) and f.endswith(".py") and not f.endswith("__init__.py")
    ]
    return all_modules
ALL_MODULES = sorted(__list_all_modules())
LOGGER.info("Modules to load: " + str(ALL_MODULES))

↪️default.py

from telegram import Update
from telegram.ext import ContextTypes, CommandHandler, filters
from telegram.constants import ParseMode
from forwarder import bot, OWNER_ID
PM_START_TEXT = """
Hey {}, I'm {}!
I'm a bot used to forward messages from one chat to another.
To obtain a list of commands, use /help.
"""
PM_HELP_TEXT = """
Here is a list of usable commands:
 - /start : Starts the bot.
 - /help : Sends you this help message.
just send /id in private chat/group/channel and i will reply it's id.
"""
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
    chat = update.effective_chat
    message = update.effective_message
    user = update.effective_user
    if not (chat and message and user):
        return
    if chat.type == "private":
        await message.reply_text(
            PM_START_TEXT.format(user.first_name, context.bot.first_name),
            parse_mode=ParseMode.HTML,
        )
    else:
        await message.reply_text("I'm up and running!")
async def help(update: Update, _):
    chat = update.effective_chat
    message = update.effective_message
    if not (chat and message):
        return
    if not chat.type == "private":
        await message.reply_text("Contact me via PM to get a list of usable commands.")
    else:
        await message.reply_text(PM_HELP_TEXT)
bot.add_handler(CommandHandler("start", start, filters=filters.User(OWNER_ID)))
bot.add_handler(CommandHandler("help", help, filters=filters.User(OWNER_ID)))

↪️forward.py

import asyncio
from typing import Union, Optional
from telegram import Update, Message, MessageId
from telegram.error import ChatMigrated, RetryAfter
from telegram.ext import MessageHandler, filters, ContextTypes
from forwarder import bot, REMOVE_TAG, LOGGER
from forwarder.utils import get_destination, get_config, predicate_text
async def send_message(
    message: Message, chat_id: int, thread_id: Optional[int] = None
) -> Union[MessageId, Message]:
    if REMOVE_TAG:
        return await message.copy(chat_id, message_thread_id=thread_id)  # type: ignore
    return await message.forward(chat_id, message_thread_id=thread_id)  # type: ignore
async def forwarder(update: Update, _: ContextTypes.DEFAULT_TYPE) -> None:
    message = update.effective_message
    source = update.effective_chat
    if not message or not source:
        return
    dest = get_destination(source.id, message.message_thread_id)
    for config in dest:
        if config.filters:
            if not predicate_text(config.filters, message.text or ""):
                return
        if config.blacklist:
            if predicate_text(config.blacklist, message.text or ""):
                return
        for chat in config.destination:
            LOGGER.debug(f"Forwarding message {source.id} to {chat}")
            try:
                await send_message(message, chat.get_id(), chat.get_topic())
            except RetryAfter as err:
                LOGGER.warning(f"Rate limited, retrying in {err.retry_after} seconds")
                await asyncio.sleep(err.retry_after + 0.2)
                await send_message(message, chat.get_id(), thread_id=chat.get_topic())
            except ChatMigrated as err:
                await send_message(message, err.new_chat_id)
                LOGGER.warning(
                    f"Chat {chat} has been migrated to {err.new_chat_id}!! Edit the config file!!"
                )
            except Exception as err:
                LOGGER.error(f"Failed to forward message from {source.id} to {chat} due to {err}")
FORWARD_HANDLER = MessageHandler(
    filters.Chat([config.source.get_id() for config in get_config()])
    & ~filters.COMMAND
    & ~filters.StatusUpdate.ALL,
    forwarder,
)
bot.add_handler(FORWARD_HANDLER)

↪️misc.py

from telegram import Update
from telegram.constants import ParseMode
from telegram.ext import filters, MessageHandler
from forwarder import OWNER_ID, bot
async def get_id(update: Update, _):
    message = update.effective_message
    chat = update.effective_chat
    if not message or not chat:
        return
    if chat.type == "private":  # Private chat with the bot
        return await message.reply_text(f"🙋‍♂️ Your ID is `{chat.id}`")
    result = f"👥 Chat ID: `{chat.id}`"
    if chat.is_forum:
        result += f"\n💬 Forum/Topic ID: `{message.message_thread_id}`"
    if message.reply_to_message:
        forwarder = message.reply_to_message.from_user
        if message.reply_to_message.forward_from:  # Forwarded user
            sender = message.reply_to_message.forward_from
            result += f"🙋‍♂️ The original sender ({sender.first_name}), ID is: `{sender.id}`\n"
            result += f"⏩ The forwarder ({forwarder.first_name if forwarder else 'Unknown'}) ID: `{forwarder.id if forwarder else 'Unknown'}`"
        if message.reply_to_message.forward_from_chat:  # Forwarded channel
            channel = message.reply_to_message.forward_from_chat
            result += f"💬 The channel {channel.title} ID: `{channel.id}`\n"
            result += f"⏩ The forwarder ({forwarder.first_name if forwarder else 'Unknown'}) ID: `{forwarder.id if forwarder else 'Unknown'}`"
    return await message.reply_text(
        result,
        parse_mode=ParseMode.MARKDOWN,
    )
GET_ID_HANDLER = MessageHandler(
    filters.COMMAND & filters.Regex(r"^/id") & (filters.User(OWNER_ID) | filters.ChatType.CHANNEL),
    get_id,
)
bot.add_handler(GET_ID_HANDLER)

✉utils

↪️__init__.py

from .chat import *
from .message import *

↪️chat.py

from typing import List, List, Union, Optional
from forwarder import CONFIG
PARSED_CONFIG = []
class ChatConfig:
    __chat: Union[str, int]
    def __init__(self, chat_id: Union[str, int]):
        self.__chat = chat_id
    def __repr__(self) -> str:
        if self.is_topic:
            return f"{self.get_id()}#{self.get_topic()}"
        return str(self.get_id())
    @property
    def is_topic(self) -> bool:
        if isinstance(self.__chat, str) and len(self.__chat.split("#")) == 2:
            return True
        return False
    def get_topic(self) -> Optional[int]:
        if not self.is_topic:
            return None
        if isinstance(self.__chat, int):
            return None
        return int(self.__chat.split("#")[1])
    def get_id(self) -> int:
        if isinstance(self.__chat, int):
            return self.__chat
        return int(self.__chat.split("#")[0])
class ForwardConfig:
    source: ChatConfig
    destination: List[ChatConfig]
    filters: Optional[List[str]]
    blacklist: Optional[List[str]]
    def __init__(
        self,
        source: Union[str, int],
        destination: List[Union[str, int]],
        filters: Optional[List[str]] = None,
        blacklist: Optional[List[str]] = None,
    ):
        self.source = ChatConfig(source)
        self.destination = [ChatConfig(item) for item in destination]
        self.filters = filters
        self.blacklist = blacklist
def get_config() -> List[ForwardConfig]:
    global PARSED_CONFIG
    if PARSED_CONFIG:
        return PARSED_CONFIG
    PARSED_CONFIG = [
        ForwardConfig(
            source=chat["source"],
            destination=chat["destination"],
            filters=chat.get("filters"),
            blacklist=chat.get("blacklist"),
        )
        for chat in CONFIG
    ]
    return PARSED_CONFIG
def get_destination(chat_id: int, topic_id: Optional[int] = None) -> List[ForwardConfig]:
    """Get destination from a specific source chat
    Args:
        chat_id (`int`): source chat id
        topic_id (`Optional[int]`): source topic id. Defaults to None.
    """
    dest: List[ForwardConfig] = []
    for chat in get_config():
        if chat.source.get_id() == chat_id and chat.source.get_topic() == topic_id:
            dest.append(chat)
    return dest

↪️message.py

import re
from typing import List
def predicate_text(filters: List[str], text: str) -> bool:
    """Check if the text contains any of the filters"""
    for i in filters:
        pattern = r"( |^|[^\w])" + re.escape(i) + r"( |$|[^\w])"
        if re.search(pattern, text, flags=re.IGNORECASE):
            return True
    return False

↪️__init__.py

import logging
import json
from os import getenv, path
from dotenv import load_dotenv
from telegram.ext import ApplicationBuilder
load_dotenv(".env")
logging.basicConfig(
    format="[ %(asctime)s: %(levelname)-8s ] %(name)-20s - %(message)s",
    level=logging.INFO,
)
LOGGER = logging.getLogger(__name__)
httpx_logger = logging.getLogger('httpx')
httpx_logger.setLevel(logging.WARNING)
# load json file
config_name = "chat_list.json"
if not path.isfile(config_name):
    LOGGER.error("No chat_list.json config file found! Exiting...")
    exit(1)
with open(config_name, "r") as data:
    CONFIG = json.load(data)
BOT_TOKEN = getenv("BOT_TOKEN")
if not BOT_TOKEN:
    LOGGER.error("No BOT_TOKEN token provided!")
    exit(1)
OWNER_ID = int(getenv("OWNER_ID", "0"))
REMOVE_TAG = getenv("REMOVE_TAG", "False") in {"true", "True", 1}
bot = ApplicationBuilder().token(BOT_TOKEN).build()

↪️__main__.py

from forwarder import main
if __name__ == "__main__":
    main.run()

↪️main.py

import importlib
from forwarder import LOGGER, bot
from forwarder.modules import ALL_MODULES
for module in ALL_MODULES:
    importlib.import_module("forwarder.modules." + module)
def run():
    LOGGER.info("Successfully loaded modules: " + str(ALL_MODULES))
    LOGGER.info("Starting bot...")
    bot.run_polling()

↪️.dockerignore

.git
__pycache__
/chat_list.json
/.env

↪️.gitignore

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
#  Usually these files are written by a python script from a template
#  before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
# Translations
*.mo
*.pot
# Django stuff:
*.log
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Visual Studio Code files
\.vscode/
# Bot config file
# DO NOT REMOVE THIS FILE FROM .gitignore
.env
chat_list.json

↪️Dockerfile

FROM python:3.10-slim-bullseye
WORKDIR /app
RUN pip install poetry
RUN poetry config virtualenvs.create false
COPY pyproject.toml poetry.lock ./
RUN poetry install --no-root --only main
COPY . .
RUN poetry install --only main
CMD ["poetry", "run", "forwarder"]

↪️chat_list.sample.json

[
    {
        "source": -10012345678,
        "destination": [-10098765432, -10012345678]
    },
    {
        "source": -10098765432,
        "destination": [-10012345678, "-10012345678#98765"],
        "filters": ["word1", "word2"]
    },
    {
        "source": -10087654321,
        "destination": [-10033333333],
        "blacklist": ["word3", "word4"]
    },
    {
        "source": -10087654321,
        "destination": [-10033333333],
        "filters": ["word5"],
        "blacklist": ["word6"]
    }
]

↪️docker-compose.yml

services:
  telegram-forwarder:
    container_name: telegram-forwarder
    image: telegram-forwarder-bot
    restart: unless-stopped
    build:
      context: .
      dockerfile: Dockerfile
    env_file:
      - .env
    volumes:
      - ./chat_list.json:/app/chat_list.json

↪️poetry.lock

# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand.
[[package]]
name = "anyio"
version = "4.0.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
    {file = "anyio-4.0.0-py3-none-any.whl", hash = "sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f"},
    {file = "anyio-4.0.0.tar.gz", hash = "sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a"},
]
[package.dependencies]
exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""}
idna = ">=2.8"
sniffio = ">=1.1"
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)"]
test = ["anyio[trio]", "coverage[toml] (>=7)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"]
trio = ["trio (>=0.22)"]
[[package]]
name = "black"
version = "23.9.0"
description = "The uncompromising code formatter."
category = "dev"
optional = false
python-versions = ">=3.8"
files = [
    {file = "black-23.9.0-py3-none-any.whl", hash = "sha256:9366c1f898981f09eb8da076716c02fd021f5a0e63581c66501d68a2e4eab844"},
    {file = "black-23.9.0.tar.gz", hash = "sha256:3511c8a7e22ce653f89ae90dfddaf94f3bb7e2587a245246572d3b9c92adf066"},
]
[package.dependencies]
click = ">=8.0.0"
mypy-extensions = ">=0.4.3"
packaging = ">=22.0"
pathspec = ">=0.9.0"
platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
[package.extras]
colorama = ["colorama (>=0.4.3)"]
d = ["aiohttp (>=3.7.4)"]
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
uvloop = ["uvloop (>=0.15.2)"]
[[package]]
name = "certifi"
version = "2023.7.22"
description = "Python package for providing Mozilla's CA Bundle."
category = "main"
optional = false
python-versions = ">=3.6"
files = [
    {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"},
    {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"},
]
[[package]]
name = "click"
version = "8.1.7"
description = "Composable command line interface toolkit"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
    {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
]
[package.dependencies]
colorama = {version = "*", markers = "platform_system == \"Windows\""}
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
category = "dev"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
    {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
    {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "exceptiongroup"
version = "1.1.3"
description = "Backport of PEP 654 (exception groups)"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
    {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"},
    {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"},
]
[package.extras]
test = ["pytest (>=6)"]
[[package]]
name = "h11"
version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
    {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
    {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
]
[[package]]
name = "httpcore"
version = "0.17.3"
description = "A minimal low-level HTTP client."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
    {file = "httpcore-0.17.3-py3-none-any.whl", hash = "sha256:c2789b767ddddfa2a5782e3199b2b7f6894540b17b16ec26b2c4d8e103510b87"},
    {file = "httpcore-0.17.3.tar.gz", hash = "sha256:a6f30213335e34c1ade7be6ec7c47f19f50c56db36abef1a9dfa3815b1cb3888"},
]
[package.dependencies]
anyio = ">=3.0,<5.0"
certifi = "*"
h11 = ">=0.13,<0.15"
sniffio = ">=1.0.0,<2.0.0"
[package.extras]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "httpx"
version = "0.24.1"
description = "The next generation HTTP client."
category = "main"
optional = false
python-versions = ">=3.7"
files = [
    {file = "httpx-0.24.1-py3-none-any.whl", hash = "sha256:06781eb9ac53cde990577af654bd990a4949de37a28bdb4a230d434f3a30b9bd"},
    {file = "httpx-0.24.1.tar.gz", hash = "sha256:5853a43053df830c20f8110c5e69fe44d035d850b2dfe795e196f00fdb774bdd"},
]
[package.dependencies]
certifi = "*"
httpcore = ">=0.15.0,<0.18.0"
idna = "*"
sniffio = "*"
[package.extras]
brotli = ["brotli", "brotlicffi"]
cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (>=1.0.0,<2.0.0)"]
[[package]]
name = "idna"
version = "3.4"
description = "Internationalized Domain Names in Applications (IDNA)"
category = "main"
optional = false
python-versions = ">=3.5"
files = [
    {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"},
    {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"},
]
[[package]]
name = "isort"
version = "5.12.0"
description = "A Python utility / library to sort Python imports."
category = "dev"
optional = false
python-versions = ">=3.8.0"
files = [
    {file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
    {file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
]
[package.extras]
colors = ["colorama (>=0.4.3)"]
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
plugins = ["setuptools"]
requirements-deprecated-finder = ["pip-api", "pipreqs"]
[[package]]
name = "mypy-extensions"
version = "1.0.0"
description = "Type system extensions for programs checked with the mypy type checker."
category = "dev"
optional = false
python-versions = ">=3.5"
files = [
    {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
    {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
]
[[package]]
name = "packaging"
version = "23.1"
description = "Core utilities for Python packages"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
    {file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
]
[[package]]
name = "pathspec"
version = "0.11.2"
description = "Utility library for gitignore style pattern matching of file paths."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "pathspec-0.11.2-py3-none-any.whl", hash = "sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20"},
    {file = "pathspec-0.11.2.tar.gz", hash = "sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3"},
]
[[package]]
name = "platformdirs"
version = "3.10.0"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "platformdirs-3.10.0-py3-none-any.whl", hash = "sha256:d7c24979f292f916dc9cbf8648319032f551ea8c49a4c9bf2fb556a02070ec1d"},
    {file = "platformdirs-3.10.0.tar.gz", hash = "sha256:b45696dab2d7cc691a3226759c0d3b00c47c8b6e293d96f6436f733303f77f6d"},
]
[package.extras]
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"]
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"]
[[package]]
name = "python-dotenv"
version = "1.0.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
    {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"},
    {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"},
]
[package.extras]
cli = ["click (>=5.0)"]
[[package]]
name = "python-telegram-bot"
version = "20.5"
description = "We have made you a wrapper you can't refuse"
category = "main"
optional = false
python-versions = ">=3.8"
files = [
    {file = "python-telegram-bot-20.5.tar.gz", hash = "sha256:2f45a94c861cbd40440ece2be176ef0fc69e10d84e6dfa17f9a456e32aeece13"},
    {file = "python_telegram_bot-20.5-py3-none-any.whl", hash = "sha256:fc9605a855794231c802cc3948e6f7c319a817b5cd1827371f170bc7ca0ca279"},
]
[package.dependencies]
httpx = ">=0.24.1,<0.25.0"
[package.extras]
all = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "cryptography (>=39.0.1)", "httpx[http2]", "httpx[socks]", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"]
callback-data = ["cachetools (>=5.3.1,<5.4.0)"]
ext = ["APScheduler (>=3.10.4,<3.11.0)", "aiolimiter (>=1.1.0,<1.2.0)", "cachetools (>=5.3.1,<5.4.0)", "pytz (>=2018.6)", "tornado (>=6.2,<7.0)"]
http2 = ["httpx[http2]"]
job-queue = ["APScheduler (>=3.10.4,<3.11.0)", "pytz (>=2018.6)"]
passport = ["cryptography (>=39.0.1)"]
rate-limiter = ["aiolimiter (>=1.1.0,<1.2.0)"]
socks = ["httpx[socks]"]
webhooks = ["tornado (>=6.2,<7.0)"]
[[package]]
name = "sniffio"
version = "1.3.0"
description = "Sniff out which async library your code is running under"
category = "main"
optional = false
python-versions = ">=3.7"
files = [
    {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"},
    {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"},
]
[[package]]
name = "tomli"
version = "2.0.1"
description = "A lil' TOML parser"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]
[[package]]
name = "typing-extensions"
version = "4.7.1"
description = "Backported and Experimental Type Hints for Python 3.7+"
category = "dev"
optional = false
python-versions = ">=3.7"
files = [
    {file = "typing_extensions-4.7.1-py3-none-any.whl", hash = "sha256:440d5dd3af93b060174bf433bccd69b0babc3b15b1a8dca43789fd7f61514b36"},
    {file = "typing_extensions-4.7.1.tar.gz", hash = "sha256:b75ddc264f0ba5615db7ba217daeb99701ad295353c45f9e95963337ceeeffb2"},
]
[metadata]
lock-version = "2.0"
python-versions = "^3.9"
content-hash = "75ecc460179a48fc1e03d65d5e35ddfceb5a814d89703bddfa3d447eefaf2ace"

↪️pyproject.toml

[tool.poetry]
name = "telegram-forwarder"
version = "2.3.0"
description = ""
authors = ["mrmissx <hi@mrmiss.my.id>"]
license = "GNU General Public License v3.0"
readme = "README.md"
packages = [{ include = "forwarder" }]
[tool.poetry.scripts]
forwarder = "forwarder.main:run"
[tool.poetry.dependencies]
python = "^3.9"
python-telegram-bot = "^20.5"
python-dotenv = "^1.0.0"
[tool.poetry.group.dev.dependencies]
black = "^23.3.0"
isort = "^5.12.0"
[tool.black]
line-length = 100
target-version = ["py38"]
#
# Isort Config
#
[tool.isort]
profile = "black"
known_third_party = ["telegram", "dotenv"]
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

↪️requirements.txt

anyio==3.6.2 ; python_version >= "3.9" and python_version < "4.0"
certifi==2022.12.7 ; python_version >= "3.9" and python_version < "4.0"
h11==0.14.0 ; python_version >= "3.9" and python_version < "4.0"
httpcore==0.16.3 ; python_version >= "3.9" and python_version < "4.0"
httpx==0.23.3 ; python_version >= "3.9" and python_version < "4.0"
idna==3.4 ; python_version >= "3.9" and python_version < "4.0"
python-dotenv==1.0.0 ; python_version >= "3.9" and python_version < "4.0"
python-telegram-bot==20.2 ; python_version >= "3.9" and python_version < "4.0"
rfc3986[idna2008]==1.5.0 ; python_version >= "3.9" and python_version < "4.0"
sniffio==1.3.0 ; python_version >= "3.9" and python_version < "4.0"

↪️sample.env

BOT_TOKEN=your_bot_token
OWNER_ID=your_telegram_id
REMOVE_TAG=True