#!/usr/bin/env python3 """ Telegram Data Collector для OpenClaw ----------------------------------- Скрипт для сбора сообщений из Telegram-групп и каналов и сохранения их в формате, удобном для анализа с помощью OpenClaw. """ import os import sys import json import asyncio from datetime import datetime, timedelta from pathlib import Path from telethon import TelegramClient, events from telethon.tl.functions.messages import GetHistoryRequest import logging from dotenv import load_dotenv # Настройка логирования logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler(os.path.join(SKILL_DIR, "logs", "telegram_collector.log")), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) # Путь к директории скилла SKILL_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Загрузка переменных окружения из .env файла load_dotenv(os.path.expanduser("~/.openclaw/.env")) # Конфигурация API_ID = os.getenv('TELEGRAM_COLLECTOR_API_ID') API_HASH = os.getenv('TELEGRAM_COLLECTOR_API_HASH') PHONE_NUMBER = os.getenv('TELEGRAM_COLLECTOR_PHONE') SESSION_FILE = os.getenv('TELEGRAM_COLLECTOR_SESSION', 'telegram_collector') OUTPUT_DIR = os.getenv('OUTPUT_DIR', os.path.join(SKILL_DIR, '..', 'data', 'telegram-collector')) # Создание директорий для выходных данных Path(OUTPUT_DIR).mkdir(exist_ok=True) Path(f"{OUTPUT_DIR}/raw").mkdir(exist_ok=True) Path(f"{OUTPUT_DIR}/topics").mkdir(exist_ok=True) Path(f"{OUTPUT_DIR}/summaries").mkdir(exist_ok=True) class TelegramCollector: def __init__(self): self.client = None self.groups = {} self.config_file = os.path.join(SKILL_DIR, 'groups_config.json') self.load_config() def load_config(self): """Загружает конфигурацию групп из JSON-файла.""" try: if os.path.exists(self.config_file): with open(self.config_file, 'r', encoding='utf-8') as f: self.groups = json.load(f) logger.info(f"Конфигурация загружена: {len(self.groups)} групп/каналов") else: logger.warning(f"Файл конфигурации {self.config_file} не найден. Создаем новый.") self.groups = {} self.save_config() except Exception as e: logger.error(f"Ошибка загрузки конфигурации: {e}") self.groups = {} def save_config(self): """Сохраняет текущую конфигурацию групп в JSON-файл.""" with open(self.config_file, 'w', encoding='utf-8') as f: json.dump(self.groups, f, indent=2, ensure_ascii=False) logger.info("Конфигурация сохранена") def update_group_config(self, group_id, name, username=None, topics=None, last_update=None): """Обновляет конфигурацию для указанной группы.""" if str(group_id) not in self.groups: self.groups[str(group_id)] = { "name": name, "username": username, "topics": topics or [], "last_update": last_update or "1970-01-01 00:00:00" } else: if name: self.groups[str(group_id)]["name"] = name if username: self.groups[str(group_id)]["username"] = username if topics: self.groups[str(group_id)]["topics"] = topics if last_update: self.groups[str(group_id)]["last_update"] = last_update self.save_config() async def connect(self): """Подключается к Telegram API.""" if not all([API_ID, API_HASH, PHONE_NUMBER]): logger.error("API_ID, API_HASH или PHONE_NUMBER не установлены. Проверьте .env файл.") sys.exit(1) try: self.client = TelegramClient(SESSION_FILE, API_ID, API_HASH) await self.client.start(phone=PHONE_NUMBER) logger.info("Успешное подключение к Telegram API") return True except Exception as e: logger.error(f"Ошибка подключения к Telegram API: {e}") return False async def get_dialogs(self): """Получает список доступных диалогов.""" dialogs = await self.client.get_dialogs() groups_channels = [d for d in dialogs if d.is_group or d.is_channel] logger.info(f"Доступно {len(groups_channels)} групп и каналов:") for i, dialog in enumerate(groups_channels, 1): dialog_type = "канал" if dialog.is_channel else "группа" entity = dialog.entity username = getattr(entity, 'username', None) logger.info(f"{i}. {dialog.name} (ID: {dialog.id}, @{username}, тип: {dialog_type})") return groups_channels async def get_messages(self, chat_entity, limit=100, since_date=None): """Получает сообщения из указанного чата.""" try: # Если указана дата, получаем сообщения только с этой даты if since_date: since_date = datetime.strptime(since_date, "%Y-%m-%d %H:%M:%S") messages = [] offset_id = 0 while True: history = await self.client(GetHistoryRequest( peer=chat_entity, offset_id=offset_id, offset_date=None, add_offset=0, limit=100, max_id=0, min_id=0, hash=0 )) if not history.messages: break for message in history.messages: if message.date < since_date: # Достигли сообщений старше указанной даты, останавливаемся return messages if message.message: # Пропускаем пустые сообщения messages.append(message) offset_id = history.messages[-1].id if len(messages) >= limit: break return messages else: # Если дата не указана, просто берем последние сообщения return await self.client.get_messages(chat_entity, limit=limit) except Exception as e: logger.error(f"Ошибка при получении сообщений: {e}") return [] async def collect_from_group(self, group_id, limit=100): """Собирает сообщения из указанной группы.""" try: group_id = int(group_id) group_config = self.groups.get(str(group_id), {}) last_update = group_config.get("last_update", "1970-01-01 00:00:00") entity = await self.client.get_entity(group_id) logger.info(f"Сбор данных из {entity.title} (ID: {group_id})") messages = await self.get_messages(entity, limit=limit, since_date=last_update) logger.info(f"Получено {len(messages)} новых сообщений") # Обработка и сохранение сообщений if messages: processed_messages = [] for msg in messages: processed_msg = { "id": msg.id, "date": msg.date.strftime("%Y-%m-%d %H:%M:%S"), "from_id": getattr(msg.from_id, "user_id", None) if msg.from_id else None, "text": msg.message, "has_media": bool(msg.media), "reply_to_msg_id": msg.reply_to_msg_id } processed_messages.append(processed_msg) # Сохраняем сырые данные today = datetime.now().strftime("%Y-%m-%d") raw_file = f"{OUTPUT_DIR}/raw/{entity.id}_{today}.json" with open(raw_file, 'w', encoding='utf-8') as f: json.dump({ "group_id": group_id, "group_name": entity.title, "collected_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "messages_count": len(processed_messages), "messages": processed_messages }, f, indent=2, ensure_ascii=False) # Также сохраняем в формате Markdown для удобного просмотра md_file = f"{OUTPUT_DIR}/raw/{entity.id}_{today}.md" with open(md_file, 'w', encoding='utf-8') as f: f.write(f"# Сырые данные из {entity.title}\n\n") f.write(f"## Собрано: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") f.write(f"## Всего сообщений: {len(processed_messages)}\n\n") for i, msg in enumerate(processed_messages, 1): f.write(f"### Сообщение {i}\n") f.write(f"**ID**: {msg['id']}\n") f.write(f"**Дата**: {msg['date']}\n") f.write(f"**Текст**:\n{msg['text']}\n") f.write(f"**Содержит медиа**: {'Да' if msg['has_media'] else 'Нет'}\n\n") # Обновляем конфигурацию группы с новой датой последнего обновления if processed_messages: latest_date = processed_messages[0]["date"] self.update_group_config( group_id, name=entity.title, username=getattr(entity, 'username', None), last_update=latest_date ) return len(processed_messages) else: logger.info(f"В группе {entity.title} нет новых сообщений с {last_update}") return 0 except Exception as e: logger.error(f"Ошибка при сборе данных из группы {group_id}: {e}") return 0 async def list_groups(self): """Выводит список групп в конфигурации.""" if not self.groups: logger.info("В конфигурации нет сохраненных групп") return logger.info("Сохраненные группы и каналы:") for group_id, info in self.groups.items(): logger.info(f"ID: {group_id}, Название: {info['name']}, Последнее обновление: {info['last_update']}") async def add_group(self, group_id, topics=None): """Добавляет группу в список отслеживаемых.""" try: group_id = int(group_id) entity = await self.client.get_entity(group_id) self.update_group_config( group_id, name=entity.title, username=getattr(entity, 'username', None), topics=topics ) logger.info(f"Группа {entity.title} (ID: {group_id}) добавлена в конфигурацию") return True except Exception as e: logger.error(f"Ошибка при добавлении группы {group_id}: {e}") return False async def remove_group(self, group_id): """Удаляет группу из списка отслеживаемых.""" if str(group_id) in self.groups: group_name = self.groups[str(group_id)]["name"] del self.groups[str(group_id)] self.save_config() logger.info(f"Группа {group_name} (ID: {group_id}) удалена из конфигурации") return True else: logger.warning(f"Группа с ID {group_id} не найдена в конфигурации") return False async def collect_all(self, limit=100): """Собирает данные из всех групп в конфигурации.""" total_messages = 0 for group_id in self.groups: count = await self.collect_from_group(group_id, limit=limit) total_messages += count logger.info(f"Всего собрано {total_messages} новых сообщений из {len(self.groups)} групп/каналов") return total_messages async def main(): """Основная функция для запуска коллектора.""" collector = TelegramCollector() if len(sys.argv) < 2: print("Использование: python collector.py <команда> [аргументы]") print("Команды:") print(" list - вывести список доступных диалогов") print(" list-config - вывести список сохраненных групп") print(" add [topic1,topic2,...] - добавить группу") print(" remove - удалить группу") print(" collect [limit] - собрать данные из группы") print(" collect-all [limit] - собрать данные из всех групп") sys.exit(1) command = sys.argv[1].lower() if not await collector.connect(): sys.exit(1) try: if command == "list": await collector.get_dialogs() elif command == "list-config": await collector.list_groups() elif command == "add" and len(sys.argv) >= 3: group_id = sys.argv[2] topics = sys.argv[3].split(",") if len(sys.argv) > 3 else None await collector.add_group(group_id, topics) elif command == "remove" and len(sys.argv) >= 3: group_id = sys.argv[2] await collector.remove_group(group_id) elif command == "collect" and len(sys.argv) >= 3: group_id = sys.argv[2] limit = int(sys.argv[3]) if len(sys.argv) > 3 else 100 await collector.collect_from_group(group_id, limit) elif command == "collect-all": limit = int(sys.argv[2]) if len(sys.argv) > 2 else 100 await collector.collect_all(limit) else: print("Неизвестная команда или недостаточно аргументов") sys.exit(1) finally: await collector.client.disconnect() if __name__ == "__main__": asyncio.run(main())