# 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))