Более точное поведение при пересылке.

* другой алгоритм отслеживания изменённых сообщений.
По событиям вместо частых запросов.
* add: portable-install.sh
* add: docker-compose.yml
This commit is contained in:
Евгений Панков 2025-08-15 12:54:03 +03:00
parent 2bfa13b023
commit e5a37f33e5
7 changed files with 495 additions and 145 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
.env
venv*
python*
*.json
*.session

31
README.md Normal file
View File

@ -0,0 +1,31 @@
# Alerts_bot
Телеграм бот для пересылки постов по ключевым словам. Использует библиотеку Telethon и User bot API. То есть для работы бота нужен аккаунт пользователя телеграм.
## Установка, настройка и запуск
1. Запустить `portable_install.bat` для Виндовс или `portable_install.sh` для Linux.
Это скачает портбельный Пайтон и загрузит необходимые пакеты для работы. Создаст файл
2. Появится файл `run_me.bat` или `run-me.sh` соответсвенно для Windows или Linux
3. Нужно настроить файл `.env`
```.env
api_id=12345678
api_hash=1234567890....
session_name=userbot_session
source_channel_username=public_tg_channel
target_groups=1234567890,9876543210
groups_clean_srv_msgs=1234567890,9876543210 # если не указать, то берётся из target_groups
filter_keywords=триггерное выражение,слово,ещё слова
filter_negative_keywords=негативные выражения,исключение,то что не должно пересылаться
```
* api_id, api_hash получается на [my.telegram.org](https://my.telegram.org/auth) созданием приложения.
* Записав эти данные api_id, api_hash нужно получить список доступных чатов через
`python312\python.exe get_chat_list.py`.
Так же эта процедура создаст необходимый файл сессии.
Нужный чат для пересылки в него сообщений записать в `target_groups=` в файле `.env`.
Их может быть перечислено несколько через запятую.
* `source_channel_username` - публичная ссылка на исходный канал. Может сработать и chat_id из полуенного списка выше.
* `groups_clean_srv_msgs` Можно указать в каких группах ведётся очистка сервисных сообщений. По умолчанию в тех же что и target_groups
* Фильтры перечисляются через запятую без пробелов
3. После настройки запустить `run_me.bat` или `run-me.sh` соответсвенно для Windows или Linux
## Запуск на доккер
1. Нужно настроить локально по инструкции выше
2. Пример docker-compose содержится в `docker-compose.yml`. Проверить пути для файлов сессии и путь к коду, который будет подгужен в Volume.

29
docker-compose.yml Normal file
View File

@ -0,0 +1,29 @@
version: '3.8'
services:
app:
image: debian:bookworm-slim
container_name: alert-telethon-app
working_dir: /app
volumes:
- .:/app # весь проект
- ./userbot_session.session:/app/userbot_session.session # сессия (только файл)
env_file:
- .env # загружаем переменные
environment:
- TZ=Europe/Moscow # часовой пояс Москва
command: >
/bin/bash -c "
apt-get update &&
apt-get install -y --no-install-recommends curl tar bash &&
chmod +x portable_install.sh &&
./portable_install.sh &&
./run_me.sh
"
restart: unless-stopped
networks:
- alert-telethon_net
networks:
alert-telethon_net:
driver: bridge

View File

@ -8,9 +8,9 @@ print(f'Использую: {env}')
load_dotenv(env)
# === Данные юзер-бота ===
api_id=26507458
api_hash='9bf31965a06209eadd1995cec266d3ae'
session_name='Joshua_session'
api_id=os.getenv('api_id')
api_hash=os.getenv('api_hash')
session_name=os.getenv('session_name') or 'userbot'
client = TelegramClient(session_name, api_id, api_hash)
@ -19,6 +19,7 @@ async def get_group_info():
groups = await client.get_dialogs()
for dialog in groups:
if dialog.is_group or dialog.is_channel:
print(f'Название: {dialog.name} | ID: {dialog.entity.id}')
uname = dialog.entity.username or 'None'
print(f'Название: {dialog.name:25} | username: {uname:15} | ID: {dialog.entity.id}')
client.loop.run_until_complete(get_group_info())

407
main.py
View File

@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
# from __future__ import annotations
from telethon import TelegramClient, events
from telethon.tl.types import PeerUser, PeerChat, PeerChannel, Message
import re
import os
import json
@ -8,17 +10,14 @@ import logging
import asyncio
from datetime import datetime, timedelta
from dotenv import find_dotenv, load_dotenv
from typing import Optional
# ==== Настройка логирования ====
logging.basicConfig(
format='[%(levelname)s] %(asctime)s | %(message)s',
level=logging.INFO,
handlers=[
logging.StreamHandler() # Вывод в консоль
# logging.FileHandler("bot.log", encoding="utf-8") # Раскомментируй, чтобы сохранять логи в файл
]
)
log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)
console_handler = logging.StreamHandler()
console_handler.setFormatter(logging.Formatter('[%(levelname)s] %(asctime)s | %(message)s'))
log.addHandler(console_handler)
# === Загрузка переменных окружения ===
env = find_dotenv()
@ -28,19 +27,30 @@ load_dotenv(env)
# === Данные юзер-бота ===
api_id=os.getenv('api_id')
api_hash=os.getenv('api_hash')
session_name=os.getenv('session_name')
session_name=os.getenv('session_name') or 'userbot'
# === Настройки ===
source_channel_username=os.getenv('source_channel_username') # Канал, который слушаем
target_group_chat_id=int(os.getenv('target_group_chat_id')) # Группа, куда пересылаем
target_groups=os.getenv('target_groups').split(',') # Группы, куда пересылаем
for i, s in enumerate(target_groups):
if s.isdigit() or s.startswith('-') and s[1:].isdigit():
target_groups[i] = int(s)
target_entities=[]
groups_clean_srv_msgs=os.getenv('groups_clean_srv_msgs').split(',') # Группы, где удаляем сообщения о присоединившихся
if not groups_clean_srv_msgs:
groups_clean_srv_msgs = list(target_groups)
for i, s in enumerate(groups_clean_srv_msgs):
if s.isdigit() or s.startswith('-') and s[1:].isdigit():
groups_clean_srv_msgs[i] = int(s)
filter_keywords=os.getenv('filter_keywords') # Строки или регулярное выражение для поиска
filter_keywords=filter_keywords.split(',')
filter_negative_keywords=os.getenv('filter_negative_keywords') # Строки или регулярное выражение для поиска
filter_negative_keywords=filter_negative_keywords.split(',')
DATA_FILE=os.getenv('DATA_FILE') # Файл для сохранения сообщений
MAX_MESSAGES=int(os.getenv('MAX_MESSAGES')) # Количество хранящихся сообщений
CHECK_INTERVAL=int(os.getenv('CHECK_INTERVAL')) # Периодичность проверки удалений на канале
if not DATA_FILE:
DATA_FILE='messages.json'
MAX_MESSAGES = os.getenv('MAX_MESSAGES') # Количество хранящихся сообщений
MAX_MESSAGES=int(MAX_MESSAGES) if MAX_MESSAGES else 50
# =======================
@ -51,14 +61,17 @@ def load_state():
if os.path.exists(DATA_FILE):
with open(DATA_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
return [{},{}]
def save_state(data):
def save_state():
with open(DATA_FILE, 'w', encoding='utf-8') as f:
json.dump(data, f, ensure_ascii=False, indent=2)
json.dump([last_known_msg_id, source_messages], f, ensure_ascii=False, indent=2)
log.debug('Состояние сохранено')
# ==== Хранение данных ====
source_messages = load_state() # {msg_id: {'text': ..., 'forwarded_msg_id': ...}}
last_known_msg_id, source_messages = load_state()
# last_known_msg_id {chat_id: msg_id}
# source_messages {msg_id: {'text': ..., 'forwarded_msg_id': {chat_id: message_id}}}
# =======================
def shorten(text: str, length: int = 80) -> str:
@ -68,132 +81,244 @@ def shorten(text: str, length: int = 80) -> str:
return f'{text[:length-1]}'
return text
async def prepare_entities(target_groups: list) -> list:
"""
Обрабатывает список target_groups и возвращает список сущностей для рассылки сообщений.
:param client: Экземпляр TelegramClient
:param target_groups: Список, содержащий chat_id, пригласительные ссылки или имена пользователей
:return: Список сущностей, готовых для отправки сообщений
"""
for item in target_groups:
try:
# Проверяем, является ли элемент числом (chat_id)
if isinstance(item, (int, str)) and str(item).isdigit() or str(item).startswith('-') and str(item)[1:].isdigit():
# Если это chat_id, преобразуем в Peer
chat_id = int(item)
entity = await client.get_entity(chat_id)
target_entities.append(entity)
# Проверяем, является ли элемент username (начинается с @)
elif isinstance(item, str) and item.startswith('@'):
entity = await client.get_entity(item)
target_entities.append(entity)
# Проверяем, является ли элемент пригласительной ссылкой
elif isinstance(item, str) and (item.startswith('t.me/') or item.startswith('https://t.me/')):
# Удаляем https://, если есть
link = item.replace('https://', '')
# Проверяем, является ли ссылка на публичный канал/группу (@username)
if re.match(r'^t\.me/@[a-zA-Z0-9_]+$', link):
entity = await client.get_entity(link.replace('t.me/', ''))
target_entities.append(entity)
# Проверяем, является ли ссылка на приватный чат/канал (t.me/joinchat/XXXX или t.me/+XXXX)
elif re.match(r'^t\.me/(joinchat/|\+)[a-zA-Z0-9_-]+$', link):
# Для приватных чатов нужно сначала присоединиться
from telethon.tl.functions.messages import ImportChatInviteRequest
hash = link.split('/')[-1].replace('+', '')
try:
result = await client(ImportChatInviteRequest(hash))
entity = result.chats[0] # Получаем сущность чата
target_entities.append(entity)
except Exception as e:
print(f"Ошибка при попытке присоединиться к чату {link}: {e}")
else:
print(f"Неподдерживаемый формат ссылки: {item}")
else:
print(f"Неподдерживаемый формат элемента: {item}")
except ValueError as e:
print(f"Ошибка при обработке {item}: {e}")
except Exception as e:
print(f"Неизвестная ошибка при обработке {item}: {e}")
# return target_entities
async def forward_to_targets(
message: Message,
targets: Optional[list[str | int]] = None,
source_reply_to_msg_id: Optional[str | int] = None
) -> list[Message]:
# Пересылаем сообщения
new_msgs = []
for entity in (targets or target_entities):
if source_reply_to_msg_id and (forwarded_info:=source_messages.get(str(source_reply_to_msg_id))):
for str_chat_id, target_msg_id in forwarded_info['forwarded_msg_id'].items():
if str_chat_id == entity.chat.id:
link = await client.get_message_link(message)
text = f'Переслано из [{message.chat.title}]({link})\n{message.text}'
new_msg = await client.send_message(str_chat_id, text, reply_to=target_msg_id, parse_mode='markdown')
else:
new_msg = await client.forward_messages(entity, message)
new_msgs.append(new_msg)
# Запоминаем пересланные данные сообщения
source_messages[str(message.id)] = {
'text': message.text,
'timestamp': message.date.timestamp(),
'forwarded_msg_id': {str(msg.chat.id): msg.id for msg in new_msgs}
}
# Обновляем последние известные id для канала
for new_msg in new_msgs:
last_known_msg_id[str(new_msg.chat.id)] = new_msg.id
# Ограничиваем размер словаря
if len(source_messages) > MAX_MESSAGES:
oldest = min(source_messages.keys(), key=lambda m_id: source_messages[m_id]['timestamp'])
del source_messages[oldest]
# Сохраняем
save_state()
return new_msgs
# ==== Обработчик сообщений из получателей ====
@client.on(events.NewMessage(chats=target_entities))
async def handler(event: events.NewMessage.Event):
last_known_msg_id[str(event.chat.id)] = event.message.id
log.debug(f'last_known_msg_id[{event.chat.id}]={event.message.id} {event.chat.title}')
def check(text) -> Optional[bool]:
text = text or ""
if any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_keywords) \
and not any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_negative_keywords):
return True
# ==== Обработчик сообщений из источника ====
@client.on(events.NewMessage(chats=source_channel_username))
async def handler(event):
async def handler_NewMessages(event: events.NewMessage.Event):
message_text = event.message.text or ""
msg_id = event.message.id
last_known_msg_id[source_channel_username] = event.message.id
if message_text:
log.info(f"Новое сообщение: {shorten(message_text)}")
# Проверяем само сообщение
if any(re.search(keyword, message_text, re.IGNORECASE) for keyword in filter_keywords) \
and not any(re.search(keyword, message_text, re.IGNORECASE) for keyword in filter_negative_keywords):
new_msg = await client.forward_messages(target_group_chat_id, event.message)
source_messages[msg_id] = {
'text': message_text,
'timestamp': datetime.now().timestamp(),
'forwarded_msg_id': new_msg.id
}
# Ограничиваем размер словаря
if len(source_messages) > MAX_MESSAGES:
oldest = min(source_messages.keys(), key=lambda k: source_messages[k]['timestamp'])
del source_messages[oldest]
save_state(source_messages)
if check(message_text):
await forward_to_targets(event.message)
log.info(f"📩 Переслано: {shorten(message_text)}")
return
# Проверяем цепочку ответов
reply = event.message.reply_to
if reply and reply.reply_to_msg_id:
founded = await check_reply_chain(event, reply.reply_to_msg_id, depth=1)
if founded:
await client.forward_messages(target_group_chat_id, event.message)
log.info(f"⛓ Переслано по цепочке: {shorten(message_text)} | Глубина: {founded}")
source_reply_to_msg_id = await check_reply_chain(reply.reply_to_msg_id, depth=1)
if source_reply_to_msg_id:
await forward_to_targets(event.message, source_reply_to_msg_id=source_reply_to_msg_id)
log.info(f"⛓ Переслано по цепочке: {shorten(message_text)}")
# ==== Фоновая задача: проверка удалений и изменений ====
async def monitor_messages():
@client.on(events.MessageEdited(chats=source_channel_username))
async def handler_edited(event: events.MessageEdited.Event):
stored_m_info = source_messages.get(str(event.message.id))
if stored_m_info and stored_m_info['text'] != event.message.text:
log.info(f"📝 Сообщение изменено: {shorten(stored_m_info['text'])}{shorten(event.message.text)}")
# Редактируем пересланное сообщение
for str_chat_id, msg_id in stored_m_info['forwarded_msg_id'].items():
chat_id = int(str_chat_id)
# Получаем ID последнего сообщения в целевом чате
last_msg_id = last_known_msg_id.get(str_chat_id)
if not last_msg_id:
last_msg_id = await update_last_msg_id(chat_id)
log.debug(f'chat_id={chat_id}, stored_msg_id={msg_id}, last_msg_id_on_channel={last_msg_id}')
# Проверяем, является ли наше пересланное сообщение последним
if msg_id == last_msg_id:
# Это последнее сообщение — можно удалить и переслать заново
await client.delete_messages(chat_id, msg_id)
forward_to_targets(event.message, targets=[chat_id])
log.info(f"🔁 {chat_id} Сообщение было последним → Заменено")
else:
# Не последнее → отправляем изменённый текст как ответ
new_msg = await client.send_message(
chat_id,
f"🔄 Текст изменён:\n{event.message.text}",
reply_to=stored_m_info['forwarded_msg_id'][str_chat_id]
)
# Запоминаем пересланные данные сообщения
sm = source_messages.get(str(event.message.id))
if not sm:
source_messages[str(event.message.id)] = {
'text': event.message.text,
'timestamp': event.message.date.timestamp(),
'forwarded_msg_id': {str(new_msg.chat.id): new_msg.id}
}
else:
sm['text'] = event.message.text
sm['timestamp'] = event.message.date.timestamp()
sm['forwarded_msg_id'][str(new_msg.chat.id)] = new_msg.id
# Обновляем последние известные id для канала
last_known_msg_id[str(new_msg.chat.id)] = new_msg.id
log.info(f"💬 {chat_id} Сообщение не последнее → Отправлен ответ")
@client.on(events.MessageDeleted(chats=source_channel_username))
async def handler_deleted(event: events.MessageDeleted.Event):
for msg_id in event.deleted_ids:
stored_m_info = source_messages.get(str(msg_id))
if stored_m_info:
log.info(f"🗑 Исходное сообщение удалено. Удаляю пересланное: {shorten(stored_m_info['text'])}")
for str_chat_id, fwd_msg_id in tuple(stored_m_info['forwarded_msg_id'].items()):
chat_id = int(str_chat_id)
await client.delete_messages(chat_id, fwd_msg_id)
del stored_m_info['forwarded_msg_id'][str_chat_id]
if not stored_m_info['forwarded_msg_id']:
del source_messages[str(msg_id)]
async def get_batch_messages(chat_id, min_id, max_id):
chat_id = int(chat_id)
approx_count = max_id - min_id + 1
limit = int(approx_count * 1.05) # Увеличиваем лимит на 5%, чтобы точно зацепить все нужные
limit = min(limit, 3000) # Максимум 3000 (Telegram API limit)
log.debug(f"🔍 Запрашиваю {limit} сообщений для чата {chat_id} из диапазона {min_id}{max_id}")
try:
# Получаем batch сообщений
batch = await client.get_messages(chat_id, limit=limit)
batch_dict = {msg.id: msg for msg in batch}
return batch_dict
except Exception as e:
log.error(f"⚠ Ошибка при получении batch-сообщений: {e}")
return None
async def update_last_msg_id(chat_id: int|str) -> int:
# Получаем ID последнего сообщения в целевом чате
chat_id = int(chat_id)
last_msg = await client.get_messages(chat_id, limit=1)
last_msg_id = last_msg[0].id if last_msg else None
if last_msg_id:
last_known_msg_id[str(chat_id)] = last_msg_id
return last_msg_id
# ==== Фоновая задача: очистки старых сообщений ====
async def clear_old_messages():
while True:
await asyncio.sleep(CHECK_INTERVAL)
await asyncio.sleep(600) # раз в 10 минут
now = datetime.now().timestamp()
cutoff_time = now - 86400 # 24 часа
for str_msg_id, stored_m_info in list(source_messages.items()):
msg_id = int(str_msg_id)
# Удаление по возрасту
start_len = len(source_messages)
for str_msg_id, stored_m_info in tuple(source_messages.items()):
cutoff_time = now - 86400 # 24 час
if stored_m_info['timestamp'] < cutoff_time:
del source_messages[str_msg_id]
continue
# Получаем данные о пересланном сообщении
try:
target_msg = await client.get_messages(target_group_chat_id, ids=stored_m_info['forwarded_msg_id'])
except Exception as e:
log.warning(f"Не удалось получить пересланное сообщение {msg_id}: {e}")
target_msg = None
continue
# Если пересланного сообщения нет (удалено)
if target_msg is None or getattr(target_msg, 'empty', False):
log.info(f"🗑 Пересланное сообщение отсутвует (удалено администратором): {shorten(stored_m_info['text'])}")
del source_messages[str_msg_id]
continue
# Получаем сообщение из источника
try:
source_msg = await client.get_messages(source_channel_username, ids=msg_id)
except Exception as e:
log.warning(f"Не удалось получить исходное сообщение {msg_id}: {e}")
source_msg = None
continue
# Если исходного сообщения нет (удалено)
if source_msg is None or getattr(source_msg, 'empty', False):
log.info(f"🗑 Исходное сообщение удалено. Удаляю пересланное: {shorten(stored_m_info['text'])}")
await client.delete_messages(target_group_chat_id, msg_id)
del source_messages[str_msg_id]
continue
# Если текст изменился
if source_msg.text != target_msg.text:
log.info(f"📝 Сообщение изменено: {shorten(target_msg.text)}{shorten(source_msg.text)}")
# Редактируем пересланное сообщение
try:
# Получаем ID последнего сообщения в целевом чате
last_msg = await client.get_messages(target_group_chat_id, limit=1)
last_msg_id = last_msg[0].id if last_msg else None
# Проверяем, является ли наше пересланное сообщение последним
if target_msg.id == last_msg_id:
# Это последнее сообщение — можно удалить и переслать заново
await client.delete_messages(target_group_chat_id, target_msg.id)
new_msg = await client.forward_messages(target_group_chat_id, source_msg)
source_messages[str(source_msg.id)] = {
'text': source_msg.text,
'timestamp': datetime.now().timestamp(),
'forwarded_msg_id': new_msg.id
}
log.info("🔁 Сообщение было последним → Заменено")
else:
# Не последнее → отправляем изменённый текст как ответ
new_msg = await client.send_message(
target_group_chat_id,
f"🔄 Текст изменён:\n{source_msg.text}",
reply_to=stored_m_info['forwarded_msg_id']
)
source_messages[str(source_msg.id)] = {
'text': source_msg.text,
'timestamp': datetime.now().timestamp(),
'forwarded_msg_id': new_msg.id
}
log.info("💬 Сообщение не последнее → Отправлен ответ")
except Exception as e:
log.error(f"Не удалось обновить: {e}")
# Сохраняем состояние
save_state(source_messages)
if start_len != len(source_messages):
log.debug(f'Очищены старые сообщения: {start_len}{len(source_messages)}')
save_state()
async def check_reply_chain(event, msg_id, depth=1, max_depth=10) -> bool|int:
"""Рекурсивная проверка цепочки ответов"""
async def check_reply_chain(msg_id, depth=1, max_depth=10) -> bool|int:
"""Рекурсивная проверка цепочки ответов
В случае совпадения возвращает replied_msg.id"""
if depth > max_depth:
log.warning(f"🔁 Превышена максимальная глубина цепочки ({max_depth}), остановлено.")
return False
try:
replied_msg = await event.get_reply_message()
replied_msg = await client.get_messages(source_channel_username, ids=msg_id)
except Exception as e:
log.warning(f"Не удалось получить сообщение по ID {msg_id}: {e}")
return False
@ -203,19 +328,19 @@ async def check_reply_chain(event, msg_id, depth=1, max_depth=10) -> bool|int:
text = replied_msg.text or ""
log.info(f"🔍 Проверяю сообщение на уровне {depth}: {shorten(text,30)}")
if any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_keywords) \
and not any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_negative_keywords):
return depth
if check(text):
log.info(f"✔️ Есть совпадение на уровне {depth}: {shorten(text,30)}")
return replied_msg.id
else:
log.info(f"🔍 Проверяю сообщение на уровне {depth}: {shorten(text,30)}")
if replied_msg.reply_to:
return await check_reply_chain(event, replied_msg.reply_to.reply_to_msg_id, depth + 1)
return await check_reply_chain(replied_msg.reply_to.reply_to_msg_id, depth + 1)
return False
# ==== Удаление системных сообщений о входе/выходе в целевой группе ====
@client.on(events.ChatAction(chats=target_group_chat_id))
@client.on(events.ChatAction(chats=groups_clean_srv_msgs))
async def del_join_leave(event: events):
actions = {
'добавлен': event.user_added,
@ -227,16 +352,15 @@ async def del_join_leave(event: events):
action = ', '.join(action)
full_name = ' '.join([name for name in (event.user.first_name, event.user.last_name) if name])
await event.delete()
log.info(f"Удалено сообщение: Пользователь {full_name}({event.user_id}) {action} чат {target_group_chat_id}")
log.info(f"Удалено сообщение: Пользователь {full_name}({event.user_id}) {action} чат {event.chat.id}")
async def fetch_system_messages(days_to_check=5) -> tuple[int, int]:
"""Собирает системные сообщения за последние N дней"""
system_messages = []
async with client:
for chat_id in groups_clean_srv_msgs:
try:
chat = await client.get_entity(target_group_chat_id)
chat = await client.get_entity(chat_id)
except ValueError:
log.info("Не удалось найти чат. Убедитесь, что вы состоите в группе.")
return system_messages
@ -258,6 +382,7 @@ async def fetch_system_messages(days_to_check=5) -> tuple[int, int]:
system_messages.append((chat.id, message.id))
log.info(f"✅ Найдено {len(system_messages)} системных сообщений для удаления")
return system_messages
@ -279,7 +404,6 @@ async def delete_system_msgs(messages_list):
# ==== Запуск бота ====
async def main():
await client.start()
# log.info("🧹 Начинаем очистку старых системных сообщений…")
# messages = await fetch_system_messages(days_to_check=5)
@ -288,10 +412,29 @@ async def main():
# log.info("🔄 Клиент не подключён, пытаемся восстановить соединение…")
# await client.connect()
asyncio.create_task(monitor_messages())
log.info("😊 Бот запущен. Ожидание событий…")
await client.run_until_disconnected()
asyncio.create_task(clear_old_messages())
try:
await client.start()
await prepare_entities(target_groups)
log.info("Env Parameters:"
"\n"
f"\nsource_channel_username = {source_channel_username}"
f"\ntarget_groups = {target_groups}"
f"\ngroups_clean_srv_msgs = {groups_clean_srv_msgs}"
f"\nfilter_keywords = {filter_keywords}"
f"\nfilter_negative_keywords= {filter_negative_keywords}"
"\n"
"\n😊 Бот запущен. Ожидание событий…"
)
await client.run_until_disconnected()
except ConnectionError as e:
log.error(f"Ошибка подключения к Telegram: {e}. Повторная попытка через 10 секунд...")
await asyncio.sleep(10) # Подождать перед повторной попыткой
except KeyboardInterrupt:
log.info("🛑 Бот остановлен вручную.")
finally:
if client.is_connected():
await client.disconnect()
# =======================
if __name__ == '__main__':

View File

@ -101,13 +101,11 @@ if exist %python_dir%\Lib\site-packages\sitecustomize.py (
echo import site
echo import os
echo import sys
echo project_root = os.path.abspath^(os.path.join^(os.getenv^('VIRTUAL_ENV'^), '..'^)^)
echo if project_root in sys.path:
echo sys.path.remove^(project_root^)
echo sys.prefix = os.getenv^('VIRTUAL_ENV'^)
echo sys.exec_prefix = os.getenv^('VIRTUAL_ENV'^)
echo virtual_env = os.getenv^('VIRTUAL_ENV'^)
echo sys.prefix = virtual_env
echo sys.exec_prefix = virtual_env
echo def getsitepackages^(^):
echo return [os.path.join^(os.getenv^('VIRTUAL_ENV'^), 'Lib', 'site-packages'^)]
echo return [os.path.join^(virtual_env, 'Lib', 'site-packages'^)]
echo site.getsitepackages = getsitepackages
) > "%python_dir%\Lib\site-packages\sitecustomize.py"
:skip_sitecustomize
@ -156,7 +154,6 @@ echo [*] Создание %python_dir%\Scripts\activate.bat
echo set "_OLD_VIRTUAL_PATH=%%PATH%%"
echo ^)
echo set "PATH=%%VIRTUAL_ENV%%;%%VIRTUAL_ENV%%\Scripts;%%PATH%%"
echo set "PYTHONPATH=%%VIRTUAL_ENV%%\Lib\site-packages"
echo chcp %%_OLD_CODEPAGE%% ^> nul
) > %python_dir%\Scripts\activate.bat
:skip_venv
@ -169,6 +166,7 @@ if exist run_me.bat (
)
echo [+] Создание run_me.bat
(
echo @echo off
echo cd /d "%%~dp0%%"
echo call %python_dir%\Scripts\activate.bat
echo call python main.py

146
portable_install.sh Normal file
View File

@ -0,0 +1,146 @@
#!/bin/bash
# Настройки
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PYTHON_DIR="$SCRIPT_DIR/python312"
PYTHON_URL="https://github.com/astral-sh/python-build-standalone/releases/download/20250612/cpython-3.12.11+20250612-x86_64-unknown-linux-gnu-install_only_stripped.tar.gz"
PYTHON_EXE="$PYTHON_DIR/bin/python3.12"
# Цвета
GREEN='\033[32m'
YELLOW='\033[33m'
RED='\033[31m'
NC='\033[0m'
echo_color() {
echo -e "${1}${2}${NC}"
}
run_cmd() {
if ! eval "$1"; then
echo_color "$RED" "[-] Ошибка выполнения: $1"
exit 1
fi
}
# Проверка, установлен ли Python
if [[ -f "$PYTHON_EXE" ]]; then
echo_color "$GREEN" "[ ] Python уже установлен"
"$PYTHON_EXE" --version
else
echo_color "$YELLOW" "[*] Установка Python..."
mkdir -p "$PYTHON_DIR" || {
echo_color "$RED" "[-] Ошибка создания папки $PYTHON_DIR"
exit 1
}
curl -L -o python.tar.gz "$PYTHON_URL" || {
echo_color "$RED" "[-] Ошибка загрузки Python"
exit 1
}
tar --strip-components=1 -xzf python.tar.gz -C "$PYTHON_DIR" || {
echo_color "$RED" "[-] Ошибка распаковки"
rm -f python.tar.gz
exit 1
}
rm -f python.tar.gz
# Ссылка для удобства
ln -sf python3.12 "$PYTHON_DIR/bin/python"
echo_color "$GREEN" "[+] Python установлен"
# Установка pip
echo_color "$YELLOW" "[*] Установка pip"
curl -L -o get-pip.py 'https://bootstrap.pypa.io/get-pip.py' || {
echo_color "$RED" "[-] Не удалось скачать get-pip.py"
exit 1
}
"$PYTHON_EXE" get-pip.py --no-warn-script-location || {
echo_color "$RED" "[-] Ошибка установки pip"
rm -f get-pip.py
exit 1
}
rm -f get-pip.py
# Обновление базовых пакетов
echo_color "$YELLOW" "[*] Обновление pip, setuptools, wheel"
"$PYTHON_EXE" -m pip install --upgrade pip setuptools wheel --no-cache-dir || {
echo_color "$RED" "[-] Ошибка обновления"
exit 1
}
# Установка requirements.txt
if [[ -f "$SCRIPT_DIR/requirements.txt" ]]; then
echo_color "$YELLOW" "[*] Установка зависимостей"
"$PYTHON_EXE" -m pip install -r "$SCRIPT_DIR/requirements.txt" --no-cache-dir || {
echo_color "$RED" "[-] Ошибка при установке пакетов"
exit 1
}
fi
fi
# === Создание run_me.sh (без запуска!) ===
RUN_ME="$SCRIPT_DIR/run_me.sh"
if [[ -f "$RUN_ME" ]]; then
echo_color "$GREEN" "[ ] run_me.sh уже существует"
else
echo_color "$YELLOW" "[+] Создание run_me.sh"
cat > "$RUN_ME" << 'EOF'
#!/bin/bash
cd "$(dirname "$0")"
source python312/bin/activate
python main.py
EOF
chmod +x "$RUN_ME"
echo_color "$GREEN" "[✔] run_me.sh создан"
fi
# === Создание activate и pyvenv.cfg ===
ACTIVATE_SCRIPT="$PYTHON_DIR/bin/activate"
PYVENV_CFG="$PYTHON_DIR/pyvenv.cfg"
SITE_PACKAGES="$PYTHON_DIR/lib/python3.12/site-packages"
mkdir -p "$PYTHON_DIR/bin" "$SITE_PACKAGES"
# sitecustomize.py
cat > "$SITE_PACKAGES/sitecustomize.py" << 'EOF'
import site
import os
import sys
virtual_env = os.getenv("VIRTUAL_ENV")
if virtual_env:
sys.prefix = virtual_env
sys.exec_prefix = virtual_env
def getsitepackages():
return [os.path.join(virtual_env, "lib", "python3.12", "site-packages")]
site.getsitepackages = getsitepackages
EOF
# pyvenv.cfg
cat > "$PYVENV_CFG" << EOF
home = $SCRIPT_DIR/python312
include-system-site-packages = false
version = 3.12.11
executable = $PYTHON_DIR/bin/python3.12
command = $PYTHON_DIR/bin/python3.12
EOF
# activate
cat > "$ACTIVATE_SCRIPT" << 'EOF'
#!/bin/bash
export VIRTUAL_ENV="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV")
export _OLD_VIRTUAL_PATH="$PATH"
export PATH="$VIRTUAL_ENV/bin:$PATH"
unset PYTHONHOME
export PS1="($VIRTUAL_ENV_PROMPT) $PS1"
EOF
chmod +x "$ACTIVATE_SCRIPT"
echo_color "$GREEN" "[✔️] Установка завершена. Запуск через rum-me.sh"