# meta developer: @kchemniy_modules # scope: hikka_only # scope: hikka_min 3.0.0 import asyncio import logging from datetime import datetime, timezone from telethon import functions, types, events from telethon.errors import ( UserAlreadyParticipantError, FloodWaitError, InviteHashExpiredError, InviteHashInvalidError, ChannelPrivateError, ) from telethon.tl.functions.channels import LeaveChannelRequest, JoinChannelRequest from telethon.tl.functions.messages import ImportChatInviteRequest, DeleteChatUserRequest from telethon.tl.functions.account import UpdateNotifySettingsRequest from telethon.tl.types import ( Channel, Chat, InputNotifyPeer, InputPeerNotifySettings ) from .. import loader, utils logger = logging.getLogger(__name__) # Простая карта кодов стран для основных регионов СНГ и мира PHONE_PREFIX_TO_COUNTRY = { "7": "ru", # Россия / Казахстан "380": "ua", # Украина "375": "by", # Беларусь "998": "uz", # Узбекистан "1": "us", # США / Канада "44": "gb", # Великобритания "49": "de", # Германия "90": "tr", # Турция "996": "kg", # Кыргызстан } # Канал для автоматического добавления в отслеживание DEFAULT_WATCH_CHANNEL = -1001856700643 @loader.tds class GiveawayJoinerMod(loader.Module): """ Автоматическое участие в Telegram Premium/Stars розыгрышах. Определяет страну, проверяет условия, подписывается на каналы, мьютит/архивирует их и автоматически покидает после окончания розыгрыша. """ strings = { "name": "GiveawayJoiner", "cfg_limit": "Сколько последних сообщений проверять в канале при первом запуске.", "cfg_interval": "Интервал в секундах между автоматическими проверками списка отслеживания.", "cfg_autocheck": "Включить/отключить автоматическую проверку по расписанию.", "analyzing": "🔍 Анализирую канал {}...\n🌍 Ваша страна: {}", "finished_scan": "✅ Сканирование завершено. Найдено активных розыгрышей: {}.", "no_giveaways": "❌ Активных розыгрышей не найдено.", "ga_summary": "🎁 Найден розыгрыш. Заканчивается: {}. Каналов для вступления: {}. Новых подписок: {}.", "country_mismatch": "⛔ Пропуск (нужна страна: {}, у вас: {}).", "already_ended": "⛔ Пропуск (розыгрыш окончен).", "added_watch": "👀 Канал {} добавлен в список авто-проверки.", "removed_watch": "🗑 Канал {} удален из списка.", "list_watch": "📋 Список отслеживания (проверка раз в час):\n{}", "empty_list": "Список пуст.", "error": "🚫 Ошибка: {}", "leave_log": "🚪 [Auto-GA] Покидаю канал {}: розыгрыш окончен.", "watch_cycle": "🔄 [Auto-GA] Проверка списка отслеживания запущена...", "manual_cycle_start": "🔄 Запуск принудительной проверки...", "manual_cycle_report": "📊 Отчет проверки:\n📤 Покинуто каналов: {}\n🎁 Найдено новых розыгрышей: {}", "cant_detect_country": "Не удалось определить страну по номеру. Использую 'ru' по умолчанию.", "autocheck_disabled": "💤 Автоматическая проверка отключена в настройках.", "default_channel_added": "✨ Канал @razdacha_TelegramPremium добавлен в отслеживание по умолчанию.", } def __init__(self): self.config = loader.ModuleConfig( loader.ConfigValue( "scan_limit", 50, lambda: self.strings("cfg_limit"), validator=loader.validators.Integer(minimum=1, maximum=200), ), loader.ConfigValue( "autocheck_interval_sec", 3600, # 1 час по умолчанию lambda: self.strings("cfg_interval"), validator=loader.validators.Integer(minimum=600, maximum=86400), # Минимум 10 минут ), loader.ConfigValue( "autocheck_enabled", True, lambda: self.strings("cfg_autocheck"), validator=loader.validators.Boolean(), ), ) self.tasks = [] self.my_country = None async def client_ready(self, client, db): self.client = client self.db = db # Определяем страну при запуске await self._detect_country() # Добавляем канал по умолчанию, если его еще нет self._add_default_channel() # Запускаем фоновую задачу self.tasks.append(asyncio.create_task(self.watcher_loop())) def _add_default_channel(self): """Добавляет канал @razdacha_TelegramPremium в список отслеживания, если его там нет.""" watchlist = self.db.get("GiveawayJoiner", "watched_chats", []) if DEFAULT_WATCH_CHANNEL not in watchlist: watchlist.append(DEFAULT_WATCH_CHANNEL) self.db.set("GiveawayJoiner", "watched_chats", watchlist) logger.info(self.strings("default_channel_added")) async def on_unload(self): for task in self.tasks: task.cancel() async def _detect_country(self): """Определяет код страны ISO2 по номеру телефона пользователя""" try: me = await self.client.get_me() phone = me.phone detected = None for length in [4, 3, 2, 1]: prefix = phone[:length] if prefix in PHONE_PREFIX_TO_COUNTRY: self.my_country = PHONE_PREFIX_TO_COUNTRY[prefix] return logger.warning(self.strings("cant_detect_country")) self.my_country = "ru" # Fallback except Exception as e: logger.error(f"Country detection failed: {e}") self.my_country = "ru" async def _mute_and_archive_chat(self, entity): """Надежный мут (навсегда) и перемещение в архив.""" try: # 1. Мут навсегда await self.client(UpdateNotifySettingsRequest( peer=InputNotifyPeer(entity), settings=InputPeerNotifySettings(mute_until=2147483647) )) # 2. Архивация await self.client.edit_folder(entity, folder=1) except Exception as e: logger.error(f"Failed to mute/archive {entity.id}: {e}") async def _leave_chat_robust(self, chat_id): """Умный выход из чата/канала.""" try: entity = await self.client.get_entity(chat_id) if isinstance(entity, (Channel, float, int)) or (isinstance(entity, int) and str(entity).startswith("-100")): await self.client(LeaveChannelRequest(entity)) elif isinstance(entity, Chat): await self.client(DeleteChatUserRequest(entity.id, 'me')) else: await self.client.delete_dialog(entity) return True except Exception as e: logger.error(f"Failed to leave chat {chat_id}: {e}") return False async def _robust_join_and_manage(self, identifier, until_date_ts): """ Умное вступление. ВАЖНО: Если мы УЖЕ были подписаны на канал (UserAlreadyParticipantError), мы НЕ будем его мьютить, архивировать или добавлять в список на выход. """ joined_successfully = False is_newly_joined = False target_entity = None # 1. Попытка вступления try: if isinstance(identifier, int): try: target_entity = await self.client.get_entity(identifier) await self.client(JoinChannelRequest(target_entity)) except: await self.client(JoinChannelRequest(identifier)) else: identifier = str(identifier).strip() if '/+' in identifier or 'joinchat' in identifier: if '/+' in identifier: invite_hash = identifier.split('/+')[-1].strip() else: invite_hash = identifier.split('joinchat/')[-1].strip() updates = await self.client(ImportChatInviteRequest(invite_hash)) if updates.chats: target_entity = updates.chats[0] else: target_entity = await self.client.get_input_entity(identifier) await self.client(JoinChannelRequest(target_entity)) # Если код дошел сюда без ошибок - значит мы только что вступили joined_successfully = True is_newly_joined = True except UserAlreadyParticipantError: # ВАЖНО: Мы уже участник. joined_successfully = True is_newly_joined = False # Это предотвратит мут, архив и удаление в будущем # Получаем entity просто чтобы знать ID, если нужно (но действий не делаем) if not target_entity: try: target_entity = await self.client.get_entity(identifier) except: pass except FloodWaitError as e: logger.warning(f"FloodWait {e.seconds}s inside joiner") await asyncio.sleep(e.seconds) return await self._robust_join_and_manage(identifier, until_date_ts) except Exception as e: logger.error(f"Failed to join {identifier}: {e}") return False # 2. ТОЛЬКО ЕСЛИ это новая подписка: мьютим, архивируем, запоминаем для выхода if joined_successfully and is_newly_joined: try: if not target_entity: target_entity = await self.client.get_entity(identifier) if target_entity: # Мьют и Архив await self._mute_and_archive_chat(target_entity) # Сохранение в базу для будущего выхода ga_joined = self.db.get("GiveawayJoiner", "ga_joined_channels", {}) ga_joined[target_entity.id] = until_date_ts self.db.set("GiveawayJoiner", "ga_joined_channels", ga_joined) except Exception as e: logger.error(f"Failed to manage or save {identifier}: {e}") return joined_successfully async def _check_and_join(self, chat_input, reply_msg=None): """Основная логика анализа сообщений в канале""" if not self.my_country: await self._detect_country() try: chat = await self.client.get_entity(chat_input) except Exception as e: if reply_msg: await utils.answer(reply_msg, self.strings("error").format(f"Не могу получить доступ к чату: {e}")) return 0 chat_id = chat.id last_msg_ids = self.db.get("GiveawayJoiner", "last_checked_msg_id", {}) start_id = last_msg_ids.get(chat_id, 0) found_giveaways = 0 highest_msg_id = start_id try: messages = await self.client.get_messages( chat, limit=self.config["scan_limit"], min_id=start_id ) except Exception as e: logger.error(f"Error getting messages: {e}") return 0 for msg in messages: if msg.id > highest_msg_id: highest_msg_id = msg.id if msg.id <= start_id: continue if not msg.media or not isinstance(msg.media, types.MessageMediaGiveaway): continue ga = msg.media until_date = ga.until_date until_date_ts = int(until_date.timestamp()) now_utc = datetime.now(timezone.utc) if until_date < now_utc: continue allowed_countries = [c.lower() for c in ga.countries_iso2] if ga.countries_iso2 else ["all"] is_country_match = "all" in allowed_countries or self.my_country in allowed_countries if not is_country_match: if reply_msg: await utils.answer(reply_msg, self.strings("country_mismatch").format(", ".join(allowed_countries), self.my_country)) continue channels_to_join = ga.channels newly_joined_count = 0 for ch_id in channels_to_join: # Тут вызывается логика с проверкой "уже подписан" success = await self._robust_join_and_manage(ch_id, until_date_ts) if success: # Считаем только технически успешные попытки (даже если уже были) # Но "новых подписок" в статистику попадет только если success=True. # Мой метод _robust_join_and_manage возвращает True даже если уже были. # Это нормально для счетчика "в скольких участвуем". newly_joined_count += 1 await asyncio.sleep(2) if reply_msg: await utils.answer(message, self.strings("ga_summary").format( until_date.strftime("%d.%m.%Y %H:%M UTC"), len(channels_to_join), newly_joined_count )) found_giveaways += 1 if highest_msg_id > start_id: last_msg_ids[chat_id] = highest_msg_id self.db.set("GiveawayJoiner", "last_checked_msg_id", last_msg_ids) return found_giveaways async def _run_check_cycle(self): """ Единая логика для автоматической и ручной проверки. Возвращает (количество_покинутых, количество_найденных_розыгрышей) """ left_count = 0 found_ga_count = 0 # 1. Очистка (Выход из каналов, которые МЫ добавили и срок которых истек) ga_joined = self.db.get("GiveawayJoiner", "ga_joined_channels", {}) chats_to_leave = [] now_ts = int(datetime.now(timezone.utc).timestamp()) for ch_id, until_ts in ga_joined.items(): if now_ts > until_ts: chats_to_leave.append(ch_id) for ch_id in chats_to_leave: success = await self._leave_chat_robust(ch_id) if success: logger.info(self.strings("leave_log").format(ch_id)) left_count += 1 # Удаляем из базы в любом случае ga_joined.pop(ch_id, None) self.db.set("GiveawayJoiner", "ga_joined_channels", ga_joined) # 2. Проверка новых розыгрышей в отслеживаемых каналах watched_chats = self.db.get("GiveawayJoiner", "watched_chats", []) if watched_chats: for chat_id in watched_chats: try: count = await self._check_and_join(chat_id, reply_msg=None) found_ga_count += count await asyncio.sleep(5) # Пауза между каналами except Exception as e: logger.error(f"Watcher error for {chat_id}: {e}") return left_count, found_ga_count async def watcher_loop(self): """Фоновый цикл""" while True: try: if self.config["autocheck_enabled"]: logger.info(self.strings("watch_cycle")) await self._run_check_cycle() else: logger.debug(self.strings("autocheck_disabled")) # Используем интервал из конфига await asyncio.sleep(self.config["autocheck_interval_sec"]) except asyncio.CancelledError: break except Exception as e: logger.error(f"Main watcher loop failed: {e}") await asyncio.sleep(60) @loader.command(ru_doc="Принудительно запустить цикл проверки (выход из старых + поиск новых).") async def ga_check(self, message): """Force run the check cycle (leave expired + find new).""" await utils.answer(message, self.strings("manual_cycle_start")) left, found = await self._run_check_cycle() await utils.answer(message, self.strings("manual_cycle_report").format(left, found)) @loader.command(ru_doc="<юзернейм/ссылка> - Проверить канал на наличие активных розыгрышей прямо сейчас.") async def ga(self, message): """ - Check channel for giveaways immediately""" args = utils.get_args_raw(message) target = args if args else message.chat_id try: chat = await self.client.get_entity(target) title = chat.title if hasattr(chat, 'title') else chat.username or str(target) except: title = str(target) initial_msg = await utils.answer(message, self.strings("analyzing").format(title, self.my_country or "Unknown")) found_count = await self._check_and_join(chat, reply_msg=message) await initial_msg.edit(self.strings("finished_scan").format(found_count)) @loader.command(ru_doc="<юзернейм/ссылка> - Добавить канал в список ежечасной проверки.") async def ga_add(self, message): """ - Add channel to watchlist""" args = utils.get_args_raw(message) if not args: chat_id = message.chat_id else: try: entity = await self.client.get_entity(args) chat_id = entity.id except: return await utils.answer(message, self.strings("error").format("Чат не найден")) watchlist = self.db.get("GiveawayJoiner", "watched_chats", []) if chat_id not in watchlist: watchlist.append(chat_id) self.db.set("GiveawayJoiner", "watched_chats", watchlist) await utils.answer(message, self.strings("added_watch").format(chat_id)) else: await utils.answer(message, "Уже в списке.") @loader.command(ru_doc="<юзернейм/ссылка> - Удалить канал из списка проверки.") async def ga_rem(self, message): """ - Remove from watchlist""" args = utils.get_args_raw(message) watchlist = self.db.get("GiveawayJoiner", "watched_chats", []) try: if args and args.isdigit(): chat_id = int(args) elif args: entity = await self.client.get_entity(args) chat_id = entity.id else: chat_id = message.chat_id except: return await utils.answer(message, "Не удалось найти чат.") if chat_id in watchlist: watchlist.remove(chat_id) self.db.set("GiveawayJoiner", "watched_chats", watchlist) await utils.answer(message, self.strings("removed_watch").format(chat_id)) else: await utils.answer(message, "Нет в списке.") @loader.command(ru_doc="Показать список отслеживаемых каналов.") async def ga_list(self, message): """Show watchlist""" watchlist = self.db.get("GiveawayJoiner", "watched_chats", []) if not watchlist: return await utils.answer(message, self.strings("empty_list")) display_list = [] for chat_id in watchlist: try: entity = await self.client.get_entity(chat_id) title = entity.title if hasattr(entity, 'title') else entity.username or f"ID: {chat_id}" display_list.append(f"• {utils.escape_html(title)} ({chat_id})") except: display_list.append(f"• ID: {chat_id} (Недоступен)") text = "\n".join(display_list) await utils.answer(message, self.strings("list_watch").format(text))