Более точное поведение при пересылке.
* другой алгоритм отслеживания изменённых сообщений. По событиям вместо частых запросов. * add: portable-install.sh * add: docker-compose.yml
This commit is contained in:
parent
2bfa13b023
commit
3913c6ce79
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,3 +1,5 @@
|
|||||||
.env
|
.env
|
||||||
venv*
|
venv*
|
||||||
python*
|
python*
|
||||||
|
*.json
|
||||||
|
*.session
|
||||||
|
|||||||
31
README.md
Normal file
31
README.md
Normal 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
29
docker-compose.yml
Normal 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
|
||||||
@ -8,9 +8,9 @@ print(f'Использую: {env}')
|
|||||||
load_dotenv(env)
|
load_dotenv(env)
|
||||||
|
|
||||||
# === Данные юзер-бота ===
|
# === Данные юзер-бота ===
|
||||||
api_id=26507458
|
api_id=os.getenv('api_id')
|
||||||
api_hash='9bf31965a06209eadd1995cec266d3ae'
|
api_hash=os.getenv('api_hash')
|
||||||
session_name='Joshua_session'
|
session_name=os.getenv('session_name') or 'userbot'
|
||||||
|
|
||||||
client = TelegramClient(session_name, api_id, api_hash)
|
client = TelegramClient(session_name, api_id, api_hash)
|
||||||
|
|
||||||
@ -19,6 +19,7 @@ async def get_group_info():
|
|||||||
groups = await client.get_dialogs()
|
groups = await client.get_dialogs()
|
||||||
for dialog in groups:
|
for dialog in groups:
|
||||||
if dialog.is_group or dialog.is_channel:
|
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())
|
client.loop.run_until_complete(get_group_info())
|
||||||
407
main.py
407
main.py
@ -1,6 +1,8 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
# from __future__ import annotations
|
||||||
|
|
||||||
from telethon import TelegramClient, events
|
from telethon import TelegramClient, events
|
||||||
|
from telethon.tl.types import PeerUser, PeerChat, PeerChannel, Message
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
@ -8,17 +10,14 @@ import logging
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from dotenv import find_dotenv, load_dotenv
|
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 = 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()
|
env = find_dotenv()
|
||||||
@ -28,19 +27,30 @@ load_dotenv(env)
|
|||||||
# === Данные юзер-бота ===
|
# === Данные юзер-бота ===
|
||||||
api_id=os.getenv('api_id')
|
api_id=os.getenv('api_id')
|
||||||
api_hash=os.getenv('api_hash')
|
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') # Канал, который слушаем
|
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=os.getenv('filter_keywords') # Строки или регулярное выражение для поиска
|
||||||
filter_keywords=filter_keywords.split(',')
|
filter_keywords=filter_keywords.split(',')
|
||||||
filter_negative_keywords=os.getenv('filter_negative_keywords') # Строки или регулярное выражение для поиска
|
filter_negative_keywords=os.getenv('filter_negative_keywords') # Строки или регулярное выражение для поиска
|
||||||
filter_negative_keywords=filter_negative_keywords.split(',')
|
filter_negative_keywords=filter_negative_keywords.split(',')
|
||||||
DATA_FILE=os.getenv('DATA_FILE') # Файл для сохранения сообщений
|
DATA_FILE=os.getenv('DATA_FILE') # Файл для сохранения сообщений
|
||||||
MAX_MESSAGES=int(os.getenv('MAX_MESSAGES')) # Количество хранящихся сообщений
|
if not DATA_FILE:
|
||||||
CHECK_INTERVAL=int(os.getenv('CHECK_INTERVAL')) # Периодичность проверки удалений на канале
|
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):
|
if os.path.exists(DATA_FILE):
|
||||||
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
with open(DATA_FILE, 'r', encoding='utf-8') as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
return {}
|
return [{},{}]
|
||||||
|
|
||||||
def save_state(data):
|
def save_state():
|
||||||
with open(DATA_FILE, 'w', encoding='utf-8') as f:
|
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:
|
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 f'{text[:length-1]}…'
|
||||||
return text
|
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))
|
@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 ""
|
message_text = event.message.text or ""
|
||||||
msg_id = event.message.id
|
last_known_msg_id[source_channel_username] = event.message.id
|
||||||
|
|
||||||
if message_text:
|
if message_text:
|
||||||
log.info(f"Новое сообщение: {shorten(message_text)}")
|
log.info(f"Новое сообщение: {shorten(message_text)}")
|
||||||
|
|
||||||
# Проверяем само сообщение
|
# Проверяем само сообщение
|
||||||
if any(re.search(keyword, message_text, re.IGNORECASE) for keyword in filter_keywords) \
|
if check(message_text):
|
||||||
and not any(re.search(keyword, message_text, re.IGNORECASE) for keyword in filter_negative_keywords):
|
await forward_to_targets(event.message)
|
||||||
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)
|
|
||||||
log.info(f"📩 Переслано: {shorten(message_text)}")
|
log.info(f"📩 Переслано: {shorten(message_text)}")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Проверяем цепочку ответов
|
# Проверяем цепочку ответов
|
||||||
reply = event.message.reply_to
|
reply = event.message.reply_to
|
||||||
if reply and reply.reply_to_msg_id:
|
if reply and reply.reply_to_msg_id:
|
||||||
founded = await check_reply_chain(event, reply.reply_to_msg_id, depth=1)
|
source_reply_to_msg_id = await check_reply_chain(reply.reply_to_msg_id, depth=1)
|
||||||
if founded:
|
if source_reply_to_msg_id:
|
||||||
await client.forward_messages(target_group_chat_id, event.message)
|
await forward_to_targets(event.message, source_reply_to_msg_id=source_reply_to_msg_id)
|
||||||
log.info(f"⛓ Переслано по цепочке: {shorten(message_text)} | Глубина: {founded}")
|
log.info(f"⛓ Переслано по цепочке: {shorten(message_text)}")
|
||||||
|
|
||||||
|
|
||||||
# ==== Фоновая задача: проверка удалений и изменений ====
|
@client.on(events.MessageEdited(chats=source_channel_username))
|
||||||
async def monitor_messages():
|
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:
|
while True:
|
||||||
await asyncio.sleep(CHECK_INTERVAL)
|
await asyncio.sleep(600) # раз в 10 минут
|
||||||
|
|
||||||
now = datetime.now().timestamp()
|
now = datetime.now().timestamp()
|
||||||
cutoff_time = now - 86400 # 24 часа
|
cutoff_time = now - 86400 # 24 час
|
||||||
|
start_len = len(source_messages)
|
||||||
for str_msg_id, stored_m_info in list(source_messages.items()):
|
for str_msg_id, stored_m_info in tuple(source_messages.items()):
|
||||||
msg_id = int(str_msg_id)
|
|
||||||
|
|
||||||
# Удаление по возрасту
|
|
||||||
if stored_m_info['timestamp'] < cutoff_time:
|
if stored_m_info['timestamp'] < cutoff_time:
|
||||||
del source_messages[str_msg_id]
|
del source_messages[str_msg_id]
|
||||||
continue
|
if start_len != len(source_messages):
|
||||||
|
log.debug(f'Очищены старые сообщения: {start_len} → {len(source_messages)}')
|
||||||
# Получаем данные о пересланном сообщении
|
save_state()
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
if depth > max_depth:
|
||||||
log.warning(f"🔁 Превышена максимальная глубина цепочки ({max_depth}), остановлено.")
|
log.warning(f"🔁 Превышена максимальная глубина цепочки ({max_depth}), остановлено.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
replied_msg = await event.get_reply_message()
|
replied_msg = await client.get_messages(source_channel_username, ids=msg_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning(f"⚠ Не удалось получить сообщение по ID {msg_id}: {e}")
|
log.warning(f"⚠ Не удалось получить сообщение по ID {msg_id}: {e}")
|
||||||
return False
|
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 ""
|
text = replied_msg.text or ""
|
||||||
|
|
||||||
log.info(f"🔍 Проверяю сообщение на уровне {depth}: {shorten(text,30)}")
|
if check(text):
|
||||||
|
log.info(f"✔️ Есть совпадение на уровне {depth}: {shorten(text,30)}")
|
||||||
if any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_keywords) \
|
return replied_msg.id
|
||||||
and not any(re.search(keyword, text, re.IGNORECASE) for keyword in filter_negative_keywords):
|
else:
|
||||||
return depth
|
log.info(f"🔍 Проверяю сообщение на уровне {depth}: {shorten(text,30)}")
|
||||||
|
|
||||||
if replied_msg.reply_to:
|
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
|
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):
|
async def del_join_leave(event: events):
|
||||||
actions = {
|
actions = {
|
||||||
'добавлен': event.user_added,
|
'добавлен': event.user_added,
|
||||||
@ -227,16 +352,15 @@ async def del_join_leave(event: events):
|
|||||||
action = ', '.join(action)
|
action = ', '.join(action)
|
||||||
full_name = ' '.join([name for name in (event.user.first_name, event.user.last_name) if name])
|
full_name = ' '.join([name for name in (event.user.first_name, event.user.last_name) if name])
|
||||||
await event.delete()
|
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]:
|
async def fetch_system_messages(days_to_check=5) -> tuple[int, int]:
|
||||||
"""Собирает системные сообщения за последние N дней"""
|
"""Собирает системные сообщения за последние N дней"""
|
||||||
system_messages = []
|
system_messages = []
|
||||||
|
for chat_id in groups_clean_srv_msgs:
|
||||||
async with client:
|
|
||||||
try:
|
try:
|
||||||
chat = await client.get_entity(target_group_chat_id)
|
chat = await client.get_entity(chat_id)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
log.info("❌ Не удалось найти чат. Убедитесь, что вы состоите в группе.")
|
log.info("❌ Не удалось найти чат. Убедитесь, что вы состоите в группе.")
|
||||||
return system_messages
|
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))
|
system_messages.append((chat.id, message.id))
|
||||||
|
|
||||||
log.info(f"✅ Найдено {len(system_messages)} системных сообщений для удаления")
|
log.info(f"✅ Найдено {len(system_messages)} системных сообщений для удаления")
|
||||||
|
|
||||||
return system_messages
|
return system_messages
|
||||||
|
|
||||||
|
|
||||||
@ -279,7 +404,6 @@ async def delete_system_msgs(messages_list):
|
|||||||
|
|
||||||
# ==== Запуск бота ====
|
# ==== Запуск бота ====
|
||||||
async def main():
|
async def main():
|
||||||
await client.start()
|
|
||||||
|
|
||||||
# log.info("🧹 Начинаем очистку старых системных сообщений…")
|
# log.info("🧹 Начинаем очистку старых системных сообщений…")
|
||||||
# messages = await fetch_system_messages(days_to_check=5)
|
# messages = await fetch_system_messages(days_to_check=5)
|
||||||
@ -288,10 +412,29 @@ async def main():
|
|||||||
# log.info("🔄 Клиент не подключён, пытаемся восстановить соединение…")
|
# log.info("🔄 Клиент не подключён, пытаемся восстановить соединение…")
|
||||||
# await client.connect()
|
# await client.connect()
|
||||||
|
|
||||||
asyncio.create_task(monitor_messages())
|
asyncio.create_task(clear_old_messages())
|
||||||
|
try:
|
||||||
log.info("😊 Бот запущен. Ожидание событий…")
|
await client.start()
|
||||||
await client.run_until_disconnected()
|
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__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
@ -101,13 +101,11 @@ if exist %python_dir%\Lib\site-packages\sitecustomize.py (
|
|||||||
echo import site
|
echo import site
|
||||||
echo import os
|
echo import os
|
||||||
echo import sys
|
echo import sys
|
||||||
echo project_root = os.path.abspath^(os.path.join^(os.getenv^('VIRTUAL_ENV'^), '..'^)^)
|
echo virtual_env = os.getenv^('VIRTUAL_ENV'^)
|
||||||
echo if project_root in sys.path:
|
echo sys.prefix = virtual_env
|
||||||
echo sys.path.remove^(project_root^)
|
echo sys.exec_prefix = virtual_env
|
||||||
echo sys.prefix = os.getenv^('VIRTUAL_ENV'^)
|
|
||||||
echo sys.exec_prefix = os.getenv^('VIRTUAL_ENV'^)
|
|
||||||
echo def getsitepackages^(^):
|
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
|
echo site.getsitepackages = getsitepackages
|
||||||
) > "%python_dir%\Lib\site-packages\sitecustomize.py"
|
) > "%python_dir%\Lib\site-packages\sitecustomize.py"
|
||||||
:skip_sitecustomize
|
:skip_sitecustomize
|
||||||
@ -156,7 +154,6 @@ echo [*] Создание %python_dir%\Scripts\activate.bat
|
|||||||
echo set "_OLD_VIRTUAL_PATH=%%PATH%%"
|
echo set "_OLD_VIRTUAL_PATH=%%PATH%%"
|
||||||
echo ^)
|
echo ^)
|
||||||
echo set "PATH=%%VIRTUAL_ENV%%;%%VIRTUAL_ENV%%\Scripts;%%PATH%%"
|
echo set "PATH=%%VIRTUAL_ENV%%;%%VIRTUAL_ENV%%\Scripts;%%PATH%%"
|
||||||
echo set "PYTHONPATH=%%VIRTUAL_ENV%%\Lib\site-packages"
|
|
||||||
echo chcp %%_OLD_CODEPAGE%% ^> nul
|
echo chcp %%_OLD_CODEPAGE%% ^> nul
|
||||||
) > %python_dir%\Scripts\activate.bat
|
) > %python_dir%\Scripts\activate.bat
|
||||||
:skip_venv
|
:skip_venv
|
||||||
@ -169,6 +166,7 @@ if exist run_me.bat (
|
|||||||
)
|
)
|
||||||
echo [+] Создание run_me.bat
|
echo [+] Создание run_me.bat
|
||||||
(
|
(
|
||||||
|
echo @echo off
|
||||||
echo cd /d "%%~dp0%%"
|
echo cd /d "%%~dp0%%"
|
||||||
echo call %python_dir%\Scripts\activate.bat
|
echo call %python_dir%\Scripts\activate.bat
|
||||||
echo call python main.py
|
echo call python main.py
|
||||||
|
|||||||
146
portable_install.sh
Normal file
146
portable_install.sh
Normal 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"
|
||||||
Loading…
x
Reference in New Issue
Block a user