638 lines
30 KiB
Python
638 lines
30 KiB
Python
"""
|
||
Deskgram 2 — реконструкция функций по параметрам из веб-превью.
|
||
Telethon-based. Все параметры взяты из интерфейса модулей.
|
||
"""
|
||
import asyncio, random, json, os, re, time, csv
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
from telethon import TelegramClient, functions, types, errors
|
||
from telethon.tl.types import InputPeerUser, InputPeerChat, InputPeerChannel
|
||
from telethon.tl.functions.messages import GetDialogsRequest
|
||
from telethon.tl.functions.channels import InviteToChannelRequest, JoinChannelRequest
|
||
from telethon.tl.functions.account import UpdateProfileRequest, UpdateUsernameRequest
|
||
from telethon.tl.functions.photos import UploadProfilePhotoRequest
|
||
from telethon.sessions import StringSession
|
||
from dataclasses import dataclass, field
|
||
from typing import Optional
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Панель аккаунтов
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def account_check(client: TelegramClient):
|
||
"""Проверка аккаунта: статус, спамблок, премиум"""
|
||
me = await client.get_me()
|
||
try:
|
||
await client.send_message('SpamBot', '/start')
|
||
await asyncio.sleep(2)
|
||
msgs = await client.get_messages('SpamBot', limit=1)
|
||
spam_blocked = 'ограничен' in (msgs[0].message or '').lower()
|
||
except:
|
||
spam_blocked = None
|
||
return {
|
||
'id': me.id, 'username': me.username, 'phone': me.phone,
|
||
'premium': getattr(me, 'premium', False),
|
||
'spam_blocked': spam_blocked,
|
||
'last_seen': me.status.was_online if hasattr(me.status, 'was_online') else None
|
||
}
|
||
|
||
async def account_edit_profile(client: TelegramClient, *,
|
||
first_name=None, last_name=None, bio=None, username=None, photo_path=None):
|
||
"""Редактирование профиля аккаунта"""
|
||
if first_name or last_name:
|
||
await client(UpdateProfileRequest(
|
||
first_name=first_name, last_name=last_name, about=bio))
|
||
if username:
|
||
await client(UpdateUsernameRequest(username))
|
||
if photo_path:
|
||
await client(UploadProfilePhotoRequest(
|
||
await client.upload_file(photo_path)))
|
||
|
||
async def account_set_2fa(client: TelegramClient, password: str, hint: str = ''):
|
||
"""Установка 2FA"""
|
||
await client.edit_2fa(new_password=password, hint=hint)
|
||
|
||
async def account_get_sessions(client: TelegramClient):
|
||
"""Получить активные сессии"""
|
||
return await client(functions.account.GetAuthorizationsRequest())
|
||
|
||
async def account_terminate_sessions(client: TelegramClient):
|
||
"""Сбросить все сессии кроме текущей"""
|
||
await client(functions.auth.ResetAuthorizationsRequest())
|
||
|
||
async def account_privacy(client: TelegramClient, *,
|
||
hide_phone=True, hide_lastseen=True, hide_forwarded=True):
|
||
"""Настройки приватности"""
|
||
rules = []
|
||
if hide_phone:
|
||
rules.append(types.InputPrivacyValueDisallowAll())
|
||
if hide_lastseen:
|
||
pass # аналогично через account.setPrivacy
|
||
await client(functions.account.SetPrivacyRequest(
|
||
key=types.InputPrivacyKeyPhoneNumber(), rules=rules))
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Рассылка в ЛС
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
@dataclass
|
||
class SpamPMConfig:
|
||
threads: int = 3
|
||
msg_per_account: int = 50
|
||
delay_min: float = 30.0
|
||
delay_max: float = 90.0
|
||
silent: bool = False
|
||
ttl_seconds: Optional[int] = None
|
||
pin_message: bool = False
|
||
delete_sender: bool = False
|
||
block_after: bool = False
|
||
add_to_blacklist: bool = False
|
||
remove_links_preview: bool = False
|
||
rotate_on_flood: bool = True
|
||
check_spambot: bool = True
|
||
use_autoresponder: bool = False
|
||
collect_incoming: bool = False
|
||
thread_start_delay: tuple = (5, 30)
|
||
|
||
async def spam_pm(client: TelegramClient, users: list, message: str, cfg: SpamPMConfig):
|
||
"""Рассылка в ЛС с настройками из интерфейса"""
|
||
sent = 0
|
||
|
||
for user in users:
|
||
if sent >= cfg.msg_per_account:
|
||
break
|
||
try:
|
||
entity = await client.get_input_entity(user)
|
||
await client.send_message(
|
||
entity, message,
|
||
silent=cfg.silent,
|
||
background=None
|
||
)
|
||
sent += 1
|
||
|
||
if cfg.block_after:
|
||
await client(functions.contacts.BlockRequest(entity))
|
||
|
||
delay = random.uniform(cfg.delay_min, cfg.delay_max)
|
||
await asyncio.sleep(delay)
|
||
|
||
except errors.FloodWaitError as e:
|
||
if cfg.rotate_on_flood:
|
||
return False # сигнал сменить аккаунт
|
||
await asyncio.sleep(e.seconds)
|
||
except errors.PeerFloodError:
|
||
if cfg.check_spambot:
|
||
await client.send_message('SpamBot', '/start')
|
||
return False
|
||
except Exception:
|
||
continue
|
||
return True
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Нейрочаттинг
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
@dataclass
|
||
class NeuroChatConfig:
|
||
mode: str = 'keywords' # keywords | trigger | smart | promoter
|
||
chat_source: str = 'subscriptions' # subscriptions | list
|
||
chat_list: list = field(default_factory=list)
|
||
replies_per_account: int = 30
|
||
context_messages: int = 5
|
||
delay_before_reply: tuple = (5, 30)
|
||
pause_between_replies: tuple = (60, 300)
|
||
max_replies_per_chat: int = 10
|
||
chat_cooldown_min: int = 30
|
||
trigger_words: list = field(default_factory=list)
|
||
whole_word: bool = True
|
||
case_sensitive: bool = False
|
||
ignore_bots: bool = True
|
||
ignore_system: bool = True
|
||
min_message_length: int = 1
|
||
leave_if_cant_write: bool = False
|
||
auto_leave_spam: bool = False
|
||
respond_to_our_replies: bool = False
|
||
check_spambot_on_flood: bool = True
|
||
|
||
async def neurochatting(client: TelegramClient, ai_generate, cfg: NeuroChatConfig):
|
||
"""Нейрочаттинг: AI отвечает на сообщения в чатах"""
|
||
|
||
if cfg.chat_source == 'subscriptions':
|
||
dialogs = await client.get_dialogs()
|
||
chats = [d for d in dialogs if d.is_group]
|
||
else:
|
||
chats = cfg.chat_list
|
||
|
||
chat_stats = {}
|
||
for chat in chats:
|
||
if chat_stats.get(chat.id, 0) >= cfg.max_replies_per_chat:
|
||
continue
|
||
|
||
try:
|
||
messages = await client.get_messages(chat, limit=50)
|
||
except errors.ChatWriteForbiddenError:
|
||
if cfg.leave_if_cant_write:
|
||
await client.delete_dialog(chat)
|
||
continue
|
||
|
||
for msg in messages:
|
||
if not msg.message:
|
||
continue
|
||
if cfg.ignore_bots and msg.sender and getattr(msg.sender, 'bot', False):
|
||
continue
|
||
if cfg.ignore_system and getattr(msg, 'action', None):
|
||
continue
|
||
if len(msg.message or '') < cfg.min_message_length:
|
||
continue
|
||
|
||
# Проверка триггеров
|
||
should_reply = False
|
||
text = msg.message or ''
|
||
|
||
if cfg.mode == 'keywords':
|
||
for word in cfg.trigger_words:
|
||
if cfg.whole_word:
|
||
pattern = rf'\b{re.escape(word)}\b'
|
||
else:
|
||
pattern = re.escape(word)
|
||
flags = 0 if cfg.case_sensitive else re.IGNORECASE
|
||
if re.search(pattern, text, flags):
|
||
should_reply = True
|
||
break
|
||
|
||
elif cfg.mode == 'trigger':
|
||
should_reply = True # AI решает
|
||
|
||
elif cfg.mode == 'smart':
|
||
should_reply = True
|
||
|
||
elif cfg.mode == 'promoter':
|
||
should_reply = True
|
||
|
||
if should_reply:
|
||
# Контекст
|
||
context = await client.get_messages(
|
||
chat, limit=cfg.context_messages, offset_id=msg.id)
|
||
context_text = '\n'.join(
|
||
f"[{m.sender_id}]: {m.message}" for m in reversed(context) if m.message)
|
||
|
||
# AI генерация
|
||
reply = await ai_generate(context_text, msg.message)
|
||
|
||
delay = random.uniform(*cfg.delay_before_reply)
|
||
await asyncio.sleep(delay)
|
||
|
||
try:
|
||
await client.send_message(chat, reply, reply_to=msg.id)
|
||
chat_stats[chat.id] = chat_stats.get(chat.id, 0) + 1
|
||
except errors.PeerFloodError:
|
||
if cfg.check_spambot_on_flood:
|
||
await client.send_message('SpamBot', '/start')
|
||
return chat_stats
|
||
|
||
pause = random.uniform(*cfg.pause_between_replies)
|
||
await asyncio.sleep(pause)
|
||
|
||
return chat_stats
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Инвайтинг
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def invite_users(client: TelegramClient, chat, users: list,
|
||
per_account: int = 50, delay: tuple = (60, 180), rotate_on_flood: bool = True):
|
||
"""Массовый инвайт пользователей в канал/группу"""
|
||
invited = 0
|
||
for user in users:
|
||
if invited >= per_account:
|
||
break
|
||
try:
|
||
entity = await client.get_input_entity(user)
|
||
await client(InviteToChannelRequest(chat, [entity]))
|
||
invited += 1
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
except errors.FloodWaitError as e:
|
||
if rotate_on_flood:
|
||
return invited
|
||
await asyncio.sleep(e.seconds)
|
||
except errors.UserPrivacyRestrictedError:
|
||
continue
|
||
except Exception:
|
||
continue
|
||
return invited
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Парсер аудитории
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def parse_chat_members(client: TelegramClient, chat,
|
||
limit: int = 50000, active_only: bool = False) -> list[dict]:
|
||
"""Сбор участников чата/канала"""
|
||
users = []
|
||
try:
|
||
async for member in client.iter_participants(chat, limit=limit):
|
||
if active_only and member.status:
|
||
if hasattr(member.status, 'was_online'):
|
||
if (datetime.now() - member.status.was_online).days > 30:
|
||
continue
|
||
users.append({
|
||
'id': member.id,
|
||
'username': member.username,
|
||
'first_name': member.first_name,
|
||
'last_name': member.last_name,
|
||
'phone': getattr(member, 'phone', None),
|
||
'premium': getattr(member, 'premium', False),
|
||
'bot': member.bot,
|
||
'was_online': str(getattr(member.status, 'was_online', None))
|
||
})
|
||
except errors.ChatAdminRequiredError:
|
||
pass
|
||
return users
|
||
|
||
async def parse_comments(client: TelegramClient, channel, post_limit: int = 100):
|
||
"""Парсер комментаторов постов"""
|
||
commenters = {}
|
||
messages = await client.get_messages(channel, limit=post_limit)
|
||
for msg in messages:
|
||
if msg.replies and msg.replies.replies > 0:
|
||
try:
|
||
async for comment in client.iter_messages(
|
||
channel, reply_to=msg.id, limit=100):
|
||
if comment.sender_id and comment.sender_id not in commenters:
|
||
commenters[comment.sender_id] = {
|
||
'user_id': comment.sender_id,
|
||
'post_id': msg.id,
|
||
'count': 1
|
||
}
|
||
elif comment.sender_id:
|
||
commenters[comment.sender_id]['count'] += 1
|
||
except:
|
||
continue
|
||
return sorted(commenters.values(), key=lambda x: x['count'], reverse=True)
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Нейрокомментинг
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def neuro_commenting(client: TelegramClient, channels: list,
|
||
ai_generate, posts_per_channel: int = 10,
|
||
delay: tuple = (30, 120), mode: str = 'firstnah'):
|
||
"""Автокомментирование новых постов с AI"""
|
||
results = []
|
||
for channel in channels:
|
||
try:
|
||
# Первонах (firstnah) — комментарий первым
|
||
messages = await client.get_messages(channel, limit=posts_per_channel)
|
||
for msg in messages:
|
||
if not msg.message:
|
||
continue
|
||
try:
|
||
existing = await client.get_messages(
|
||
channel, reply_to=msg.id, limit=5)
|
||
our_comments = [c for c in existing
|
||
if c.sender_id == (await client.get_me()).id]
|
||
if mode == 'firstnah' and our_comments:
|
||
continue # уже комментили
|
||
except:
|
||
pass
|
||
|
||
comment = await ai_generate(
|
||
f"POST: {msg.message[:500]}",
|
||
style='positive' # из настроек модуля
|
||
)
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
await client.send_message(channel, comment, reply_to=msg.id)
|
||
results.append({'channel': channel, 'post_id': msg.id, 'comment': comment[:100]})
|
||
except Exception as e:
|
||
results.append({'channel': channel, 'error': str(e)})
|
||
return results
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Рассылка в группы
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def spam_groups(client: TelegramClient, chats: list, message: str,
|
||
ai_rewrite=False, ai_generate=None,
|
||
per_account: int = 20, delay: tuple = (60, 180)):
|
||
"""Рассылка в группы с AI-рерайтом"""
|
||
sent = 0
|
||
for chat in chats:
|
||
if sent >= per_account:
|
||
break
|
||
try:
|
||
text = message
|
||
if ai_rewrite and ai_generate:
|
||
text = await ai_generate(f"Rewrite this naturally: {message}")
|
||
|
||
await client.send_message(chat, text)
|
||
sent += 1
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
except errors.FloodWaitError as e:
|
||
await asyncio.sleep(e.seconds)
|
||
except Exception:
|
||
continue
|
||
return sent
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Автоответчик с ветвлением
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
@dataclass
|
||
class AutoReplyRule:
|
||
trigger_words: list
|
||
response: str
|
||
whole_word: bool = True
|
||
case_sensitive: bool = False
|
||
collect_usernames: bool = False
|
||
|
||
async def auto_responder(client: TelegramClient, rules: list[AutoReplyRule],
|
||
collect_usernames: bool = False, blacklist: set = set()):
|
||
"""Автоответчик на входящие сообщения"""
|
||
collected = []
|
||
while True:
|
||
try:
|
||
dialogs = await client.get_dialogs()
|
||
for dialog in dialogs:
|
||
if dialog.unread_count > 0 and dialog.id not in blacklist:
|
||
msgs = await client.get_messages(dialog.id, limit=dialog.unread_count)
|
||
for msg in msgs:
|
||
if msg.out:
|
||
continue
|
||
if collect_usernames and msg.sender:
|
||
collected.append({
|
||
'user_id': msg.sender_id,
|
||
'username': getattr(msg.sender, 'username', None),
|
||
'message': msg.message[:200]
|
||
})
|
||
|
||
text = msg.message or ''
|
||
for rule in rules:
|
||
for word in rule.trigger_words:
|
||
if rule.whole_word:
|
||
pattern = rf'\b{re.escape(word)}\b'
|
||
else:
|
||
pattern = re.escape(word)
|
||
flags = 0 if rule.case_sensitive else re.IGNORECASE
|
||
if re.search(pattern, text, flags):
|
||
await client.send_message(dialog.id, rule.response)
|
||
break
|
||
else:
|
||
continue
|
||
break
|
||
except Exception:
|
||
pass
|
||
await asyncio.sleep(5)
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Массовые подписки + лайки + просмотры
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def mass_join(client: TelegramClient, channels: list,
|
||
per_account: int = 50, delay: tuple = (30, 90)):
|
||
"""Массовое вступление в каналы/группы"""
|
||
joined = 0
|
||
for ch in channels:
|
||
if joined >= per_account:
|
||
break
|
||
try:
|
||
await client(JoinChannelRequest(ch))
|
||
joined += 1
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
except errors.FloodWaitError as e:
|
||
await asyncio.sleep(e.seconds)
|
||
except Exception:
|
||
continue
|
||
return joined
|
||
|
||
async def mass_react(client: TelegramClient, channel, post_ids: list,
|
||
reaction='👍', per_account: int = 100, delay: tuple = (5, 15)):
|
||
"""Массовые реакции на посты"""
|
||
for pid in post_ids[:per_account]:
|
||
try:
|
||
await client.send_reaction(channel, pid, reaction)
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
except errors.FloodWaitError as e:
|
||
await asyncio.sleep(e.seconds)
|
||
except Exception:
|
||
continue
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Чекер юзернеймов
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def check_usernames(client: TelegramClient, usernames: list) -> dict:
|
||
"""Проверка доступности юзернеймов"""
|
||
results = {'free': [], 'taken': [], 'errors': []}
|
||
for username in usernames:
|
||
try:
|
||
entity = await client.get_entity(username)
|
||
results['taken'].append(username)
|
||
except errors.UsernameNotOccupiedError:
|
||
results['free'].append(username)
|
||
except Exception:
|
||
results['errors'].append(username)
|
||
await asyncio.sleep(0.5)
|
||
return results
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Масслукинг (просмотр Stories)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def mass_story_view(client: TelegramClient, targets: list,
|
||
per_account: int = 200, delay: tuple = (3, 10)):
|
||
"""Массовый просмотр Telegram Stories"""
|
||
viewed = 0
|
||
for target in targets:
|
||
if viewed >= per_account:
|
||
break
|
||
try:
|
||
entity = await client.get_input_entity(target)
|
||
stories = await client(functions.stories.GetPeerStoriesRequest(
|
||
peer=entity))
|
||
if stories.stories:
|
||
await client(functions.stories.ReadStoriesRequest(
|
||
peer=entity,
|
||
max_id=max(s.id for s in stories.stories.stories)
|
||
))
|
||
viewed += 1
|
||
await asyncio.sleep(random.uniform(*delay))
|
||
except Exception:
|
||
continue
|
||
return viewed
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Прогрев аккаунтов
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def warmup_account(client: TelegramClient, days: int = 7,
|
||
daily_joins: int = 5, daily_messages: int = 3, daily_reactions: int = 10):
|
||
"""Прогрев аккаунта перед работой"""
|
||
for day in range(days):
|
||
for _ in range(random.randint(1, daily_joins)):
|
||
try:
|
||
dialogs = await client.get_dialogs()
|
||
groups = [d for d in dialogs if d.is_group]
|
||
if groups:
|
||
await client(JoinChannelRequest(random.choice(groups)))
|
||
except:
|
||
pass
|
||
await asyncio.sleep(random.uniform(300, 900))
|
||
|
||
for _ in range(random.randint(1, daily_messages)):
|
||
try:
|
||
await client.send_message('me', f'Warmup message {time.time()}')
|
||
except:
|
||
pass
|
||
await asyncio.sleep(random.uniform(600, 1800))
|
||
|
||
return True
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Постинг по каналам
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def channel_posting(client: TelegramClient, channels: list,
|
||
posts: list[dict], schedule: bool = False):
|
||
"""Публикация постов в каналы через бота-админа"""
|
||
for channel in channels:
|
||
for post in posts:
|
||
try:
|
||
entity = await client.get_entity(channel)
|
||
if post.get('photo'):
|
||
await client.send_file(entity, post['photo'],
|
||
caption=post.get('caption', ''))
|
||
else:
|
||
await client.send_message(entity, post.get('text', ''))
|
||
except Exception:
|
||
continue
|
||
await asyncio.sleep(random.uniform(10, 30))
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Снятие спамблока
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def remove_spamblock(client: TelegramClient) -> bool:
|
||
"""Снятие ограничений через SpamBot"""
|
||
try:
|
||
await client.send_message('SpamBot', '/start')
|
||
await asyncio.sleep(2)
|
||
msgs = await client.get_messages('SpamBot', limit=5)
|
||
for msg in msgs:
|
||
if msg.reply_markup and hasattr(msg.reply_markup, 'rows'):
|
||
for row in msg.reply_markup.rows:
|
||
for btn in row.buttons:
|
||
if 'ограничен' in (btn.text or '').lower() or \
|
||
'это я' in (btn.text or '').lower() or \
|
||
'продолжить' in (btn.text or '').lower():
|
||
await btn.click()
|
||
return True
|
||
except Exception:
|
||
pass
|
||
return False
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Диспетчер задач (оркестратор)
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
class TaskDispatcher:
|
||
"""Центральный диспетчер задач"""
|
||
def __init__(self):
|
||
self.tasks = {}
|
||
self.running = set()
|
||
self.results = {}
|
||
|
||
async def run_task(self, task_id: str, coro, accounts: list,
|
||
proxy_rotate=None):
|
||
"""Запуск задачи на пуле аккаунтов с ротацией"""
|
||
self.running.add(task_id)
|
||
results = []
|
||
for acc in accounts:
|
||
client = await self._get_client(acc, proxy_rotate)
|
||
try:
|
||
result = await coro(client)
|
||
results.append({'account': acc, 'result': result})
|
||
except Exception as e:
|
||
results.append({'account': acc, 'error': str(e)})
|
||
finally:
|
||
await client.disconnect()
|
||
self.running.discard(task_id)
|
||
self.results[task_id] = results
|
||
return results
|
||
|
||
async def _get_client(self, account, proxy):
|
||
"""Создать клиент с прокси"""
|
||
session = account.get('session')
|
||
api_id = account.get('api_id', 2040)
|
||
api_hash = account.get('api_hash', 'b18441a1ff607e10a989891a5462e627')
|
||
client = TelegramClient(
|
||
StringSession(session) if session else StringSession(),
|
||
api_id, api_hash,
|
||
proxy=proxy
|
||
)
|
||
await client.connect()
|
||
if not await client.is_user_authorized():
|
||
await client.start(phone=account['phone'])
|
||
return client
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# МОДУЛЬ: Прокси-менеджер
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
async def proxy_check(proxy: dict) -> dict:
|
||
"""Проверка прокси перед назначением на аккаунт"""
|
||
import socket
|
||
try:
|
||
proto, host, port = proxy['type'], proxy['host'], proxy['port']
|
||
sock = socket.create_connection((host, port), timeout=5)
|
||
sock.close()
|
||
return {'proxy': proxy, 'alive': True, 'latency': None}
|
||
except Exception as e:
|
||
return {'proxy': proxy, 'alive': False, 'error': str(e)}
|
||
|
||
async def proxy_rotate(proxies: list, accounts: list) -> dict:
|
||
"""Назначение прокси на аккаунты (round-robin)"""
|
||
alive = [p for p in proxies if (await proxy_check(p))['alive']]
|
||
mapping = {}
|
||
for i, acc in enumerate(accounts):
|
||
mapping[acc['id']] = alive[i % len(alive)] if alive else None
|
||
return mapping |