Отправитель Телеграмм
Простой Telegram-бот на Python3, который автоматически пересылает сообщения из одного чата в другой.
Миграция с версии V1
v2 использует другой формат файла конфигурации. Для получения дополнительной информации обратитесь к разделу Конфигурация. Бот не запустится, если файл конфигурации имеет неправильный формат.
Запуск бота
После того как вы настроите конфигурацию (см. ниже), просто запустите:
форвардер python -m
или с помощью поэзии (рекомендуется)
экспедитор поэтического пробега
Настройка бота (перед запуском бота ознакомьтесь с приведенными ниже инструкциями!):
Telegram Forwarder поддерживает только Python 3.9 и более поздние версии.
Конфигурация
Для работы бота необходимы два файла .env и chat_list.json.
.env
Шаблон окружения можно найти в sample.env. Переименуйте его в .env и заполните значениями:
BOT_TOKEN— Токен Telegram-бота. Вы можете получить его у @BotFatherOWNER_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, перейдя в каталог проекта и выполнив команду:
поэтическая установка - только основная
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:
returndest = 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 Truereturn 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