mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 06:24:18 +02:00
Added and updated repositories 2025-11-22 08:13:29
This commit is contained in:
11
KeyZenD/modules/VideoDistortion.py
Normal file
11
KeyZenD/modules/VideoDistortion.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# Python code obfuscated by www.development-tools.net
|
||||
|
||||
|
||||
import base64, codecs
|
||||
magic = 'aW1wb3J0IGFzeW5jaW8NCmltcG9ydCBsb2dnaW5nDQpmcm9tIC4uIGltcG9ydCBsb2FkZXIsIHV0aWxzDQoNCmxvZ2dlciA9IGxvZ2dpbmcuZ2V0TG9nZ2VyKF9fbmFtZV9fKQ0KDQpAbG9hZGVyLnRkcw0KY2xhc3MgVmlkZW9EaXN0b3J0aW9ydE1vZChsb2FkZXIuTW9kdWxlKToNCgkiIiLQltC80YvRhSDQtNC70Y8g0LLQuNC00LXQviIiIg0KCXN0cmluZ3MgPSB7Im5hbWUiOiAiVmlkZW9EaXN0b3J0aW9uIn0NCg0KCUBsb2FkZXIudW5yZXN0cmljdGVkDQoJYXN5bmMgZGVmIHZkaXN0b3J0Y21kKHNlbGYsIG1lc3NhZ2UpOg0KCQkiIiIudmRpc3RvcnQgPHJlcGx5IHRvIHZpZGVvPiIiIg0KCQlhd2FpdCBtZXNzYWdlLmVkaXQoIjxiPtCX0LDQs9GA0YPQttCw0Y4g0LLQuNC00LXQvi4uLjwvYj4iKQ0KCQlhd2FpdCBhc3luY2lvLnNsZWVwKDUpDQoJCWF3YWl0IG1lc3NhZ2UuZWRpdCgiPGI+0JTQvtGB0YLQsNGOINC60LDQtNGA0YsuLi48L2'
|
||||
love = 'V+VvxAPtxWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPH0YKDh9Pj0L4t0YoDiATY0LHhYv48Y2V+VvxAPtxWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPu0Y7DfqP40LQDfATBVAP60YQDgATN0LfhYv48Y2V+VvxAPtxWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPr0LYDi9TN0YQDfgP70L/EwvQDfgP40YGDgqP+Yv4hCP9vCvVcQDbWPJS3LJy0VTSmrJ5wnJ8hp2kyMKNbAFxAPtxWLKqunKDtoJImp2SaMF5woTyyoaDhp2IhMS9znJkyXT1yp3AuM2HhL2uuqPjtVzu0qUN6Yl94rJI0LF5goP9zY05yqzIlE29hozSUnKMyJJ91IKNhoKN0VvjtL2SjqTyiow0vCTV+GzI2MKVtE29hozRtE2y2MFOMo3HtIKNuCP9vCvVcQDbWPJS3LJy0VT1yp3AuM2HhMJEcqPtvJJ91VUquplOlnJAepz9foTIxVFVcQDbWPD0XVvVv'
|
||||
god = 'DQppbXBvcnQgYXN5bmNpbw0KaW1wb3J0IGxvZ2dpbmcNCmZyb20gLi4gaW1wb3J0IGxvYWRlciwgdXRpbHMNCg0KbG9nZ2VyID0gbG9nZ2luZy5nZXRMb2dnZXIoX19uYW1lX18pDQoNCkBsb2FkZXIudGRzDQpjbGFzcyBWaWRlb0Rpc3RvcnRpb3J0TW9kKGxvYWRlci5Nb2R1bGUpOg0KCSLQltC80YvRhSDQtNC70Y8g0LLQuNC00LXQviINCglzdHJpbmdzID0geyJuYW1lIjogIlZpZGVvRGlzdG9ydGlvbiJ9DQoNCglAbG9hZGVyLnVucmVzdHJpY3RlZA0KCWFzeW5jIGRlZiB2ZGlzdG9ydGNtZChzZWxmLCBtZXNzYWdlKToNCgkJIi52ZGlzdG9ydCA8cmVwbHkgdG8gdmlkZW8+Ig0KCQlhd2FpdCBtZXNzYWdlLmVkaXQoIjxiPtCX0LDQs9GA0YPQttCw0Y4g0LLQuNC00LXQvi4uLjwvYj4iKQ0KCQlhd2FpdCBhc3luY2lvLnNsZWVwKDUpDQoJCWF3YWl0IG1lc3NhZ2UuZWRpdCgiPGI+0JTQvtGB0YLQsNGOINC60LDQtNGA0YsuLi48L2I+IikNCg'
|
||||
destiny = 'xWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPH0YKDh9Pj0L4t0YoDiATY0LHhYv48Y2V+VvxAPtxWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPu0Y7DfqP40LQDfATBVAP60YQDgATN0LfhYv48Y2V+VvxAPtxWLKqunKDtLKA5ozAcol5moTIypPt1XD0XPDyuq2ScqPOgMKAmLJqyYzIxnKDbVwkvCgPr0LYDi9TN0YQDfgP70L/EwvQDfgP40YGDgqP+Yv4hCP9vCvVcQDbWPJS3LJy0VTSmrJ5wnJ8hp2kyMKNbAFxAPtxWLKqunKDtoJImp2SaMF5woTyyoaDhp2IhMS9znJkyXT1yp3AuM2HhL2uuqPjtVzu0qUN6Yl94rJI0LF5goP9zY05yqzIlE29hozSUnKMyJJ91IKNhoKN0VvjtL2SjqTyiow0vCTV+GzI2MKVtE29hozRtE2y2MFOMo3HtIKNuCP9vCvVcQDbWPJS3LJy0VT1yp3AuM2HhMJEcqPtvJJ91VUquplOlnJAepz9foTIxVFVcQDbWPD0XVvVvQDbWPD=='
|
||||
joy = '\x72\x6f\x74\x31\x33'
|
||||
trust = eval('\x6d\x61\x67\x69\x63') + eval('\x63\x6f\x64\x65\x63\x73\x2e\x64\x65\x63\x6f\x64\x65\x28\x6c\x6f\x76\x65\x2c\x20\x6a\x6f\x79\x29') + eval('\x67\x6f\x64') + eval('\x63\x6f\x64\x65\x63\x73\x2e\x64\x65\x63\x6f\x64\x65\x28\x64\x65\x73\x74\x69\x6e\x79\x2c\x20\x6a\x6f\x79\x29')
|
||||
eval(compile(base64.b64decode(eval('\x74\x72\x75\x73\x74')),'<string>','exec'))
|
||||
1094
SenkoGuardian/SenModules/Gemini.py
Normal file
1094
SenkoGuardian/SenModules/Gemini.py
Normal file
File diff suppressed because it is too large
Load Diff
134
SenkoGuardian/SenModules/GiftFinder.py
Normal file
134
SenkoGuardian/SenModules/GiftFinder.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
|
||||
from .. import loader, utils
|
||||
from herokutl.tl.functions.payments import GetSavedStarGiftsRequest
|
||||
from herokutl.tl.functions.channels import GetFullChannelRequest
|
||||
from herokutl.tl.types import Message, StarGiftUnique, Channel
|
||||
from herokutl.errors.rpcerrorlist import DocumentInvalidError, FloodWaitError, ChatAdminRequiredError
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
@loader.tds
|
||||
class GiftFinderMod(loader.Module):
|
||||
"""Парсер пользователей с NFT-подарками в чате."""
|
||||
strings = {
|
||||
"name": "GiftFinder",
|
||||
"not_a_chat": "🚫 <b>Не удалось найти указанный чат.</b>",
|
||||
"scanning": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Сканирую участников...</b>",
|
||||
"scanning_supplement": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Список участников неполон. Дополнительно сканирую сообщения...</b>",
|
||||
"scanning_messages_only": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Участники скрыты. Сканирую только сообщения...</b>",
|
||||
"header": "<emoji document_id=5237868881267153432>🔖</emoji> Те у кого есть НФТ подарки:",
|
||||
"premium_star": "<emoji document_id=5274026806477857971>⭐️</emoji>",
|
||||
"flood_wait": "\n<emoji document_id=5212102117953384237>😖</emoji> Поймал FloodWait на {} секунд. Увеличиваю задержку и жду...",
|
||||
"scanning_safe": "⏳ <b>Сканирую участников...</b>",
|
||||
"scanning_supplement_safe": "⏳ <b>Список участников неполон. Дополнительно сканирую сообщения...</b>",
|
||||
"scanning_messages_only_safe": "⏳ <b>Участники скрыты. Сканирую только сообщения...</b>",
|
||||
"flood_wait_safe": "\n😖 Поймал FloodWait на {} секунд. Увеличиваю задержку и жду...",
|
||||
"no_users_found": "🚫 <b>В этом чате не найдено пользователей с NFT-подарками.</b>",
|
||||
}
|
||||
|
||||
async def _safe_edit(self, msg: Message, text_premium: str, text_safe: str):
|
||||
try:
|
||||
await msg.edit(text_premium)
|
||||
except DocumentInvalidError:
|
||||
await msg.edit(text_safe)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def giftscancmd(self, message: Message):
|
||||
"""
|
||||
Ищет пользователей с NFT-подарками в чате.
|
||||
Использование: .giftscan [лимит] или .giftscan [ID чата] [лимит]
|
||||
"""
|
||||
args = utils.get_args_raw(message)
|
||||
chat_arg = None
|
||||
msgs_limit = 3000
|
||||
if args:
|
||||
parts = args.split()
|
||||
first_arg = parts[0]
|
||||
if first_arg.lstrip('-').isdigit():
|
||||
chat_arg = int(first_arg)
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
msgs_limit = int(parts[1])
|
||||
else:
|
||||
chat_arg = first_arg
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
msgs_limit = int(parts[1])
|
||||
if not chat_arg and args and args.isdigit():
|
||||
msgs_limit = int(args)
|
||||
try:
|
||||
msg = await utils.answer(message, self.strings("scanning"))
|
||||
except DocumentInvalidError:
|
||||
msg = await utils.answer(message, self.strings("scanning_safe"))
|
||||
try:
|
||||
chat = await self.client.get_entity(chat_arg) if chat_arg is not None else await message.get_chat()
|
||||
except Exception:
|
||||
await self._safe_edit(msg, self.strings("not_a_chat"), self.strings("not_a_chat"))
|
||||
return
|
||||
user_ids = set()
|
||||
scan_messages_mode = False
|
||||
try:
|
||||
if isinstance(chat, Channel):
|
||||
full_chat = await self.client(GetFullChannelRequest(channel=chat))
|
||||
total_participants = full_chat.full_chat.participants_count
|
||||
else:
|
||||
total_participants = chat.participants_count
|
||||
participants = await self.client.get_participants(chat, limit=None)
|
||||
user_ids.update(user.id for user in participants)
|
||||
if len(participants) < total_participants:
|
||||
scan_messages_mode = True
|
||||
await self._safe_edit(msg, self.strings("scanning_supplement"), self.strings("scanning_supplement_safe"))
|
||||
except (ChatAdminRequiredError, AttributeError, TypeError, ValueError):
|
||||
scan_messages_mode = True
|
||||
await self._safe_edit(msg, self.strings("scanning_messages_only"), self.strings("scanning_messages_only_safe"))
|
||||
if scan_messages_mode:
|
||||
async for m in self.client.iter_messages(chat, limit=msgs_limit):
|
||||
if m.from_id and hasattr(m.from_id, 'user_id'):
|
||||
user_ids.add(m.from_id.user_id)
|
||||
found_users = []
|
||||
base_delay_min, base_delay_max, flood_penalty = 0.5, 1.5, 0.0
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
user = await self.client.get_entity(user_id)
|
||||
if user.bot or user.is_self: continue
|
||||
except Exception: continue
|
||||
await asyncio.sleep(random.uniform(base_delay_min + flood_penalty, base_delay_max + flood_penalty))
|
||||
while True:
|
||||
try:
|
||||
all_gifts = await self.client(GetSavedStarGiftsRequest(peer=user, offset="", limit=100))
|
||||
if gifts := [g for g in all_gifts.gifts if isinstance(g.gift, StarGiftUnique)]:
|
||||
raw_name = get_display_name(user)
|
||||
s_name = re.sub(r'[\u2066-\u2069\u200e\u200f\u202a-\u202e\u3164\u115f\u2800]', '', raw_name).strip()
|
||||
link_text = f"@{user.username}" if not s_name and user.username else (f"User ID: {user.id}" if not s_name else utils.escape_html(s_name))
|
||||
link = f'<a href="https://t.me/{user.username}">{link_text}</a>' if user.username else f'<a href="tg://user?id={user.id}">{link_text}</a>'
|
||||
p_icon = self.strings('premium_star') if getattr(user, 'premium', False) else ""
|
||||
found_users.append(f"• {p_icon} {link} - {len(gifts)}")
|
||||
break
|
||||
except FloodWaitError as e:
|
||||
current_text = (await self.client.get_messages(msg.chat_id, ids=msg.id)).text
|
||||
premium_text = current_text + self.strings("flood_wait").format(e.seconds)
|
||||
safe_text = current_text + self.strings("flood_wait_safe").format(e.seconds)
|
||||
await self._safe_edit(msg, premium_text, safe_text)
|
||||
flood_penalty += 0.2
|
||||
await asyncio.sleep(e.seconds)
|
||||
continue
|
||||
except Exception: break
|
||||
if not found_users:
|
||||
await self._safe_edit(msg, self.strings("no_users_found"), self.strings("no_users_found"))
|
||||
return
|
||||
user_list = "\n".join(found_users)
|
||||
response_text = f"{self.strings('header')}\n<blockquote expandable>{user_list}</blockquote>"
|
||||
safe_header = "🔖 " + self.strings("header").split("</emoji>")[1]
|
||||
safe_list = [line.replace(self.strings("premium_star"), "⭐️") for line in found_users]
|
||||
safe_user_list = '\n'.join(safe_list)
|
||||
response_text_safe = f"{safe_header}\n<blockquote expandable>{safe_user_list}</blockquote>"
|
||||
await self._safe_edit(msg, response_text, response_text_safe)
|
||||
# горе кодер
|
||||
21
SenkoGuardian/SenModules/LICENSE.md
Normal file
21
SenkoGuardian/SenModules/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Senko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
705
SenkoGuardian/SenModules/MaillingChatGT99.py
Normal file
705
SenkoGuardian/SenModules/MaillingChatGT99.py
Normal file
@@ -0,0 +1,705 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
__version__ = (1, 3, 0)
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from telethon import errors
|
||||
from telethon.tl import types as tl_types
|
||||
from telethon.utils import get_display_name, get_peer_id
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SpecificWarningFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if record.name == 'hikkatl.hikkatl.client.users' and \
|
||||
'PersistentTimestampOutdatedError' in record.getMessage() and \
|
||||
'GetChannelDifferenceRequest' in record.getMessage():
|
||||
return False
|
||||
return True
|
||||
|
||||
class ChatTarget:
|
||||
def __init__(self, raw_input: str, context_message: Optional[tl_types.Message] = None):
|
||||
self.raw = raw_input
|
||||
self.context = context_message
|
||||
self.entity_to_find: any = raw_input
|
||||
self.topic_id: Optional[int] = None
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
match = re.match(r"https://t\.me/(?:c/)?([\w\d_.-]+)/(\d+)", self.raw)
|
||||
if match:
|
||||
chat_identifier = match.group(1)
|
||||
if "/c/" in self.raw and chat_identifier.isdigit():
|
||||
self.entity_to_find = int(f"-100{chat_identifier}")
|
||||
else:
|
||||
self.entity_to_find = chat_identifier
|
||||
try:
|
||||
self.topic_id = int(match.group(2))
|
||||
except ValueError:
|
||||
pass
|
||||
elif self.context:
|
||||
self.entity_to_find = self.context.chat_id
|
||||
if getattr(self.context, 'is_topic_message', False):
|
||||
self.topic_id = getattr(self.context, 'reply_to_top_id', self.context.id)
|
||||
else:
|
||||
try:
|
||||
self.entity_to_find = int(self.raw)
|
||||
except ValueError:
|
||||
self.entity_to_find = self.raw
|
||||
|
||||
@loader.tds
|
||||
class MailChats(loader.Module):
|
||||
"""Модуль для массовой рассылки сообщений по чатам (Поддерживает все типы сообщений)"""
|
||||
strings = {
|
||||
"name": "MailChats",
|
||||
"add_chat": "➕ Добавить текущий чат/тему. Используйте .add_chat или .add_chat <ID/Username/Ссылка> (Можно сразу несколько ссылкок в 1 комманду).",
|
||||
"remove_chat": "🗑️ Удалить чат/тему по номеру из списка. Используйте .remove_chat <номер>.",
|
||||
"list_chats": "📜 Показать список чатов/тем для рассылки.",
|
||||
"add_msg": "➕ Добавить сообщение (ответом).",
|
||||
"remove_msg": "➖ Удалить сообщение по номеру. Используйте .remove_msg <номер>.",
|
||||
"clear_msgs": "🗑️ Очистить список сообщений.",
|
||||
"list_msgs": "📜 Показать список сообщений для рассылки.",
|
||||
"set_seller": "⚙️ Установить ID чата/пользователя продавца для уведомлений. Используйте .set_seller <ID/Username/Ссылка/'me'>.",
|
||||
"mail_status": "📊 Показать статус рассылки.",
|
||||
"start_mail": "🚀 Запустить рассылку. Использование: .start_mail <время_сек> <интервал_цикла_от-до_сек>.",
|
||||
"stop_mail": "⏹️ Остановить рассылку.",
|
||||
"error_getting_entity": "⚠️ Не удалось получить информацию о чате/сущности: {}",
|
||||
"error_sending_message": "⚠️ Ошибка при отправке сообщения ({}) в чат {} ({}): {}",
|
||||
"notification_sent": "✅ Уведомление отправлено.",
|
||||
"invalid_arguments": "⚠️ Неверные аргументы.",
|
||||
"chats_empty": "⚠️ Сначала добавьте чаты.",
|
||||
"messages_empty": "⚠️ Сначала добавьте сообщения.",
|
||||
"already_running": "⚠️ Рассылка уже запущена.",
|
||||
"started_mailing": "✅ Рассылка начата.\n⏳ Общее время: {} сек.\n⏱️ Интервал между циклами: {}-{} сек.\n⏱️ Интервал между чатами: ~{}-{} сек\n⏱️ Интервал между сообщениями в чате: ~{}-{} сек",
|
||||
"stopped_mailing": "✅ Рассылка остановлена.",
|
||||
"not_running": "⚠️ Рассылка не активна.",
|
||||
"chat_added": "✅ Чат/тема '{}' добавлен в список рассылки.",
|
||||
"chat_already_added": "⚠️ Чат/тема '{}' уже в списке.",
|
||||
"chat_removed": "✅ #{} '{}' удален из списка рассылки.",
|
||||
"invalid_chat_selection": "⛔️ Неверный номер чата.",
|
||||
"chats_cleared": "✅ Все чаты удалены из списка.",
|
||||
"messages_cleared": "✅ Список сообщений очищен.",
|
||||
"no_chats": "📃 Список чатов пуст.",
|
||||
"no_messages": "✍️ Ответьте на сообщение, чтобы добавить его в список. Список сообщений пуст.",
|
||||
"message_added": "✅ Сообщение добавлено (Snippet: {}).",
|
||||
"message_removed": "✅ Сообщение №{} удалено (Snippet: {}).",
|
||||
"invalid_message_number": "✍️ Укажите корректный номер сообщения.",
|
||||
"seller_set": "✅ Установлен чат продавца.",
|
||||
"duration_invalid": "✍️ Использование: .start_mail <время_сек> <интервал_цикла_от-до_сек>. Укажите целое число для времени и интервал между циклами (например: 45-70).",
|
||||
"seller_notification": "Автоматическое уведомление: рассылка завершена",
|
||||
"mailing_complete": "✅ Рассылка завершена!",
|
||||
"safe_mode_enabled": "🟢 <b>Безопасный режим ВКЛЮЧЁН</b>\n• Только группы/каналы\n• Макс {} чатов/цикл\n• Интервал между чатами: ~{}-{} сек\n• Интервал между циклами: ~{}-{} сек\n• Интервал между сообщениями в чате: ~{}-{} сек",
|
||||
"safe_mode_disabled": "🔴 <b>Безопасный режим ВЫКЛЮЧЕН</b>",
|
||||
"mail_not_running": "⚠️ Рассылка не активна.",
|
||||
"no_permission": "️️️️️️️️️️️️⚠️ Нет прав на отправку в чат {} ({}), пропускаем.",
|
||||
"processing_entity": "⏳ Обработка сущности...",
|
||||
"failed_to_send_message": "⚠️ Не удалось отправить сообщение {} в чат {}. Причина: {}",
|
||||
"failed_perm_check": "⚠️ Не удалось проверить права в чатe {} ({}) из-за ошибки: {}. Пропускаем.",
|
||||
"permission_denied_skip": "🚫 Пропуск чата {} (ID: {}, Topic: {}) из-за отсутствия прав на отправку. Причина: {}",
|
||||
"cfg_safe_mode": "Включить безопасный режим (Отправка только по группам/каналам, больше задержка)",
|
||||
"cfg_max_chats_safe": "Максимальное кол-во чатов за цикл в безопасном режиме",
|
||||
"cfg_chats_interval": "Интервал между чатами (сек, от-до). Пример: 2,5",
|
||||
"cfg_safe_chats_interval": "Интервал между чатами в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 10,20",
|
||||
"cfg_safe_cycle_interval": "Интервал между циклами в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 180,300",
|
||||
"cfg_safe_message_interval": "Интервал между сообщениями в 1 чат в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 5,10",
|
||||
"cfg_message_interval": "Интервал между сообщениями в 1 чат (сек, от-до). Пример: 1,3",
|
||||
"cfg_delete_replies_delay": "⏱️ Задержка автоудаления для ответов команд (сек, 0 - не удалять)",
|
||||
"cfg_randomize_messages": "Рандомизировать сообщения (1 случайное сообщение на чат за цикл)",
|
||||
"add_chat_summary_title": "<b>Результаты добавления чатов:</b>\n\n",
|
||||
"add_chat_success_header": "<b>✅ Добавлено:</b>\n",
|
||||
"add_chat_already_exists_header": "<b>⚠️ Уже существуют:</b>\n",
|
||||
"add_chat_errors_header": "<b>❌ Ошибки:</b>\n",
|
||||
"no_valid_chats_provided": "⚠️ Не предоставлено валидных идентификаторов чатов или произошли ошибки при их обработке.",
|
||||
}
|
||||
PERMISSION_ERRORS = {
|
||||
"ChatForbiddenError", "UserBannedInChannelError", "ChatWriteForbiddenError",
|
||||
"ChatAdminRequiredError", "UserBlocked", "TopicClosedError",
|
||||
"TopicEditedError", "ForumTopicDeletedError",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
logger.setLevel(logging.WARNING)
|
||||
h_logger = logging.getLogger('hikkatl.hikkatl.client.users')
|
||||
if not any(isinstance(f, SpecificWarningFilter) for f in h_logger.filters):
|
||||
h_logger.addFilter(SpecificWarningFilter())
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply SpecificWarningFilter: {e}")
|
||||
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue("safe_mode", False, self.strings["cfg_safe_mode"], validator=loader.validators.Boolean()),
|
||||
loader.ConfigValue("max_chats_safe", 10, self.strings["cfg_max_chats_safe"], validator=loader.validators.Integer(minimum=1)),
|
||||
loader.ConfigValue("chats_interval", "2,5", self.strings["cfg_chats_interval"]),
|
||||
loader.ConfigValue("safe_chats_interval", "10,20", self.strings["cfg_safe_chats_interval"]),
|
||||
loader.ConfigValue("safe_cycle_interval", "180,300", self.strings["cfg_safe_cycle_interval"]),
|
||||
loader.ConfigValue("safe_message_interval", "5,10", self.strings["cfg_safe_message_interval"]),
|
||||
loader.ConfigValue("message_interval", "1,3", self.strings["cfg_message_interval"]),
|
||||
loader.ConfigValue("delete_replies_delay", 5, self.strings["cfg_delete_replies_delay"], validator=loader.validators.Integer(minimum=0)),
|
||||
loader.ConfigValue("randomize_messages", False, self.strings["cfg_randomize_messages"], validator=loader.validators.Boolean()),
|
||||
)
|
||||
self.chats: Dict[Tuple[int, Optional[int]], str] = {}
|
||||
self.messages: List[Dict] = []
|
||||
self.mail_task: Optional[asyncio.Task] = None
|
||||
self.seller_chat_id: Optional[int] = None
|
||||
self.total_messages_sent = 0
|
||||
self.start_time: Optional[datetime] = None
|
||||
self.end_time: Optional[datetime] = None
|
||||
self.is_running = False
|
||||
self.lock = asyncio.Lock()
|
||||
self._current_cycle_start_time: Optional[datetime] = None
|
||||
self._processed_chats_in_cycle = 0
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
await self._load_data()
|
||||
|
||||
def _get_db_chats(self):
|
||||
return {str(k): v for k, v in self.chats.items()}
|
||||
|
||||
def _save_db_chats(self):
|
||||
self.db.set(self.strings["name"], "chats", self._get_db_chats())
|
||||
|
||||
async def _load_data(self):
|
||||
stored_chats = self.db.get(self.strings["name"], "chats", {})
|
||||
migrated_chats = {}
|
||||
needs_resave = False
|
||||
if isinstance(stored_chats, dict):
|
||||
for key, name in stored_chats.items():
|
||||
try:
|
||||
chat_tuple = eval(key)
|
||||
if isinstance(chat_tuple, tuple) and len(chat_tuple) == 2:
|
||||
migrated_chats[chat_tuple] = name
|
||||
else:
|
||||
migrated_chats[(int(key), None)] = name
|
||||
needs_resave = True
|
||||
except Exception:
|
||||
try:
|
||||
migrated_chats[(int(key), None)] = name
|
||||
needs_resave = True
|
||||
except Exception:
|
||||
logger.warning(f"Could not migrate chat key '{key}'")
|
||||
elif isinstance(stored_chats, list):
|
||||
for chat_id in stored_chats:
|
||||
migrated_chats[(int(chat_id), None)] = f"Chat {chat_id}"
|
||||
needs_resave = True
|
||||
self.chats = migrated_chats
|
||||
if needs_resave:
|
||||
self._save_db_chats()
|
||||
self.messages = self.db.get(self.strings["name"], "messages", [])
|
||||
self.seller_chat_id = self.db.get(self.strings["name"], "seller_chat_id")
|
||||
|
||||
async def _edit_or_reply_and_handle_deletion(self, message_event, text: str, delay: Optional[int] = None):
|
||||
if delay is None:
|
||||
delay = self.config["delete_replies_delay"]
|
||||
processed_message = None
|
||||
can_edit = message_event and hasattr(message_event, "edit") and callable(message_event.edit)
|
||||
try:
|
||||
if can_edit:
|
||||
try:
|
||||
if getattr(message_event, "deleted", False):
|
||||
can_edit = False
|
||||
else:
|
||||
processed_message = await message_event.edit(text, parse_mode='html')
|
||||
except errors.MessageNotModifiedError:
|
||||
processed_message = message_event
|
||||
except errors.MessageIdInvalidError:
|
||||
can_edit = False
|
||||
except errors.RPCError as e:
|
||||
can_edit = False
|
||||
logger.warning(f"RPC ошибка при попытке ({type(e).__name__}) редактировать {getattr(message_event, 'id', 'N/A')}: {e}. Попытка отправить новое.")
|
||||
if not processed_message or not can_edit:
|
||||
chat_to_reply = None
|
||||
if message_event and hasattr(message_event, "chat_id") and message_event.chat_id is not None: chat_to_reply = message_event.chat_id
|
||||
elif message_event and hasattr(message_event, "chat") and message_event.chat is not None: chat_to_reply = utils.get_peer_id(message_event.chat)
|
||||
if chat_to_reply:
|
||||
processed_message = await self.client.send_message(chat_to_reply, text, parse_mode='html')
|
||||
else:
|
||||
return None
|
||||
except Exception as e_edit_reply_outer:
|
||||
logger.error(f"Критическая ошибка на этапе редактирования/отправки сообщения: {e_edit_reply_outer}")
|
||||
return None
|
||||
if not processed_message:
|
||||
return None
|
||||
if delay > 0:
|
||||
self.client.loop.create_task(self._delete_message_after_delay(processed_message, delay))
|
||||
return processed_message
|
||||
|
||||
async def _delete_message_after_delay(self, message, delay):
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
if hasattr(message, 'delete') and not getattr(message, 'deleted', False):
|
||||
await message.delete()
|
||||
except errors.MessageDeleteForbiddenError:
|
||||
logger.warning(f"Нет прав на удаление сообщения {message.id}.")
|
||||
except Exception as e_del:
|
||||
logger.warning(f"Произошла ошибка при удалении сообщения {message.id}: {e_del}")
|
||||
|
||||
async def _find_chat(self, target: ChatTarget) -> Optional[dict]:
|
||||
try:
|
||||
entity = await self.client.get_entity(target.entity_to_find)
|
||||
chat_id = get_peer_id(entity)
|
||||
topic_id = target.topic_id if getattr(entity, 'forum', False) else None
|
||||
display_name = utils.escape_html(get_display_name(entity))
|
||||
if topic_id:
|
||||
try:
|
||||
topic_msg = await self.client.get_messages(entity, ids=topic_id)
|
||||
if topic_msg and isinstance(getattr(topic_msg, "action", None), tl_types.MessageActionTopicCreate):
|
||||
display_name += f" | Тема: '{utils.escape_html(topic_msg.action.title)}'"
|
||||
else:
|
||||
display_name += f" | Тема ID: {topic_id}"
|
||||
except Exception:
|
||||
display_name += f" | Тема ID: {topic_id}"
|
||||
return {"key": (chat_id, topic_id), "name": display_name}
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось найти чат '{target.raw}': {e}")
|
||||
return None
|
||||
|
||||
@loader.command()
|
||||
async def mail_help(self, message):
|
||||
"""📋 Показать пошаговую инструкцию по настройке рассылки."""
|
||||
help_text = """
|
||||
<blockquote expandable>
|
||||
<b>📋 Инструкция по настройке рассылки:</b>
|
||||
|
||||
<b>Шаг 1: Добавьте чаты для рассылки</b>
|
||||
• <b>Вручную:</b> Перейдите в нужный чат и напишите <code>.add_chat</code>.
|
||||
• <b>По ссылке/ID:</b> <code>.add_chat @username https://t.me/channel/123</code>
|
||||
|
||||
<b>✨ Бэкап и восстановление списка:</b>
|
||||
• <code>.dump_chats</code> — <b>Бэкап.</b> Модуль выгрузит в файл только те чаты, что уже есть в списке рассылки.
|
||||
• <code>.load_chats</code> — <b>Загрузка.</b> Ответьте этой командой на полученный файл, чтобы добавить чаты в рассылку.
|
||||
|
||||
<b>Шаг 2: Добавьте сообщения</b>
|
||||
• Ответьте на любое сообщение (текст, фото, видео) командой <code>.add_msg</code>.
|
||||
• Можно добавить несколько сообщений для рассылки.
|
||||
|
||||
<b>Шаг 3: Проверьте списки</b>
|
||||
• <code>.list_chats</code> — посмотреть список чатов. Если их больше 50, отправит файлом.
|
||||
• <code>.list_msgs</code> — посмотреть список сообщений.
|
||||
|
||||
<b>Шаг 4: Тонкая настройка (по желанию)</b>
|
||||
Откройте конфиг командой <code>.cfg MailChats</code>. Вот что значат основные параметры:
|
||||
|
||||
<b>-- Режимы работы --</b>
|
||||
• <code>safe_mode</code>: <b>Безопасный режим.</b> Если включить, рассылка будет идти медленнее и только в группы/каналы, чтобы снизить риск спам-блока.
|
||||
• <code>randomize_messages</code>: <b>Случайные сообщения.</b> Если включить, в каждый чат будет отправляться только ОДНО случайное сообщение из вашего списка. Если выключить — отправляются ВСЕ по порядку.
|
||||
|
||||
<b>-- Настройка пауз (формат: <code>min,max</code> секунд) --</b>
|
||||
• <code>chats_interval</code>: Пауза между отправкой в <b>разные чаты</b> (обычный режим). Пример: <code>2,5</code>.
|
||||
• <code>message_interval</code>: Пауза между отправкой <b>нескольких сообщений</b> в ОДИН чат (обычный режим).
|
||||
• <code>safe_chats_interval</code>: Пауза между чатами в <b>безопасном режиме</b> (больше для безопасности).
|
||||
• <code>safe_message_interval</code>: Пауза между сообщениями в <b>безопасном режиме</b>.
|
||||
• <code>safe_cycle_interval</code>: Пауза между <b>кругами рассылки</b> в безопасном режиме (например <code>180,300</code> = 3-5 минут).
|
||||
|
||||
<b>-- Прочее --</b>
|
||||
• <code>delete_replies_delay</code>: Через сколько секунд удалять ответы модуля (например, "✅ Чат добавлен"). Поставьте <code>0</code>, чтобы не удалять.
|
||||
• <code>max_chats_safe</code>: Сколько максимум чатов обрабатывать за один круг в <b>безопасном режиме</b>.
|
||||
|
||||
<b>Шаг 5: Запустите рассылку</b>
|
||||
• Используйте команду <code>.start_mail <время> <пауза></code>
|
||||
• <b>Пример:</b> <code>.start_mail 3600 180-300</code>
|
||||
<i>(Это запустит рассылку на 1 час (3600 сек) с паузой между кругами от 3 до 5 минут).</i>
|
||||
|
||||
<b>Другие команды:</b>
|
||||
• <code>.stop_mail</code> — остановить рассылку.
|
||||
• <code>.mail_status</code> — проверить, сколько времени осталось.
|
||||
• <code>.remove_chat <номер></code> — удалить чат из списка.
|
||||
• <code>.remove_msg <номер></code> — удалить сообщение.
|
||||
• <code>.clear_chats</code> / <code>.clear_msgs</code> - полная очистка списков.
|
||||
</blockquote>
|
||||
"""
|
||||
await self._edit_or_reply_and_handle_deletion(message, help_text, delay=240)
|
||||
|
||||
@loader.command()
|
||||
async def add_chat(self, message):
|
||||
"""➕ Добавить чат. Можно несколько: .add_chat @user1 ссылка ..."""
|
||||
args = utils.get_args_raw(message)
|
||||
targets_to_find = []
|
||||
if args:
|
||||
targets_to_find = [ChatTarget(raw) for raw in args.split()]
|
||||
elif message.chat:
|
||||
targets_to_find = [ChatTarget(str(message.chat_id), context_message=message)]
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_arguments"]); return
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(
|
||||
message,
|
||||
self.strings["processing_entity"],
|
||||
delay=0
|
||||
)
|
||||
tasks = [self._find_chat(target) for target in targets_to_find]
|
||||
results = await asyncio.gather(*tasks)
|
||||
added, exists, errors_list = [], [], []
|
||||
async with self.lock:
|
||||
for i, res in enumerate(results):
|
||||
if res:
|
||||
if res["key"] in self.chats:
|
||||
exists.append(f"• {res['name']}")
|
||||
else:
|
||||
self.chats[res["key"]] = res["name"]
|
||||
added.append(f"• {res['name']}")
|
||||
else:
|
||||
errors_list.append(f"• {utils.escape_html(targets_to_find[i].raw)}")
|
||||
if added:
|
||||
self._save_db_chats()
|
||||
if len(targets_to_find) > 50:
|
||||
summary = self.strings["add_chat_summary_title"]
|
||||
if added: summary += f"<b>✅ Добавлено:</b> {len(added)}\n"
|
||||
if exists: summary += f"<b>⚠️ Уже существуют:</b> {len(exists)}\n"
|
||||
if errors_list: summary += f"<b>❌ Ошибки:</b> {len(errors_list)}\n"
|
||||
final_summary = summary.strip()
|
||||
else:
|
||||
summary = ""
|
||||
if added: summary += self.strings["add_chat_success_header"] + "\n".join(added) + "\n\n"
|
||||
if exists: summary += self.strings["add_chat_already_exists_header"] + "\n".join(exists) + "\n\n"
|
||||
if errors_list: summary += self.strings["add_chat_errors_header"] + "\n".join(errors_list)
|
||||
if not summary.strip():
|
||||
final_summary = self.strings["no_valid_chats_provided"]
|
||||
else:
|
||||
final_summary = self.strings["add_chat_summary_title"] + summary.strip()
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, final_summary)
|
||||
|
||||
@loader.command()
|
||||
async def remove_chat(self, message):
|
||||
"""🗑️ Удалить чат по номеру."""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args or not args.isdigit():
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_chat_selection"]); return
|
||||
idx_to_remove = int(args) - 1
|
||||
async with self.lock:
|
||||
sorted_keys = sorted(self.chats.keys(), key=lambda k: (self.chats[k], k[0], k[1] or -1))
|
||||
if 0 <= idx_to_remove < len(sorted_keys):
|
||||
key_to_remove = sorted_keys[idx_to_remove]
|
||||
removed_name = self.chats.pop(key_to_remove)
|
||||
self._save_db_chats()
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chat_removed"].format(idx_to_remove + 1, removed_name))
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_chat_selection"])
|
||||
|
||||
@loader.command()
|
||||
async def clear_chats(self, message):
|
||||
"""🗑️ Очистить список чатов."""
|
||||
async with self.lock:
|
||||
self.chats.clear()
|
||||
self.db.set(self.strings["name"], "chats", {})
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chats_cleared"])
|
||||
|
||||
@loader.command()
|
||||
async def list_chats(self, message):
|
||||
"""📜 Показать список чатов."""
|
||||
async with self.lock:
|
||||
current_chats_copy = dict(self.chats)
|
||||
if not current_chats_copy:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_chats"])
|
||||
return
|
||||
output_header = "Список чатов для рассылки:\n\n"
|
||||
sorted_items = sorted(current_chats_copy.items(), key=lambda item: (item[1], item[0][0], item[0][1] or -1))
|
||||
if len(sorted_items) > 50:
|
||||
file_content = output_header
|
||||
for i, ((cid, tid), name) in enumerate(sorted_items):
|
||||
topic_str = f' | Тема: {tid}' if tid is not None else ''
|
||||
file_content += f"{i+1}. {name} ({cid}{topic_str})\n"
|
||||
file = io.BytesIO(file_content.encode("utf-8"))
|
||||
file.name = "Mailing_Chat_List.txt"
|
||||
await self._edit_or_reply_and_handle_deletion(message, "📝 <b>Список чатов слишком большой, отправляю файлом...</b>", delay=0)
|
||||
await self.client.send_file(message.chat_id, file, caption=f"✅ <b>Список из {len(sorted_items)} чатов.</b>")
|
||||
return
|
||||
output = "<b>" + output_header.strip() + "</b>\n\n"
|
||||
for i, ((cid, tid), name) in enumerate(sorted_items):
|
||||
topic_str = f' | Тема: <code>{tid}</code>' if tid is not None else ''
|
||||
output += f"<b>{i+1}.</b> {utils.escape_html(name)} (<code>{cid}</code>{topic_str})\n"
|
||||
await self._edit_or_reply_and_handle_deletion(message, output, delay=60)
|
||||
|
||||
@loader.command()
|
||||
async def add_msg(self, message):
|
||||
"""➕ Добавить сообщение (ответом)."""
|
||||
reply = await message.get_reply_message()
|
||||
if not reply:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_messages"].split(". ")[0] + "."); return
|
||||
if reply.text: snippet_text = reply.text.replace("\n", " ")
|
||||
elif reply.photo: snippet_text = "[Фото]"
|
||||
elif reply.video: snippet_text = "[Видео]"
|
||||
elif reply.sticker:
|
||||
alt = next((attr.alt for attr in reply.sticker.attributes if isinstance(attr, tl_types.DocumentAttributeSticker)), "?")
|
||||
snippet_text = f"[Стикер: {alt}]"
|
||||
else: snippet_text = "[Медиа/Файл]"
|
||||
snippet = snippet_text[:100] + "..." if len(snippet_text) > 100 else snippet_text
|
||||
async with self.lock:
|
||||
self.messages.append({"id": reply.id, "chat_id": get_peer_id(reply.peer_id), "snippet": snippet})
|
||||
self.db.set(self.strings["name"], "messages", self.messages)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["message_added"].format(utils.escape_html(snippet)))
|
||||
|
||||
@loader.command()
|
||||
async def remove_msg(self, message):
|
||||
"""➖ Удалить сообщение по номеру."""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args or not args.isdigit():
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_message_number"]); return
|
||||
idx = int(args) - 1
|
||||
async with self.lock:
|
||||
if 0 <= idx < len(self.messages):
|
||||
removed = self.messages.pop(idx)
|
||||
self.db.set(self.strings["name"], "messages", self.messages)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["message_removed"].format(idx + 1, utils.escape_html(removed["snippet"])))
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_message_number"])
|
||||
|
||||
@loader.command()
|
||||
async def clear_msgs(self, message):
|
||||
"""🗑️ Очистить список сообщений."""
|
||||
async with self.lock:
|
||||
self.messages.clear()
|
||||
self.db.set(self.strings["name"], "messages", [])
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["messages_cleared"])
|
||||
|
||||
@loader.command()
|
||||
async def list_msgs(self, message):
|
||||
"""📜 Показать список сообщений."""
|
||||
if not self.messages:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_messages"]); return
|
||||
text = "<b>Список сообщений для рассылки:</b>\n\n"
|
||||
for i, msg in enumerate(self.messages):
|
||||
text += f"<b>{i + 1}.</b> {utils.escape_html(msg['snippet'])}\n"
|
||||
await self._edit_or_reply_and_handle_deletion(message, text, delay=60)
|
||||
|
||||
@loader.command()
|
||||
async def set_seller(self, message):
|
||||
"""⚙️ Установить ID для уведомлений."""
|
||||
args = utils.get_args_raw(message).strip()
|
||||
if not args:
|
||||
await self._edit_or_reply_and_handle_deletion(message, "✍️ Укажите ID чата, username, ссылку или 'me'."); return
|
||||
identifier = self.client.tg_id if args.lower() == 'me' else args
|
||||
try:
|
||||
entity = await self.client.get_entity(identifier)
|
||||
seller_id = get_peer_id(entity)
|
||||
async with self.lock:
|
||||
self.seller_chat_id = seller_id
|
||||
self.db.set(self.strings["name"], "seller_chat_id", seller_id)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["seller_set"] + f": {get_display_name(entity)} (<code>{seller_id}</code>)")
|
||||
except Exception as e:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["error_getting_entity"].format(e))
|
||||
|
||||
@loader.command()
|
||||
async def mail_status(self, message):
|
||||
"""📊 Показать статус рассылки."""
|
||||
async with self.lock:
|
||||
if not self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["not_running"]); return
|
||||
now = datetime.now()
|
||||
elapsed = now - self.start_time
|
||||
remaining = self.end_time - now
|
||||
status = (
|
||||
f"📊 <b>Статус рассылки:</b> Активна ✅\n"
|
||||
f"⏳ <b>Прошло:</b> {str(elapsed).split('.')[0]}\n"
|
||||
f"⏱️ <b>Осталось:</b> {str(remaining).split('.')[0] if remaining.total_seconds() > 0 else '0:00:00'}\n"
|
||||
f"✉️ <b>Отправлено сообщений:</b> {self.total_messages_sent}\n"
|
||||
f"🔄 <b>Цикл:</b> {self._processed_chats_in_cycle} чатов обработано"
|
||||
)
|
||||
await self._edit_or_reply_and_handle_deletion(message, status, delay=30)
|
||||
|
||||
@loader.command()
|
||||
async def start_mail(self, message):
|
||||
"""🚀 Запустить рассылку."""
|
||||
args = utils.get_args(message)
|
||||
if len(args) != 2:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["duration_invalid"]); return
|
||||
try:
|
||||
duration = int(args[0])
|
||||
min_interval, max_interval = map(float, args[1].replace(",", ".").split("-"))
|
||||
if not (duration > 0 and 0 <= min_interval <= max_interval): raise ValueError
|
||||
cycle_interval = (min_interval, max_interval)
|
||||
except Exception:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["duration_invalid"]); return
|
||||
async with self.lock:
|
||||
if self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["already_running"]); return
|
||||
if not self.chats:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chats_empty"]); return
|
||||
if not self.messages:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["messages_empty"]); return
|
||||
self.is_running = True
|
||||
self.total_messages_sent = 0
|
||||
self.start_time = datetime.now()
|
||||
self.end_time = self.start_time + timedelta(seconds=duration)
|
||||
self._current_cycle_start_time = None
|
||||
self._processed_chats_in_cycle = 0
|
||||
self.mail_task = self.client.loop.create_task(self._mail_loop(duration, cycle_interval, message))
|
||||
await self._edit_or_reply_and_handle_deletion(message, f"✅ Рассылка запущена на {duration} секунд.")
|
||||
|
||||
@loader.command()
|
||||
async def stop_mail(self, message):
|
||||
"""⏹️ Остановить рассылку."""
|
||||
async with self.lock:
|
||||
if not self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["not_running"]); return
|
||||
self.is_running = False
|
||||
if self.mail_task:
|
||||
self.mail_task.cancel()
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["stopped_mailing"])
|
||||
def _validate_interval_tuple(self, value, default_tuple: Tuple[float, float]) -> Tuple[float, float]:
|
||||
try:
|
||||
v_min, v_max = map(float, str(value).replace("-",",").split(','))
|
||||
if 0 <= v_min <= v_max: return (v_min, v_max)
|
||||
except Exception:
|
||||
pass
|
||||
return default_tuple
|
||||
|
||||
async def _is_safe_chat(self, entity: tl_types.TypePeer) -> bool:
|
||||
return isinstance(entity, (tl_types.Chat, tl_types.Channel)) and get_peer_id(entity) < -1000000000
|
||||
|
||||
async def _send_to_chat(self, target_chat_id: int, msg_info: dict, target_topic_id: Optional[int]) -> Tuple[bool, str]:
|
||||
try:
|
||||
original_msg = await self.client.get_messages(msg_info["chat_id"], ids=msg_info["id"])
|
||||
if not original_msg:
|
||||
return False, "Original message not found"
|
||||
for attempt in range(3):
|
||||
try:
|
||||
await self.client.send_message(entity=target_chat_id, message=original_msg, reply_to=target_topic_id)
|
||||
async with self.lock:
|
||||
self.total_messages_sent += 1
|
||||
return True, "OK" # :/
|
||||
except errors.FloodWaitError as e:
|
||||
if attempt == 2: return False, f"FloodWait ({e.seconds}s)"
|
||||
await asyncio.sleep(e.seconds + random.uniform(1, 3))
|
||||
except errors.SlowModeWaitError as e:
|
||||
await asyncio.sleep(e.seconds + random.uniform(0.2, 0.5))
|
||||
except Exception as e:
|
||||
if type(e).__name__ in self.PERMISSION_ERRORS:
|
||||
return False, type(e).__name__
|
||||
if attempt == 2: return False, str(e)
|
||||
await asyncio.sleep(random.uniform(2, 5))
|
||||
return False, "Max retries"
|
||||
except Exception as e:
|
||||
return False, f"Get message error: {e}"
|
||||
|
||||
async def _mail_loop(self, duration_seconds: int, cycle_interval_seconds_range: Tuple[float, float], initial_command_message_event):
|
||||
"""Оригинальный, надежный цикл рассылки"""
|
||||
end_time_loop = self.start_time + timedelta(seconds=duration_seconds)
|
||||
final_status_for_user = self.strings["mailing_complete"]
|
||||
try:
|
||||
while self.is_running and datetime.now() < end_time_loop:
|
||||
self._current_cycle_start_time = datetime.now()
|
||||
self._processed_chats_in_cycle = 0
|
||||
async with self.lock:
|
||||
current_chats = list(self.chats.keys())
|
||||
current_messages_list = list(self.messages)
|
||||
is_safe_mode = self.config["safe_mode"]
|
||||
randomize_messages_cfg = self.config["randomize_messages"]
|
||||
max_c_per_cycle = self.config["max_chats_safe"]
|
||||
chats_interval_key = "safe_chats_interval" if is_safe_mode else "chats_interval"
|
||||
short_interval = self._validate_interval_tuple(self.config[chats_interval_key], (10, 20) if is_safe_mode else (2, 5))
|
||||
message_interval_key = "safe_message_interval" if is_safe_mode else "message_interval"
|
||||
message_interval_val = self._validate_interval_tuple(self.config[message_interval_key], (5, 10) if is_safe_mode else (1, 3))
|
||||
if not current_chats or not current_messages_list:
|
||||
final_status_for_user = "Рассылка остановлена: список чатов или сообщений пуст."
|
||||
break
|
||||
random.shuffle(current_chats)
|
||||
chats_for_this_cycle = current_chats[:min(max_c_per_cycle if is_safe_mode else len(current_chats), len(current_chats))]
|
||||
for i, (chat_id_target, topic_id_target) in enumerate(chats_for_this_cycle):
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
messages_to_send_now = [random.choice(current_messages_list)] if randomize_messages_cfg else current_messages_list
|
||||
for message_detail in messages_to_send_now:
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
success_send, reason_send = await self._send_to_chat(chat_id_target, message_detail, topic_id_target)
|
||||
if not success_send:
|
||||
if reason_send in self.PERMISSION_ERRORS:
|
||||
logger.warning(f"Permission issue in {chat_id_target}, skipping chat.")
|
||||
else:
|
||||
logger.warning(f"Failed to send to {chat_id_target}: {reason_send}")
|
||||
break
|
||||
if len(messages_to_send_now) > 1:
|
||||
await asyncio.sleep(random.uniform(*message_interval_val))
|
||||
self._processed_chats_in_cycle += 1
|
||||
if i < len(chats_for_this_cycle) - 1:
|
||||
await asyncio.sleep(random.uniform(*short_interval))
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
await asyncio.sleep(random.uniform(*cycle_interval_seconds_range))
|
||||
except asyncio.CancelledError:
|
||||
final_status_for_user = self.strings["stopped_mailing"]
|
||||
except Exception as e_loop:
|
||||
logger.exception("Критическая ошибка в цикле рассылки:")
|
||||
final_status_for_user = f"❌ Критическая ошибка: {type(e_loop).__name__}"
|
||||
finally:
|
||||
final_report = f"{final_status_for_user} (Отправлено: {self.total_messages_sent})"
|
||||
await self.client.send_message(initial_command_message_event.chat_id, final_report)
|
||||
if self.seller_chat_id:
|
||||
await self.client.send_message(self.seller_chat_id, f"🔔 Уведомление: {final_report}")
|
||||
async with self.lock:
|
||||
self.is_running = False
|
||||
self.mail_task = None
|
||||
|
||||
@loader.command()
|
||||
async def dump_chats(self, message):
|
||||
"""📤 Выгрузить список чатов рассылки в .txt файл (для бэкапа)."""
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(message, "⏳ <b>Экспорт списка рассылки...</b>", delay=0)
|
||||
async with self.lock:
|
||||
if not self.chats:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "⚠️ <b>Список чатов для рассылки пуст.</b>")
|
||||
return
|
||||
export_list = []
|
||||
for (cid, tid), name in self.chats.items():
|
||||
if tid is not None and cid < -1000000000:
|
||||
chat_id_for_link = str(cid)[4:]
|
||||
export_list.append(f"https://t.me/c/{chat_id_for_link}/{tid}")
|
||||
else:
|
||||
export_list.append(str(cid))
|
||||
file_content = "\n".join(export_list)
|
||||
file = io.BytesIO(file_content.encode("utf-8"))
|
||||
file.name = "mailing_list_backup.txt"
|
||||
await self.client.send_file(
|
||||
message.chat_id,
|
||||
file,
|
||||
caption=f"✅ <b>Экспортировано {len(export_list)} чатов из списка рассылки.</b>\n\nИспользуйте <code>.load_chats</code> в ответе на этот файл, чтобы импортировать их.")
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "✅ <b>Экспорт завершен!</b>")
|
||||
|
||||
@loader.command()
|
||||
async def load_chats(self, message):
|
||||
"""📤 Загрузить чаты в рассылку из .txt файла (ответом на файл)."""
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.document:
|
||||
await self._edit_or_reply_and_handle_deletion(message, "✍️ <b>Ответьте на .txt файл с ID чатов.</b>")
|
||||
return
|
||||
if reply.document.mime_type != 'text/plain':
|
||||
await self._edit_or_reply_and_handle_deletion(message, "⚠️ <b>Файл должен быть в формате .txt</b>")
|
||||
return
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(message, "⏳ <b>Начинаю загрузку чатов из файла...</b>", delay=0)
|
||||
content = await reply.download_media(bytes)
|
||||
chat_identifiers = content.decode("utf-8").splitlines()
|
||||
chat_identifiers = [line.strip() for line in chat_identifiers if line.strip()]
|
||||
if not chat_identifiers:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "⚠️ <b>Файл пуст или не содержит идентификаторов чатов.</b>")
|
||||
return
|
||||
added, exists, errors_list = [], [], []
|
||||
for i, identifier in enumerate(chat_identifiers):
|
||||
if i > 0 and i % 20 == 0:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, f"⏳ <b>Обработано {i}/{len(chat_identifiers)}...</b>", delay=0)
|
||||
res = await self._find_chat(ChatTarget(identifier))
|
||||
if res:
|
||||
if res["key"] not in self.chats:
|
||||
self.chats[res["key"]] = res["name"]
|
||||
added.append(res["name"])
|
||||
else:
|
||||
exists.append(res["name"])
|
||||
else:
|
||||
errors_list.append(identifier)
|
||||
if added:
|
||||
self._save_db_chats()
|
||||
summary = f"✅ <b>Загрузка завершена!</b>\n\n"
|
||||
if added: summary += f"<b>Добавлено новых чатов:</b> {len(added)}\n"
|
||||
if exists: summary += f"<b>Уже были в списке:</b> {len(exists)}\n"
|
||||
if errors_list: summary += f"<b>Не удалось найти:</b> {len(errors_list)}\n"
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, summary)
|
||||
81
SenkoGuardian/SenModules/NekoEditorMod.py
Normal file
81
SenkoGuardian/SenModules/NekoEditorMod.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
from hikkatl.types import Message
|
||||
from .. import loader, utils
|
||||
import random
|
||||
|
||||
@loader.tds
|
||||
class NekoEditorMod(loader.Module):
|
||||
"""Neko-редактор сообщений | Владелецы: @SstAngelStar × @ilovesenko """
|
||||
strings = {
|
||||
"name": "NekoEditor",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"enabled",
|
||||
False,
|
||||
"Автоматическое редактирование",
|
||||
validator=loader.validators.Boolean()
|
||||
)
|
||||
)
|
||||
|
||||
async def nekoedcmd(self, message: Message):
|
||||
"""Управление Neko-режимом | .nekoed [on/off]"""
|
||||
args = utils.get_args_raw(message)
|
||||
me = await message.client.get_me()
|
||||
is_premium = getattr(me, 'premium', False)
|
||||
if not args:
|
||||
status = "включён" if self.config["enabled"] else "выключен"
|
||||
return await utils.answer(message, f"🐱 NekoEditor: {status}")
|
||||
if args.lower() in ["on", "вкл", "1"]:
|
||||
self.config["enabled"] = True
|
||||
if is_premium:
|
||||
await utils.answer(message, '<emoji document_id="5335044582218412321">☺️</emoji> Режим включён! Nya~')
|
||||
else:
|
||||
await utils.answer(message, "🐾 Режим включён! Nya~")
|
||||
elif args.lower() in ["off", "выкл", "0"]:
|
||||
self.config["enabled"] = False
|
||||
if is_premium:
|
||||
await utils.answer(message, '<emoji document_id="5377309873614627829">👌</emoji> Режим выключен... >_<')
|
||||
else:
|
||||
await utils.answer(message, "🌀 Режим выключен... >_<", parse_mode=None)
|
||||
|
||||
self.db.set("NekoEditor", "enabled", self.config["enabled"])
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
if (
|
||||
not self.config["enabled"]
|
||||
or not getattr(message, "out", False)
|
||||
or getattr(message, "fwd_from", None)
|
||||
or getattr(message, "forward", None)
|
||||
or not message.text
|
||||
or "nekoed" in message.raw_text.lower()
|
||||
):
|
||||
return
|
||||
neko_words = ["Nya~", "UwU", "OwO", ">_<", "^^", "(≧▽≦)"]
|
||||
modified_text = message.text
|
||||
neko_word = random.choice(neko_words)
|
||||
if random.random() < 0.5:
|
||||
modified_text = f"{neko_word} {modified_text}"
|
||||
else:
|
||||
modified_text = f"{modified_text} {neko_word}"
|
||||
replacements = {
|
||||
"р": "w",
|
||||
"л": "w",
|
||||
"но": "ня",
|
||||
"на": "ня"
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
modified_text = modified_text.replace(old, new)
|
||||
try:
|
||||
if message.text != modified_text:
|
||||
await message.edit(modified_text)
|
||||
except Exception:
|
||||
pass
|
||||
7
SenkoGuardian/SenModules/README.md
Normal file
7
SenkoGuardian/SenModules/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
- 👋 Hi, I’m @SenkoGuardian or Senko. I'm doing modules for Heroku UserBot
|
||||
- 📫 How to reach me -> Telegram: @ilovesenko
|
||||
- My Telegram chanel: @SenkoGuardianModules
|
||||
<!---
|
||||
SenkoGuardian/SenkoGuardian is a ✨ special ✨ repository because its `README.md` (this file) appears on your GitHub profile.
|
||||
You can click the Preview link to take a look at your changes.
|
||||
--->
|
||||
BIN
ZetGoHack/nullmod/20250401_100043.jpg
Normal file
BIN
ZetGoHack/nullmod/20250401_100043.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 578 KiB |
927
ZetGoHack/nullmod/Chess.py
Normal file
927
ZetGoHack/nullmod/Chess.py
Normal file
@@ -0,0 +1,927 @@
|
||||
__version__ = (2, 0, 0)
|
||||
#░░░███░███░███░███░███
|
||||
#░░░░░█░█░░░░█░░█░░░█░█
|
||||
#░░░░█░░███░░█░░█░█░█░█
|
||||
#░░░█░░░█░░░░█░░█░█░█░█
|
||||
#░░░███░███░░█░░███░███
|
||||
#H:Mods Team [💎]
|
||||
|
||||
# meta developer: @nullmod
|
||||
# requires: python-chess
|
||||
# packurl: https://github.com/ZetGoHack/TestingModules/raw/main/chess.yml
|
||||
|
||||
|
||||
from .. import loader, utils
|
||||
from ..inline.types import BotInlineCall, InlineCall, InlineMessage
|
||||
|
||||
import asyncio
|
||||
import chess
|
||||
import chess.pgn
|
||||
import copy
|
||||
import random as r
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from telethon.tl.types import PeerUser, User, Message
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
class Timer:
|
||||
def __init__(self, scnds):
|
||||
self.starttime = scnds
|
||||
self.timers = {"white": scnds, "black": scnds}
|
||||
self.running = {"white": False, "black": False}
|
||||
self.last_time = time.monotonic()
|
||||
self.t = None
|
||||
|
||||
def minutes(self) -> int:
|
||||
return self.starttime // 60
|
||||
|
||||
async def _count(self):
|
||||
while True:
|
||||
await asyncio.sleep(0.1)
|
||||
now = time.monotonic()
|
||||
elapsed = now - self.last_time
|
||||
self.last_time = now
|
||||
for color in ("white", "black"):
|
||||
if self.running[color]:
|
||||
self.timers[color] = max(0, self.timers[color] - elapsed)
|
||||
|
||||
async def start(self, from_color: str = "white"):
|
||||
self.last_time = time.monotonic()
|
||||
if from_color == "restore":
|
||||
if self.running["white"]:
|
||||
from_color = "white"
|
||||
else:
|
||||
from_color = "black"
|
||||
await self._turn(from_color)
|
||||
self.t = asyncio.create_task(self._count())
|
||||
|
||||
async def switch(self):
|
||||
self.running["white"] = not self.running["white"]
|
||||
self.running["black"] = not self.running["black"]
|
||||
|
||||
async def _turn(self, color):
|
||||
now = time.monotonic()
|
||||
e = now - self.last_time
|
||||
self.last_time = now
|
||||
for clr in ("white", "black"):
|
||||
if self.running[clr]:
|
||||
self.timers[clr] = max(0, self.timers[clr] - e)
|
||||
self.running = {"white": color == "white", "black": color == "black"}
|
||||
|
||||
async def white_time(self):
|
||||
return round(self.timers["white"], 0)
|
||||
|
||||
async def black_time(self):
|
||||
return round(self.timers["black"], 0)
|
||||
|
||||
def restore(self, white_time: float, black_time: float, running: dict):
|
||||
self.timers["white"] = white_time
|
||||
self.timers["black"] = black_time
|
||||
self.running = running
|
||||
|
||||
def backup(self) -> dict:
|
||||
return {
|
||||
"white_time": self.timers["white"],
|
||||
"black_time": self.timers["black"],
|
||||
"running": self.running
|
||||
}
|
||||
|
||||
async def stop(self):
|
||||
if self.t:
|
||||
self.t.cancel()
|
||||
self.running = {"white": False, "black": False}
|
||||
|
||||
class Player(TypedDict):
|
||||
id: int
|
||||
name: str
|
||||
color: bool | None
|
||||
|
||||
class TimerDict(TypedDict):
|
||||
timer: Timer
|
||||
timer_loop: bool
|
||||
timer_is_set: bool
|
||||
message: InlineCall
|
||||
|
||||
class GameParams(TypedDict):
|
||||
chosen_figure_coord: str
|
||||
reason_of_ending: str
|
||||
promotion_move: str
|
||||
winner_color: bool | None
|
||||
resigner_color: bool | None
|
||||
draw_offerer: bool | None
|
||||
|
||||
class Game(TypedDict):
|
||||
board: chess.Board
|
||||
message: InlineCall
|
||||
root_node: chess.pgn.Game
|
||||
curr_node: chess.pgn.Game
|
||||
state: str
|
||||
reason: str
|
||||
add_params: GameParams
|
||||
|
||||
class GameObj(TypedDict):
|
||||
game_id: str
|
||||
game: Game
|
||||
sender: Player
|
||||
opponent: Player
|
||||
Timer: TimerDict
|
||||
time: int
|
||||
host_plays: bool
|
||||
style: dict[str, str]
|
||||
|
||||
GamesDict = dict[str, GameObj]
|
||||
|
||||
@loader.tds
|
||||
class Chess(loader.Module):
|
||||
"""A reworked version of the Chess module"""
|
||||
strings = {
|
||||
"": "",
|
||||
"name": "Chess",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"play_self",
|
||||
False,
|
||||
"Allows you to make moves without turn checks (also, you can play with yourself)",
|
||||
validator=loader.validators.Boolean(),
|
||||
)
|
||||
)
|
||||
|
||||
async def client_ready(self):
|
||||
self.styles = {
|
||||
"figures-with-circles": {
|
||||
"symbol": "[♔⚪] ",
|
||||
"r": "♖⚫", "n": "♘⚫", "b": "♗⚫", "q": "♕⚫", "k": "♔⚫", "p": "♙⚫",
|
||||
"R": "♖⚪", "N": "♘⚪", "B": "♗⚪", "Q": "♕⚪", "K": "♔⚪", "P": "♙⚪",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
"figures": {
|
||||
"symbol": "[♔] ",
|
||||
"r": "♜", "n": "♞", "b": "♝", "q": "𝗾", "k": "♚", "p": "♟",
|
||||
"R": "♖", "N": "♘", "B": "♗", "Q": "𝗤", "K": "♔", "P": "♙",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
"letters": {
|
||||
"symbol": "[𝗞] ",
|
||||
"r": "𝗿", "n": "𝗻", "b": "𝗯", "q": "𝗾", "k": "𝗸", "p": "𝗽",
|
||||
"R": "𝗥", "N": "𝗡", "B": "𝗕", "Q": "𝗤", "K": "𝗞", "P": "𝗣",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
"figures-with-cyr-letters": {
|
||||
"symbol": "[♔Б] ",
|
||||
"r": "♖Ч", "n": "♘Ч", "b": "♗Ч", "q": "♕Ч", "k": "♔Ч", "p": "♙Ч",
|
||||
"R": "♖Б", "N": "♘Б", "B": "♗Б", "Q": "♕Б", "K": "♔Б", "P": "♙Б",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
"figures-with-latin-letters": {
|
||||
"symbol": "[♔W] ",
|
||||
"r": "♖B", "n": "♘B", "b": "♗B", "q": "♕B", "k": "♔B", "p": "♙B",
|
||||
"R": "♖W", "N": "♘W", "B": "♗W", "Q": "♕W", "K": "♔W", "P": "♙W",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
"figures-with-comb-letters": {
|
||||
"symbol": "[♔ⷱ] ",
|
||||
"r": "♖ⷱ", "n": "♘ⷱ", "b": "♗ⷱ", "q": "♕ⷱ", "k": "♔ⷱ", "p": "♙ⷱ",
|
||||
"R": "♖ⷠ", "N": "♘ⷠ", "B": "♗ⷠ", "Q": "♕ⷠ", "K": "♔ⷠ", "P": "♙ⷠ",
|
||||
"move": "●", "capture": "×", "promotion": "↻", "capture_promotion": "×↻",
|
||||
},
|
||||
}
|
||||
self.coords = {
|
||||
f"{col}{row}": "" for row in range(1, 9)
|
||||
for col in "hgfedcba"
|
||||
}
|
||||
self.games: GamesDict = self.get("games_backup", {})
|
||||
self.gsettings = {
|
||||
"style": "figures-with-circles",
|
||||
}
|
||||
self.pgn = {
|
||||
'Event': "Chess Play In Module",
|
||||
'Site': "https://t.me/nullmod/",
|
||||
'Date': "{date}",
|
||||
'Round': "{game_id}",
|
||||
'White': "{player}",
|
||||
'Black': "{player}",
|
||||
}
|
||||
|
||||
async def _check_player(self, call: InlineCall, game_id: str, only_opponent: bool = False, skip_turn_check: bool = False) -> bool:
|
||||
if isinstance(call, (BotInlineCall, InlineCall, InlineMessage)):
|
||||
game = self.games[game_id]
|
||||
_from_id = call.from_user.id
|
||||
|
||||
if game.get("game", None) and game["game"]["state"] == "the_end":
|
||||
await call.answer(self.strings["game_ended"], show_alert=True)
|
||||
return
|
||||
if _from_id != game["sender"]["id"]:
|
||||
if _from_id != game["opponent"]["id"]:
|
||||
await call.answer(self.strings["not_available"])
|
||||
return False
|
||||
if _from_id == game["sender"]["id"] and only_opponent and not self.config["play_self"]:
|
||||
await call.answer(self.strings["not_you"])
|
||||
return False
|
||||
elif not self.config["play_self"] and game.get("game", None) and not skip_turn_check:
|
||||
if game["sender"]["color"] == game["game"]["board"].turn and game["sender"]["id"] != _from_id:
|
||||
await call.answer(self.strings["opp_move"])
|
||||
return False
|
||||
elif game["opponent"]["color"] == game["game"]["board"].turn and game["opponent"]["id"] != _from_id:
|
||||
await call.answer(self.strings["opp_move"])
|
||||
return False
|
||||
return True
|
||||
|
||||
async def get_players(self, message: Message):
|
||||
sender = {
|
||||
"id": message.from_id.user_id if isinstance(message.peer_id, PeerUser) else message.sender.id,
|
||||
"name": (await self.client.get_entity(message.from_id if isinstance(message.peer_id, PeerUser) else message.sender.id)).first_name
|
||||
}
|
||||
if message.is_reply:
|
||||
r = await message.get_reply_message()
|
||||
opponent = r.sender
|
||||
if not isinstance(opponent, User):
|
||||
await utils.answer(message, self.strings["not_a_user"])
|
||||
return (None, None)
|
||||
opp_id = opponent.id
|
||||
opp_name = opponent.first_name
|
||||
else:
|
||||
args = utils.get_args(message)
|
||||
if len(args)==0:
|
||||
await utils.answer(message, self.strings["noargs"])
|
||||
return (None, None)
|
||||
opponent = args[0]
|
||||
try:
|
||||
if opponent.isdigit():
|
||||
opp_id = int(opponent)
|
||||
opponent = await self.client.get_entity(opp_id)
|
||||
if not isinstance(opponent, User):
|
||||
await utils.answer(message, self.strings["not_a_user"])
|
||||
return (None, None)
|
||||
opp_name = opponent.first_name
|
||||
else:
|
||||
opponent = await self.client.get_entity(opponent)
|
||||
if not isinstance(opponent, User):
|
||||
await utils.answer(message, self.strings["not_a_user"])
|
||||
return (None, None)
|
||||
opp_name = opponent.first_name
|
||||
opp_id = opponent.id
|
||||
except:
|
||||
await utils.answer(message, self.strings["whosthat"])
|
||||
return (None, None)
|
||||
opponent = {
|
||||
"id": opp_id,
|
||||
"name": opp_name
|
||||
}
|
||||
return (sender, opponent)
|
||||
|
||||
async def _invite(self, call: InlineCall, game_id: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]
|
||||
await utils.answer(
|
||||
call,
|
||||
self.strings["invite"].format(opponent=utils.escape_html(self.games[game_id]["opponent"]["name"])) + self.strings['settings_text'].format(
|
||||
style=game['style'],
|
||||
|
||||
timer=self.strings['available'] if game['Timer']['available'] and not game['Timer']['timer']
|
||||
else self.strings['timer'].format(game['Timer']['timer'].minutes()) if game['Timer']['timer']
|
||||
else self.strings['not_available'],
|
||||
|
||||
color=self.strings['random'] if game['host_plays'] == 'r'
|
||||
else self.strings['white'] if game['host_plays'] == True
|
||||
else self.strings['black']
|
||||
),
|
||||
reply_markup = [
|
||||
[
|
||||
{
|
||||
"text": self.strings["yes"],
|
||||
"callback": self._init_game,
|
||||
"args": (game_id,)
|
||||
},
|
||||
{
|
||||
"text": self.strings["no"],
|
||||
"callback": self._init_game,
|
||||
"args": (game_id, "no")
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": self.strings["settings"],
|
||||
"callback": self.settings,
|
||||
"args": (game_id,)
|
||||
}
|
||||
]
|
||||
],
|
||||
disable_security=True
|
||||
)
|
||||
|
||||
async def settings(self, call: InlineCall, game_id: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]
|
||||
reply_markup = []
|
||||
if game["Timer"]["available"]:
|
||||
reply_markup.append([
|
||||
{"text": self.strings["time_btn"], "callback": self._settings, "args": (game_id, "t", )}
|
||||
])
|
||||
|
||||
reply_markup.extend([
|
||||
[
|
||||
{"text": self.strings["color_btn"], "callback": self._settings, "args": (game_id, "c", )}
|
||||
],
|
||||
[
|
||||
{"text": self.strings["style_btn"], "callback": self._settings, "args": (game_id, "s", )}
|
||||
],
|
||||
[
|
||||
{"text": self.strings['back'], "callback": self._invite, "args": (game_id,)}
|
||||
]
|
||||
])
|
||||
await utils.answer(
|
||||
call,
|
||||
self.strings['settings_text'].format(
|
||||
style=game['style'],
|
||||
|
||||
timer=self.strings['available'] if game['Timer']['available'] and not game['Timer']['timer']
|
||||
else self.strings['timer'].format(game['Timer']['timer'].minutes()) if game['Timer']['timer']
|
||||
else self.strings['not_available'],
|
||||
|
||||
color=self.strings['random'] if game['host_plays'] == 'r'
|
||||
else self.strings['white'] if game['host_plays'] == True
|
||||
else self.strings['black']
|
||||
),
|
||||
reply_markup=reply_markup,
|
||||
disable_security=True
|
||||
)
|
||||
async def _settings(self, call: InlineCall, game_id: str, ruleset: str | list):
|
||||
reply_markup = []
|
||||
text = "🍓"
|
||||
if isinstance(ruleset, str):
|
||||
if ruleset == "t":
|
||||
text = "⏳"
|
||||
reply_markup.extend([
|
||||
[
|
||||
{"text": self.strings['blitz_text'], "action": "answer", "message": self.strings['blitz_message']}
|
||||
],
|
||||
[
|
||||
{"text": self.strings['timer'].format(3), "callback":self._settings, "args": (game_id, ['Timer', 3])},
|
||||
{"text": self.strings['timer'].format(5), "callback":self._settings, "args": (game_id, ['Timer', 5])},
|
||||
],
|
||||
[
|
||||
{"text": self.strings['rapid_text'], "action": "answer", "message": self.strings['rapid_message']}
|
||||
],
|
||||
[
|
||||
{"text": self.strings['timer'].format(10), "callback":self._settings, "args": (game_id, ['Timer', 10])},
|
||||
{"text": self.strings['timer'].format(15), "callback":self._settings, "args": (game_id, ['Timer', 15])},
|
||||
{"text": self.strings['timer'].format(30), "callback":self._settings, "args": (game_id, ['Timer', 30])},
|
||||
{"text": self.strings['timer'].format(60), "callback":self._settings, "args": (game_id, ['Timer', 60])}
|
||||
],
|
||||
[
|
||||
{"text": self.strings['no_clock_text'], "callback":self._settings, "args": (game_id, ['Timer', True])}
|
||||
]
|
||||
])
|
||||
elif ruleset == "c":
|
||||
text = "♟️"
|
||||
reply_markup.extend([
|
||||
[
|
||||
{"text": self.strings['white'], "callback":self._settings, "args": (game_id, ['host_plays', True])},
|
||||
{"text": self.strings['black'], "callback":self._settings, "args": (game_id, ['host_plays', True] )}
|
||||
],
|
||||
[
|
||||
{"text": self.strings['random'], "callback":self._settings, "args": (game_id, ['host_plays', 'r'])}
|
||||
]
|
||||
])
|
||||
elif ruleset == "s":
|
||||
text = "✏️"
|
||||
reply_markup.extend([
|
||||
[{"text": st["symbol"] + self.strings[name], "callback":self._settings, "args": (game_id, ["style", name])}]
|
||||
for name, st in self.styles.items()
|
||||
])
|
||||
|
||||
reply_markup.append(
|
||||
[
|
||||
{"text": self.strings['back'], "callback": self.settings, "args": (game_id,)}
|
||||
]
|
||||
)
|
||||
|
||||
await utils.answer(call, text, reply_markup=reply_markup, disable_security=True)
|
||||
else:
|
||||
await call.answer("✅")
|
||||
if ruleset[0] == "style":
|
||||
self.set('style', ruleset[1])
|
||||
if ruleset[0] == "Timer" and isinstance(ruleset[1], int):
|
||||
self.games[game_id]['Timer']['timer'] = Timer(ruleset[1]*60)
|
||||
else:
|
||||
self.games[game_id][ruleset[0]] = ruleset[1]
|
||||
await self.settings(call, game_id)
|
||||
|
||||
|
||||
@loader.command(ru_doc="[reply/username/id] - предложить человеку сыграть партию")
|
||||
async def chess(self, message: Message):
|
||||
"""[reply/username/id] - propose a person to play a game"""
|
||||
sender, opponent = await self.get_players(message)
|
||||
if not sender or not opponent: return
|
||||
if sender['id'] == opponent['id'] and not self.config["play_self"]:
|
||||
await utils.answer(message, self.strings["playing_with_yourself?"])
|
||||
return
|
||||
if self.games:
|
||||
past_game = next(reversed(self.games.values()))
|
||||
if not past_game.get("game", None):
|
||||
self.games.pop(past_game['game_id'], None)
|
||||
if not self.games:
|
||||
game_id = str(1)
|
||||
else:
|
||||
game_id = str(max(map(int, self.games.keys())) + 1)
|
||||
self.games[game_id] = GameObj(
|
||||
game_id = game_id,
|
||||
sender = sender,
|
||||
opponent = opponent,
|
||||
Timer = {"available": True if isinstance(message.peer_id, PeerUser) else False, "timer": None, "timer_loop": False},
|
||||
time = int(time.time()),
|
||||
host_plays = "r",
|
||||
style = self.gsettings['style']
|
||||
)
|
||||
await self._invite(message, game_id)
|
||||
|
||||
# @loader.command(ru_doc="посмотреть текущее состояние модуля и статистику своих партий")
|
||||
# async def chesstats(self, message: Message):
|
||||
# """view the current state of the module and statistics of your games"""
|
||||
# total_games = len(self.get("games_backup", {}))
|
||||
# await utils.answer(message, f"♟️ <b>{self.strings['name']}</b> ♟️\n\nTotal games played: <b>{total_games}</b>")
|
||||
# TODO: добавить кнопки для просмотра состояния каждой партии; считать победы/поражения/ничьи и прочую бесполезную статистику; проверка на наличие исполняемого файла шахматного движка для возможности игры против ИИ; возможность экспорта партии в PGN; возможность продолжить сохранённую партию
|
||||
|
||||
############## Preparing all for game start... ##############
|
||||
|
||||
async def _init_game(self, call: InlineCall, game_id: str, ans="yes"):
|
||||
if not await self._check_player(call, game_id=game_id, only_opponent=True): return
|
||||
if ans == "no":
|
||||
self.games.pop(game_id, None)
|
||||
await utils.answer(call, self.strings["declined"])
|
||||
return
|
||||
game = self.games[game_id]
|
||||
await utils.answer(call, self.strings["step1"])
|
||||
await asyncio.sleep(0.8)
|
||||
await utils.answer(call, self.strings["step2"])
|
||||
game["style"] = self.styles[game["style"]]
|
||||
await asyncio.sleep(0.8)
|
||||
await utils.answer(call, self.strings["step3"])
|
||||
if (turn := game.pop("host_plays")) == "r":
|
||||
turn = r.choice([True, False])
|
||||
game["sender"]["color"] = True if turn else False
|
||||
game["opponent"]["color"] = False if turn else True
|
||||
await asyncio.sleep(0.8)
|
||||
await utils.answer(call, self.strings["step4"])
|
||||
game["Timer"].pop("available", None)
|
||||
await asyncio.sleep(0.8)
|
||||
if isinstance(self.games[game_id]["Timer"]["timer"], Timer):
|
||||
await utils.answer(call, self.strings["step4.T"])
|
||||
await self._set_timer(call, game_id, call._units[call.unit_id]['chat'])
|
||||
await asyncio.sleep(0.8)
|
||||
return await utils.answer(call, self.strings["waiting_for_start"])
|
||||
await self._start_game(call, game_id)
|
||||
|
||||
async def _set_timer(self, board_call: InlineCall, game_id: str, chat_id):
|
||||
timer = self.games[game_id]["Timer"]["timer"]
|
||||
self.games[game_id]["Timer"]["message"] = (
|
||||
await self.inline.form(self.strings["timer_text"].format(
|
||||
int(await timer.white_time()),
|
||||
int(await timer.black_time()),
|
||||
""
|
||||
),
|
||||
chat_id,
|
||||
reply_markup = {"text": self.strings["start_timer"], "callback": self._start_timer, "args": (board_call, game_id,)},
|
||||
disable_security = True,
|
||||
)
|
||||
)
|
||||
|
||||
@loader.loop(interval=1, autostart=True)
|
||||
async def main_loop(self):
|
||||
for game_id in self.games:
|
||||
if not self.games[game_id].get("backup", False) and self.games[game_id]["Timer"]["timer_loop"] and not self.games[game_id]["Timer"]["timer_is_set"]:
|
||||
async def timer_loop(game_id):
|
||||
timer = self.games[game_id]["Timer"]["timer"]
|
||||
await timer.start()
|
||||
self.games[game_id]["Timer"]["timer_is_set"] = True
|
||||
while self.games[game_id]["Timer"]["timer_loop"]:
|
||||
if not all([await timer.white_time(), await timer.black_time()]):
|
||||
self.games[game_id]["Timer"]["timer_loop"] = False
|
||||
self.the_end(game_id, "time_is_up")
|
||||
elif self.games[game_id]["game"]["state"] == "the_end":
|
||||
self.games[game_id]["Timer"]["timer_loop"] = False
|
||||
|
||||
loser, winner = self._get_loser_and_winner(game_id)
|
||||
|
||||
await self.games[game_id]["Timer"]["message"].edit(self.strings["timer_text"].format(
|
||||
int(await timer.white_time()),
|
||||
int(await timer.black_time()),
|
||||
"" if self.games[game_id]["game"]["state"] != "the_end"
|
||||
else "⏹️ " + self.strings[self.games[game_id]["game"]["add_params"]["reason_of_ending"]].format(
|
||||
loser, winner
|
||||
)
|
||||
),
|
||||
)
|
||||
await asyncio.sleep(1)
|
||||
await timer.stop()
|
||||
asyncio.create_task(timer_loop(game_id))
|
||||
|
||||
if self.games[game_id].get("game", None):
|
||||
if not self.games[game_id].get("backup", False):
|
||||
self.games[game_id]["game"]["message"].inline_manager._units[
|
||||
self.games[game_id]["game"]["message"].unit_id
|
||||
]["always_allow"] = True # для ругающегося на эту строку гпт - по неизвестно какой причине фреймворк в какое-то время попросту
|
||||
# забывает про отключение его проверки. мне это нужно, чтобы сам модуль брал на себя ответсвенность
|
||||
# проверки, кто может управлять доской, а до кого очередь ещё не дошла
|
||||
games_backup = {}
|
||||
games = self.games
|
||||
for game_id, game in games.items():
|
||||
if game.get("game", None):
|
||||
game_copy = game
|
||||
if not game.get("backup", None):
|
||||
game_copy = {}
|
||||
game_copy["backup"] = True
|
||||
|
||||
game_copy["game"] = {
|
||||
k: v for k, v in game["game"].items()
|
||||
if k not in ("message", "root_node", "curr_node", "board")
|
||||
}
|
||||
game_copy["game"]["node"] = str(game["game"]["root_node"])
|
||||
|
||||
if game.get("Timer", None) and game["Timer"].get("timer", None):
|
||||
game_copy["Timer"] = game["Timer"]["timer"].backup()
|
||||
|
||||
for key, value in game.items():
|
||||
if key not in ("game", "Timer"):
|
||||
game_copy[key] = value
|
||||
|
||||
games_backup[game_id] = game_copy
|
||||
|
||||
self.set("games_backup", games_backup)
|
||||
|
||||
############## Starting game... ##############
|
||||
|
||||
async def _start_timer(self, call: InlineCall, board_call: InlineCall, game_id: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
timer = self.games[game_id]["Timer"]
|
||||
timer["timer_loop"] = True
|
||||
await self._start_game(board_call, game_id)
|
||||
|
||||
async def _start_game(self, call: InlineCall, game_id: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]
|
||||
node = chess.pgn.Game()
|
||||
pgn = copy.deepcopy(self.pgn)
|
||||
pgn["Date"] = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
||||
pgn["Round"] = str(game_id)
|
||||
pgn["White"] = game["sender"]["name"] if game["sender"]["color"] else game["opponent"]["name"]
|
||||
pgn["Black"] = game["opponent"]["name"] if game["sender"]["color"] else game["sender"]["name"]
|
||||
pgn["Result"] = "*"
|
||||
node.headers.update(pgn)
|
||||
game["game"] = {
|
||||
"board": chess.Board(),
|
||||
"message": call,
|
||||
"root_node": node,
|
||||
"curr_node": node,
|
||||
"state": "idle",
|
||||
"add_params": {
|
||||
"chosen_figure_coord": "",
|
||||
"reason_of_ending": "",
|
||||
"promotion_move": "",
|
||||
"winner_color": None,
|
||||
"resigner_color": None,
|
||||
"draw_offerer": None,
|
||||
}
|
||||
}
|
||||
await self.update_board(game_id)
|
||||
|
||||
def idle(self, game_id: str):
|
||||
game = self.games[game_id]["game"]
|
||||
game["state"] = "idle"
|
||||
game["add_params"]["chosen_figure_coord"] = ""
|
||||
game["add_params"]["promotion_move"] = ""
|
||||
game["add_params"]["draw_offerer"] = None
|
||||
|
||||
def choose(self, game_id: str, coord: str):
|
||||
game = self.games[game_id]["game"]
|
||||
game["state"] = "in_choose"
|
||||
game["add_params"]["chosen_figure_coord"] = coord
|
||||
game["add_params"]["promotion_move"] = ""
|
||||
|
||||
def promotion(self, game_id: str, move: str):
|
||||
game = self.games[game_id]["game"]
|
||||
game["state"] = "in_promotion"
|
||||
game["add_params"]["chosen_figure_coord"] = ""
|
||||
game["add_params"]["promotion_move"] = move
|
||||
|
||||
def the_end(self, game_id: str, reason: str, winner: bool = None):
|
||||
game = self.games[game_id]["game"]
|
||||
game["state"] = "the_end"
|
||||
game["add_params"]["reason_of_ending"] = reason
|
||||
game["add_params"]["winner_color"] = winner
|
||||
game["add_params"]["chosen_figure_coord"] = ""
|
||||
game["add_params"]["promotion_move"] = ""
|
||||
game["root_node"].headers["Result"] = (
|
||||
"1-0" if winner is True else
|
||||
"0-1" if winner is False else
|
||||
"1/2-1/2"
|
||||
)
|
||||
|
||||
def _get_loser_and_winner(self, game_id: str) -> tuple[str, str]:
|
||||
game = self.games[game_id]
|
||||
if game["sender"]["color"] == self.games[game_id]["game"]["add_params"]["winner_color"]:
|
||||
return (game["opponent"]["name"], game["sender"]["name"])
|
||||
else:
|
||||
return (game["sender"]["name"], game["opponent"]["name"])
|
||||
|
||||
def _get_piece_symbol(self, game_id: str, coord: str) -> str:
|
||||
game = self.games[game_id]
|
||||
piece = game["game"]["board"].piece_at(chess.parse_square(coord))
|
||||
return game["style"][piece.symbol()] if piece else " "
|
||||
|
||||
def _get_move_symbol(self, game_id: str, move: str) -> str:
|
||||
game = self.games[game_id]
|
||||
if len(move) == 5:
|
||||
return game["style"][
|
||||
"capture_promotion" if (move := chess.Move.from_uci(move))
|
||||
and game["game"]["board"].is_capture(move)
|
||||
else "promotion"
|
||||
]
|
||||
else:
|
||||
return game["style"][
|
||||
"capture" if (move := chess.Move.from_uci(move))
|
||||
and game["game"]["board"].is_capture(move)
|
||||
else "move"
|
||||
]
|
||||
|
||||
def _get_available_moves(self, game_id: str, coord: str) -> list[str]:
|
||||
if not coord: return []
|
||||
game = self.games[game_id]
|
||||
coord = chess.parse_square(coord)
|
||||
moves = [move.uci() for move in game["game"]["board"].legal_moves if move.from_square == coord]
|
||||
return moves
|
||||
|
||||
def _get_board_dict(self, game_id: str) -> dict[str, str]:
|
||||
game = self.games[game_id]
|
||||
coords = copy.deepcopy(self.coords)
|
||||
for coord in self.coords:
|
||||
coords[coord] = self._get_piece_symbol(game_id, coord)
|
||||
|
||||
if game["game"]["state"] == "in_choose":
|
||||
choosen_coord = game["game"]["add_params"]["chosen_figure_coord"]
|
||||
for move in self._get_available_moves(game_id, choosen_coord):
|
||||
coord = move[2:4]
|
||||
coords[coord] = self._get_move_symbol(game_id, move)
|
||||
|
||||
return coords
|
||||
|
||||
def _get_reply_markup(self, game_id: str, promotion: bool = False, resign_confirm: bool = False, draw_confirm: bool = False) -> list[list[dict]]:
|
||||
game = self.games[game_id]
|
||||
is_end = game["game"]["state"] == "the_end"
|
||||
reply_markup = utils.chunks(
|
||||
[
|
||||
{
|
||||
"text": figure,
|
||||
"callback": self.choose_coord,
|
||||
"args": (game_id, coord),
|
||||
}
|
||||
for coord, figure in self._get_board_dict(game_id).items()
|
||||
][::-1],
|
||||
8
|
||||
)
|
||||
|
||||
if promotion:
|
||||
reply_markup.append(
|
||||
[{"text": "⬇️↻⬇️", "action": "answer", "message": self.strings["choose_promotion"]}]
|
||||
)
|
||||
reply_markup.append(
|
||||
[
|
||||
{
|
||||
"text": game["style"].get(piece, piece),
|
||||
"callback": self.pawn_promotion,
|
||||
"args": (game_id, piece),
|
||||
} for piece in "qrnb"
|
||||
]
|
||||
)
|
||||
elif resign_confirm:
|
||||
reply_markup.extend(
|
||||
[
|
||||
[
|
||||
{
|
||||
"text": self.strings["resign_check"],
|
||||
"data": "_there_is_nothing",
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": self.strings["resign_yes"],
|
||||
"callback": self.resign,
|
||||
"args": (game_id, True),
|
||||
},
|
||||
{
|
||||
"text": self.strings["resign_no"],
|
||||
"callback": self._back_to_game,
|
||||
"args": (game_id,),
|
||||
},
|
||||
]
|
||||
]
|
||||
)
|
||||
elif draw_confirm:
|
||||
reply_markup.extend(
|
||||
[
|
||||
[
|
||||
{
|
||||
"text": self.strings["draw_offer"].format(
|
||||
self.strings["white"] if game["game"]["add_params"]["draw_offerer"]
|
||||
else self.strings["black"]
|
||||
),
|
||||
"data": "_there_is_nothing",
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": self.strings["draw_yes"],
|
||||
"callback": self.draw,
|
||||
"args": (game_id, True),
|
||||
},
|
||||
{
|
||||
"text": self.strings["resign_no"],
|
||||
"callback": self._back_to_game,
|
||||
"args": (game_id,),
|
||||
},
|
||||
]
|
||||
]
|
||||
)
|
||||
elif not is_end:
|
||||
resign = [
|
||||
{
|
||||
"text": "🏳️",
|
||||
"callback": self.resign,
|
||||
"args": (game_id,),
|
||||
},
|
||||
{
|
||||
"text": "🤝",
|
||||
"callback": self.draw,
|
||||
"args": (game_id,),
|
||||
}
|
||||
]
|
||||
reply_markup.append(resign)
|
||||
return reply_markup
|
||||
|
||||
async def update_board(self, game_id: str, promotion: bool = False, resign_confirm: bool = False, draw_confirm: bool = False):
|
||||
game = self.games[game_id]
|
||||
is_end = game["game"]["state"] == "the_end"
|
||||
reason_of_ending = game["game"]["add_params"]["reason_of_ending"]
|
||||
status = (
|
||||
self.strings["check"] if game["game"]["board"].is_check() and not is_end
|
||||
else self.strings[reason_of_ending] + "\n"
|
||||
)
|
||||
loser, winner = self._get_loser_and_winner(game_id)
|
||||
|
||||
reply_markup = self._get_reply_markup(game_id, promotion, resign_confirm, draw_confirm)
|
||||
|
||||
pgn = game["game"]["root_node"].accept(chess.pgn.StringExporter(columns=None, headers=False)).replace("*", "").rsplit(maxsplit=1)
|
||||
if pgn:
|
||||
pgn[-1] = f"<b>{pgn[-1]}</b>"
|
||||
else:
|
||||
pgn = ["<b>|</b>"]
|
||||
last_moves = " ".join(pgn)
|
||||
|
||||
await utils.answer(
|
||||
game["game"]["message"],
|
||||
self.strings["board"].format(
|
||||
game_id,
|
||||
utils.escape_html(game["sender"]["name"] if game["sender"]["color"] else game["opponent"]["name"]),
|
||||
utils.escape_html(game["opponent"]["name"] if game["sender"]["color"] else game["sender"]["name"]),
|
||||
self.strings["white"] if game["game"]["board"].turn else self.strings["black"],
|
||||
status.format(loser=loser, winner=winner),
|
||||
last_moves[-32:],
|
||||
),
|
||||
reply_markup=reply_markup,
|
||||
)
|
||||
|
||||
def make_move(self, game_id: str, move: str):
|
||||
game = self.games[game_id]["game"]
|
||||
move = chess.Move.from_uci(move)
|
||||
game["board"].push(move)
|
||||
game["curr_node"] = game["curr_node"].add_variation(move)
|
||||
|
||||
async def pawn_promotion(self, call: InlineCall, game_id: str, piece: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]["game"]
|
||||
move = game["add_params"]["promotion_move"] + piece
|
||||
|
||||
self.make_move(game_id, move)
|
||||
self.set_game_state(game_id)
|
||||
|
||||
return await self.update_board(game_id)
|
||||
|
||||
async def _back_to_game(self, _, game_id: str):
|
||||
self.set_game_state(game_id)
|
||||
await self.update_board(game_id)
|
||||
|
||||
async def resign(self, call: InlineCall, game_id: str, confirm: bool = False):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]
|
||||
if not confirm:
|
||||
game["game"]["add_params"]["resign_offerer"] = self._get_color_by_player(
|
||||
game_id,
|
||||
call.from_user.id
|
||||
)
|
||||
return await self.update_board(game_id, resign_confirm=True)
|
||||
self.the_end(game_id, "resign", winner=not game["game"]["board"].turn)
|
||||
await self.update_board(game_id)
|
||||
|
||||
async def draw(self, call: InlineCall, game_id: str, accept: bool = False):
|
||||
if not await self._check_player(call, game_id, skip_turn_check=True): return
|
||||
game = self.games[game_id]
|
||||
if accept:
|
||||
offerer_id = self._get_player_by_color(
|
||||
game_id,
|
||||
game["game"]["add_params"]["draw_offerer"]
|
||||
)["id"]
|
||||
|
||||
if call.from_user.id == offerer_id:
|
||||
await call.answer(self.strings["draw_not_you"])
|
||||
return
|
||||
self.the_end(game_id, "draw")
|
||||
return await self.update_board(game_id)
|
||||
|
||||
game["game"]["add_params"]["draw_offerer"] = self._get_color_by_player(
|
||||
game_id,
|
||||
call.from_user.id
|
||||
)
|
||||
return await self.update_board(game_id, draw_confirm=True)
|
||||
|
||||
def set_game_state(self, game_id: str):
|
||||
game = self.games[game_id]["game"]
|
||||
board = game["board"]
|
||||
self.idle(game_id)
|
||||
if board.is_checkmate():
|
||||
self.the_end(game_id, "checkmate", winner=not board.turn)
|
||||
elif board.is_stalemate():
|
||||
self.the_end(game_id, "stalemate")
|
||||
elif board.is_insufficient_material():
|
||||
self.the_end(game_id, "insufficient_material")
|
||||
elif board.is_seventyfive_moves():
|
||||
self.the_end(game_id, "seventyfive_moves")
|
||||
elif board.is_fivefold_repetition():
|
||||
self.the_end(game_id, "fivefold_repetition")
|
||||
|
||||
async def choose_coord(self, call: BotInlineCall, game_id: str, coord: str):
|
||||
if not await self._check_player(call, game_id): return
|
||||
game = self.games[game_id]["game"]
|
||||
state = game["state"]
|
||||
|
||||
if state == "idle":
|
||||
if self._get_available_moves(game_id, coord):
|
||||
self.choose(game_id, coord)
|
||||
else:
|
||||
await call.answer(self.strings["no_moves"])
|
||||
return await self.update_board(game_id)
|
||||
|
||||
elif state == "in_choose":
|
||||
if coord == game["add_params"]["chosen_figure_coord"]:
|
||||
self.idle(game_id)
|
||||
return await self.update_board(game_id)
|
||||
|
||||
av_moves = self._get_available_moves(game_id, game["add_params"]["chosen_figure_coord"])
|
||||
coord_matches = [move for move in av_moves if coord in move]
|
||||
|
||||
if len(coord_matches) == 1:
|
||||
self.make_move(game_id, coord_matches[0])
|
||||
self.set_game_state(game_id)
|
||||
return await self.update_board(game_id)
|
||||
|
||||
elif len(coord_matches) > 1:
|
||||
move = coord_matches[0][:4]
|
||||
self.promotion(game_id, move)
|
||||
return await self.update_board(game_id, promotion=True)
|
||||
|
||||
elif game["board"].piece_at(chess.parse_square(coord)):
|
||||
self.choose(game_id, coord)
|
||||
return await self.update_board(game_id)
|
||||
|
||||
else:
|
||||
self.idle(game_id)
|
||||
return await self.update_board(game_id)
|
||||
|
||||
elif state == "in_promotion":
|
||||
return await call.answer(self.strings["can_not_move"])
|
||||
|
||||
elif state == "the_end":
|
||||
return await call.answer(self.strings["game_ended"])
|
||||
|
||||
else:
|
||||
await call.answer("ты игру сломал?")
|
||||
self.idle(game_id)
|
||||
return await self.update_board(game_id)
|
||||
|
||||
|
||||
|
||||
def _get_player_by_color(self, game_id: str, color: bool):
|
||||
game = self.games[game_id]
|
||||
return game["sender"] if game["sender"]["color"] == color else game["opponent"]
|
||||
|
||||
def _get_color_by_player(self, game_id: str, player_id: int):
|
||||
game = self.games[game_id]
|
||||
if game["sender"]["id"] == player_id:
|
||||
return game["sender"]["color"]
|
||||
elif game["opponent"]["id"] == player_id:
|
||||
return game["opponent"]["color"]
|
||||
return None
|
||||
435
ZetGoHack/nullmod/HaremManager.py
Normal file
435
ZetGoHack/nullmod/HaremManager.py
Normal file
@@ -0,0 +1,435 @@
|
||||
__version__ = (1,1,7)
|
||||
#░░░███░███░███░███░███
|
||||
#░░░░░█░█░░░░█░░█░░░█░█
|
||||
#░░░░█░░███░░█░░█░█░█░█
|
||||
#░░░█░░░█░░░░█░░█░█░█░█
|
||||
#░░░███░███░░█░░███░███
|
||||
# H:Mods Team [💎]
|
||||
# meta developer: @nullmod
|
||||
|
||||
# - main - #
|
||||
from .. import loader, utils
|
||||
# - func - #
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
import re
|
||||
# - func(tl) - #
|
||||
from telethon.tl.functions.chatlists import CheckChatlistInviteRequest, JoinChatlistInviteRequest, LeaveChatlistRequest
|
||||
from telethon.tl.functions.messages import ImportChatInviteRequest, CheckChatInviteRequest
|
||||
from telethon.tl.functions.channels import JoinChannelRequest, LeaveChannelRequest
|
||||
from telethon.tl.functions.contacts import BlockRequest, UnblockRequest
|
||||
# - types - #
|
||||
from telethon.tl.types import InputChatlistDialogFilter, UpdateDialogFilter
|
||||
# - errors - #
|
||||
from telethon.errors import YouBlockedUserError, InviteRequestSentError
|
||||
# - end - #
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@loader.tds
|
||||
class HaremManager(loader.Module):
|
||||
"""Module for harem bots: Gif Harem, Waifu Harem, Horny Harem"""
|
||||
|
||||
strings = {
|
||||
"name": "HaremManager"
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"ignore-chats",
|
||||
[],
|
||||
"Список чатов, где модуль НЕ будет ловить вайфу. Указывайте ID чатов в виде 123456789",
|
||||
validator=loader.validators.Series(
|
||||
validator=loader.validators.Integer(),
|
||||
)
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"whitelist-chats",
|
||||
[],
|
||||
"Список чатов, где модуль будет ловить вайфу. Указывайте ID чатов в виде 123456789. Если что-то указано, то модуль будет ловить вайфу только в этих чатах",
|
||||
validator=loader.validators.Series(
|
||||
validator=loader.validators.Integer(),
|
||||
)
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"interval-horny",
|
||||
4,
|
||||
"Интервал между автобонусом",
|
||||
validator=loader.validators.Float(2.0)
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"interval-waifu",
|
||||
4,
|
||||
"Интервал между автобонусом",
|
||||
validator=loader.validators.Float(2.0)
|
||||
),
|
||||
loader.ConfigValue(
|
||||
"interval-gif",
|
||||
4,
|
||||
"Интервал между автобонусом",
|
||||
validator=loader.validators.Float(2.0)
|
||||
),
|
||||
)
|
||||
|
||||
async def client_ready(self):
|
||||
self.harems = {
|
||||
"horny": "@Horny_GaremBot",
|
||||
"waifu": "@garem_chatbot",
|
||||
"gif": "@GIFgarem_bot",
|
||||
}
|
||||
self.harems_ids = {
|
||||
"horny": 7896566560,
|
||||
"waifu": 6704842953,
|
||||
"gif": 7084965046,
|
||||
}
|
||||
|
||||
temp_values = [
|
||||
"config",
|
||||
"ab-horny",
|
||||
"catch-horny",
|
||||
"out-horny",
|
||||
"ab-waifu",
|
||||
"catch-waifu",
|
||||
"out-waifu",
|
||||
"ab-gif",
|
||||
"catch-gif",
|
||||
"out-gif"
|
||||
]
|
||||
if not self.get("config", None):
|
||||
for value in temp_values:
|
||||
self.set(value, False if value not in "config" else True)
|
||||
|
||||
@loader.loop(interval=1, autostart=True)
|
||||
async def loop(self):
|
||||
for bot in self.harems:
|
||||
if self.get(f"ab-{bot}", None):
|
||||
if (not self.get(f"ab-t-{bot}") or (time.time() - self.get(f"ab-t-{bot}")) >= int(3600*self.config[f"interval-{bot}"])):
|
||||
await self._autobonus(self.harems[bot], bot)
|
||||
|
||||
@loader.watcher("only_messages")
|
||||
async def watcher(self, message):
|
||||
"""Watcher"""
|
||||
chatid = int(str(message.chat_id).replace("-100", ""))
|
||||
for bot in self.harems:
|
||||
if bot == "waifu": continue
|
||||
if message.sender_id == self.harems_ids[bot] and self.get(f"catch-{bot}", None):
|
||||
if self.config["whitelist-chats"]:
|
||||
if chatid not in self.config["whitelist-chats"]:
|
||||
return
|
||||
elif chatid in self.config["ignore-chats"]:
|
||||
return
|
||||
if (not self.get(f"catcher_time-{bot}") or int(time.time()) - int(self.get(f"catcher_time-{bot}")) > 14400):
|
||||
if "заблудилась" in message.text.lower():
|
||||
try:
|
||||
await message.click()
|
||||
await asyncio.sleep(5)
|
||||
msgs = await message.client.get_messages(chatid, limit=10)
|
||||
for msg in msgs:
|
||||
if msg.mentioned and "забрали" in msg.text and msg.sender_id == self.harems_ids[bot]:
|
||||
if self.get(f"out-{bot}", None):
|
||||
match = re.search(r", Вы забрали (.+?)\. Вайфу", msg.text)
|
||||
waifu = match.group(1)
|
||||
caption = f"{waifu} в вашем гареме! <emoji document_id=5395592707580127159>😎</emoji>"
|
||||
await self.client.send_file(self.harems[bot], caption=caption, file=message.media)
|
||||
self.set(f"catcher_time-{bot}", int(time.time()))
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при ловле вайфу для {bot}(не критично): {e}")
|
||||
|
||||
|
||||
def _main_markup(self):
|
||||
return [
|
||||
[
|
||||
{
|
||||
"text": "[✔️] Horny Harem" if self.get("ab-horny") else "[❌] Horny Harem",
|
||||
"callback": self.callback_handler,
|
||||
"args": ("horny",)
|
||||
},
|
||||
{
|
||||
"text": "[✔️] Waifu Harem" if self.get("ab-waifu") else "[❌] Waifu Harem",
|
||||
"callback": self.callback_handler,
|
||||
"args": ("waifu",)
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "[✔️] Gif Harem" if self.get("ab-gif") else "[❌] Gif Harem",
|
||||
"callback": self.callback_handler,
|
||||
"args": ("gif",)
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"text": "🔻 Закрыть",
|
||||
"action": "close",
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
def _menu_markup(self, bot):
|
||||
markup = [[],[]]
|
||||
markup[0].append({
|
||||
"text": "[✔️] Автобонус" if self.get(f"ab-{bot}", None) else "[❌] Автобонус",
|
||||
"callback": self.callback_handler,
|
||||
"args": (f"ab-{bot}",)
|
||||
})
|
||||
if "waifu" not in bot:
|
||||
markup[0].append({
|
||||
"text": "[✔️] Автоловля" if self.get(f"catch-{bot}", None) else "[❌] Автоловля",
|
||||
"callback": self.callback_handler,
|
||||
"args": (f"catch-{bot}",)
|
||||
})
|
||||
markup[1].append({
|
||||
"text": "[✔️] Вывод от ловца" if self.get(f"out-{bot}", None) else "[❌] Вывод от ловца",
|
||||
"callback": self.callback_handler,
|
||||
"args": (f"out-{bot}",)
|
||||
})
|
||||
markup.append([
|
||||
{
|
||||
"text":"🔁 Перезапустить автобонус",
|
||||
"callback": self.callback_handler,
|
||||
"args": (f"restart-{bot}",)
|
||||
},
|
||||
])
|
||||
markup.append([
|
||||
{
|
||||
"text":"↩️ Назад",
|
||||
"callback":self.callback_handler,
|
||||
"args": ("back",)
|
||||
}
|
||||
])
|
||||
return markup
|
||||
|
||||
async def _set_menu(self, message):
|
||||
await utils.answer(
|
||||
message,
|
||||
f"❤️ Выберите бота для управления\n\n✅ <i>- означает, что автобонус включён</i>\
|
||||
\n\nБольше настроек в конфиге модуля(<code>{self.get_prefix()}config HaremManager</code>)",
|
||||
reply_markup=self._main_markup()
|
||||
)
|
||||
|
||||
async def callback_handler(self, call, data):
|
||||
if data == "back":
|
||||
await self._set_menu(call)
|
||||
return
|
||||
elif data.startswith("restart-"):
|
||||
bot = data.split("-")[-1]
|
||||
await call.answer(f"Перезапуск бонуса для {self.harems[bot]}...")
|
||||
await self._autobonus(self.harems[bot], bot)
|
||||
return
|
||||
elif data.startswith("ab-"):
|
||||
bot = data.split("-")[-1]
|
||||
self.set(data, not self.get(data, None))
|
||||
await utils.answer(call, f"Меню <code>{self.harems[bot]}</code>", reply_markup=self._menu_markup(bot))
|
||||
elif data.startswith("catch-"):
|
||||
bot = data.split("-")[-1]
|
||||
self.set(data, not self.get(data, None))
|
||||
await utils.answer(call, f"Меню <code>{self.harems[bot]}</code>", reply_markup=self._menu_markup(bot))
|
||||
elif data.startswith("out-"):
|
||||
bot = data.split("-")[-1]
|
||||
self.set(data, not self.get(data, None))
|
||||
await utils.answer(call, f"Меню <code>{self.harems[bot]}</code>", reply_markup=self._menu_markup(bot))
|
||||
else:
|
||||
bot = data
|
||||
await utils.answer(call, f"Меню <code>{self.harems[bot]}</code>", reply_markup=self._menu_markup(bot))
|
||||
|
||||
async def _autobonus(self, id, bot):
|
||||
wait_boost = False
|
||||
async with self._client.conversation(id) as conv:
|
||||
try:
|
||||
await conv.send_message("/bonus")
|
||||
except YouBlockedUserError:
|
||||
await self.client(UnblockRequest(id))
|
||||
await conv.send_message("/bonus")
|
||||
r = None
|
||||
try:
|
||||
r = await conv.get_response(timeout=5*60)
|
||||
except:
|
||||
tryings = 5
|
||||
while tryings > 0:
|
||||
tryings -= 1
|
||||
try:
|
||||
await conv.send_message("/bonus")
|
||||
r = await conv.get_response(5*60)
|
||||
break
|
||||
except:
|
||||
pass
|
||||
if r is None:
|
||||
logger.warning("Ответ от бота не получен. Вероятно, он снова лёг\n\nПерезапустите автобонус, когда бот очнётся")
|
||||
self.set(f"ab-{bot}", False)
|
||||
return
|
||||
self.set(f"ab-t-{bot}", int(time.time()))
|
||||
if "Доступен бонус за подписки" in r.text:
|
||||
await conv.send_message("/start ad_bonus")
|
||||
r = await conv.get_response()
|
||||
if "проверка пройдена" not in r.text:
|
||||
to_leave, to_block, folders, chats_in_folders = [], [], [], []
|
||||
wait_boost = False
|
||||
if r.reply_markup:
|
||||
a = r.buttons
|
||||
for i in a:
|
||||
for button in i: # каждая кнопка...
|
||||
if button.url:
|
||||
alr = False # "уже зашёл"
|
||||
if "addlist/" in button.url: # добавление папок
|
||||
slug = button.url.split("addlist/")[-1]
|
||||
peers = await self.client(CheckChatlistInviteRequest(slug=slug))
|
||||
if peers:
|
||||
peers = peers.peers
|
||||
try:
|
||||
a = await self.client(JoinChatlistInviteRequest(slug=slug, peers=peers))
|
||||
chats_in_folders.append(peers) # для выхода
|
||||
for update in a.updates:
|
||||
if isinstance(update, UpdateDialogFilter):
|
||||
folders.append(InputChatlistDialogFilter(filter_id=update.id)) # для удаления папки
|
||||
except: pass
|
||||
continue
|
||||
if "t.me/boost" in button.url: # бустить не обязательно
|
||||
wait_boost = True
|
||||
continue
|
||||
if not bool(re.match(r"^https?:\/\/t\.me\/[^\/]+\/?$", button.url)): # дополнительные вложения отметаем
|
||||
continue
|
||||
if "t.me/+" in button.url: # приватные чаты
|
||||
try:
|
||||
a = await self.client(CheckChatInviteRequest(button.url.split("+")[-1]))
|
||||
if not hasattr(a, "request_needed") or not a.request_needed: # получить айди приватного чата/канала с приглашениями без входа невозможно
|
||||
pass
|
||||
else:
|
||||
url = button.url.split("?")[0] if "?" in button.url else button.url
|
||||
try:
|
||||
await self.client(ImportChatInviteRequest(button.url.split("+")[-1]))
|
||||
except InviteRequestSentError: pass
|
||||
await asyncio.sleep(3)
|
||||
try:
|
||||
entity = await self.client.get_entity(url)
|
||||
except ValueError:
|
||||
try:
|
||||
await asyncio.sleep(15)
|
||||
entity = await self.client.get_entity(url)
|
||||
except:
|
||||
continue
|
||||
except:
|
||||
pass
|
||||
alr = True
|
||||
except: continue
|
||||
url = button.url.split("?")[0] if "?" in button.url else button.url
|
||||
if not alr:
|
||||
try:
|
||||
entity = await self.client.get_entity(url)
|
||||
except:
|
||||
entity = (await self.client(ImportChatInviteRequest(button.url.split("+")[-1]))).chats[0] #gotten class Updates
|
||||
alr = True
|
||||
if hasattr(entity, "broadcast"):
|
||||
if not alr:
|
||||
await self.client(JoinChannelRequest(button.url))
|
||||
to_leave.append(entity.id)
|
||||
else:
|
||||
to_leave.append(entity.chat.id) if hasattr(entity,"chat") else to_leave.append(entity.id) if hasattr(entity,"id") else None
|
||||
elif hasattr(entity, "bot"):
|
||||
username = entity.username if entity.username is not None else entity.usernames[0].username
|
||||
try:
|
||||
await self.client(UnblockRequest(username))
|
||||
except: print("блин")
|
||||
await self.client.send_message(entity, "/start")
|
||||
to_block.append(username)
|
||||
flyer_messages = await self.client.get_messages(id, limit=1)
|
||||
if wait_boost:
|
||||
await asyncio.sleep(150)
|
||||
for m in flyer_messages:
|
||||
await asyncio.sleep(5)
|
||||
await m.click(-1)
|
||||
await asyncio.sleep(5)
|
||||
for folder, chats in zip(folders, chats_in_folders):
|
||||
await self.client(LeaveChatlistRequest(peers=chats, chatlist=folder))
|
||||
for bot in to_block:
|
||||
await self.client(BlockRequest(bot))
|
||||
await self.client.delete_dialog(bot)
|
||||
for channel in to_leave:
|
||||
try:
|
||||
await self.client(LeaveChannelRequest(channel))
|
||||
except Exception as e:
|
||||
pass
|
||||
count = 0
|
||||
if not self.get(f"last_lout-{bot}") or int(time.time()) - self.get(f"last_lout-{bot}") > 43200:
|
||||
while count <= 3: # на всякий случай 4 попытки. Бот может забагаться и не выдать завершающий ответ
|
||||
await conv.send_message("/lout")
|
||||
r = await conv.get_response()
|
||||
if r.reply_markup:
|
||||
pattern = self._parse(r)
|
||||
clicks = self._solution(pattern)
|
||||
for i in range(len(clicks)):
|
||||
if clicks[i] == 1:
|
||||
await r.click(i)
|
||||
self.set(f"last_lout-{bot}", int(time.time()))
|
||||
count += 1
|
||||
else:
|
||||
break
|
||||
|
||||
def _parse(self, r):
|
||||
a = r.buttons
|
||||
pattern = []
|
||||
for i in a:
|
||||
for m in i:
|
||||
t = m.text
|
||||
if t == "🌚":
|
||||
pattern.append(0)
|
||||
elif t == "🌞":
|
||||
pattern.append(1)
|
||||
else:
|
||||
pass
|
||||
return pattern
|
||||
|
||||
def _solution(self, pole):
|
||||
n = len(pole)
|
||||
for num in range(2**n):
|
||||
binary_string = bin(num)[2:].zfill(n)
|
||||
presses = [int(char) for char in binary_string]
|
||||
temp = pole[:]
|
||||
|
||||
for i in range(n):
|
||||
if presses[i]:
|
||||
temp[i] ^= 1
|
||||
if i % 3 > 0: temp[i - 1] ^= 1
|
||||
if i % 3 < 2: temp[i + 1] ^= 1
|
||||
if i >= 3: temp[i - 3] ^= 1
|
||||
if i < 6: temp[i + 3] ^= 1
|
||||
|
||||
if sum(temp) == 0:
|
||||
return presses
|
||||
|
||||
return None
|
||||
|
||||
@loader.command()
|
||||
async def Harems(self, message):
|
||||
"""Открыть меню управления"""
|
||||
await self._set_menu(message)
|
||||
|
||||
@loader.command()
|
||||
async def lightsout(self, message):
|
||||
"""[ответ на соо с полем] Автоматически решает Lights Out"""
|
||||
if message.is_reply:
|
||||
r = await message.get_reply_message()
|
||||
if r.reply_markup:
|
||||
pattern = self._parse(r)
|
||||
else:
|
||||
await utils.answer(message, "<emoji document_id=5299030091735525430>❗️</emoji> Не вижу поля игры. Это точно то сообщение?")
|
||||
return
|
||||
|
||||
else:
|
||||
|
||||
await utils.answer(message, "<emoji document_id=5299030091735525430>❗️</emoji> Пропиши команду в ответ на игру.")
|
||||
return
|
||||
if pattern:
|
||||
await utils.answer(message, "<emoji document_id=5472146462362048818>💡</emoji>")
|
||||
clicks = self._solution(pattern)
|
||||
if not clicks:
|
||||
await utils.answer(message, "Иди код трейси гений.")
|
||||
return #*смачный пинок кодеру под зад.*
|
||||
for i in range(len(clicks)):
|
||||
if clicks[i] == 1:
|
||||
await r.click(i)
|
||||
await utils.answer(message, "<emoji document_id=5395592707580127159>😎</emoji> Готово.")
|
||||
else:
|
||||
await utils.answer(message, "<emoji document_id=5299030091735525430>❗️</emoji> Ты ответил не на поле игры.")
|
||||
return
|
||||
82
ZetGoHack/nullmod/SchedulePlus.py
Normal file
82
ZetGoHack/nullmod/SchedulePlus.py
Normal file
@@ -0,0 +1,82 @@
|
||||
__version__ = (1,1,1)
|
||||
#░░░███░███░███░███░███
|
||||
#░░░░░█░█░░░░█░░█░░░█░█
|
||||
#░░░░█░░███░░█░░█░█░█░█
|
||||
#░░░█░░░█░░░░█░░█░█░█░█
|
||||
#░░░███░███░░█░░███░███
|
||||
|
||||
# Team: 'H:Mods'
|
||||
# meta developer: @nullmod
|
||||
|
||||
|
||||
from .. import loader, utils
|
||||
import re
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@loader.tds
|
||||
class SchedulePlus(loader.Module):
|
||||
"""Планирование периодичных сообщений"""
|
||||
strings = {"name": "SchedulePlus",
|
||||
"no_args": "<emoji document_id=5019523782004441717>❌</emoji> Invalid arguments",
|
||||
"too_many": "<emoji document_id=5019523782004441717>❌</emoji> Maximum number of scheduled messages is 100.",
|
||||
"scheduled": "<emoji document_id=5062291541624619917>✈️</emoji> Messages will be scheduled"}
|
||||
|
||||
strings_ru = {"name": "SchedulePlus",
|
||||
"no_args": "<emoji document_id=5019523782004441717>❌</emoji> Неверные аргументы",
|
||||
"too_many": "<emoji document_id=5019523782004441717>❌</emoji> Максимальное число отложенных сообщений - 100.",
|
||||
"scheduled": "<emoji document_id=5062291541624619917>✈️</emoji> Сообщения будут запланированы"}
|
||||
|
||||
@loader.command()
|
||||
async def sch(self, message):
|
||||
"""Используй .sch <периодичность в секундах> <количество отправок> <текст/содержимое из ответа>
|
||||
|
||||
Проф. режим: .sch 15 3 test{x=1;x*2}/{y=0;y+1}
|
||||
Запланирует три сообщения: test2/1, test4/2, test8/3"""
|
||||
args = utils.get_args_raw(message.text).split(' ', 2)
|
||||
resp = (await message.get_reply_message()) if len(args) < 3 and message.is_reply else message
|
||||
if not resp or not args[0].isdigit() or not args[1].isdigit():
|
||||
return await utils.answer(message, self.strings["no_args"])
|
||||
|
||||
interval, count, text = int(args[0]), int(args[1]), args[2] if len(args) > 2 else resp.text
|
||||
if count > 100:
|
||||
return await utils.answer(message, self.strings["too_many"])
|
||||
|
||||
chat_id = message.chat_id
|
||||
reply_message_id = resp.reply_to.reply_to_msg_id if resp.reply_to else None
|
||||
await utils.answer(message, self.strings["scheduled"])
|
||||
|
||||
variables = {}
|
||||
|
||||
for i in range(count):
|
||||
send_time = datetime.now(timezone.utc) + timedelta(seconds=interval * i)
|
||||
formatted_text = self.process_text(text, variables)
|
||||
await self.client.send_message(chat_id, formatted_text, file=resp.media, schedule=send_time, reply_to=reply_message_id)
|
||||
|
||||
def process_text(self, text, variables):
|
||||
"""Process text okay?"""
|
||||
def replace_match(match):
|
||||
return self.eval_expr(match.group(1), variables)
|
||||
|
||||
return re.sub(r"\{(.*?)\}", replace_match, text)
|
||||
|
||||
def eval_expr(self, expr, variables):
|
||||
"""eval()"""
|
||||
parts = expr.split(";")
|
||||
last_value = None
|
||||
var_name = None
|
||||
for part in parts:
|
||||
part = part.strip()
|
||||
if "=" in part and part.count("=") == 1:
|
||||
var, value = part.split("=")
|
||||
var = var.strip()
|
||||
if var not in variables:
|
||||
variables[var] = eval(value, {"__builtins__": {}}, variables)
|
||||
last_value = variables[var]
|
||||
var_name = var
|
||||
else:
|
||||
last_value = eval(part, {"__builtins__": {}}, variables)
|
||||
|
||||
if var_name is not None:
|
||||
variables[var_name] = last_value
|
||||
return str(last_value)
|
||||
3
ZetGoHack/nullmod/full.txt
Normal file
3
ZetGoHack/nullmod/full.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
Chess
|
||||
HaremManager
|
||||
SchedulePlus
|
||||
@@ -1,4 +1,4 @@
|
||||
__version__ = (2, 0, 0)
|
||||
__version__ = (2, 0, 1)
|
||||
# region KAMEKURO.
|
||||
# █▄▀ ▄▀█ █▀▄▀█ █▀▀ █▄▀ █ █ █▀█ █▀█
|
||||
# █ █ █▀█ █ ▀ █ ██▄ █ █ ▀▄▄▀ █▀▄ █▄█ ▄
|
||||
@@ -31,6 +31,7 @@ import random
|
||||
import requests
|
||||
import string
|
||||
import textwrap
|
||||
import typing
|
||||
from PIL import Image, ImageDraw, ImageEnhance, ImageFilter, ImageFont
|
||||
|
||||
import telethon
|
||||
@@ -67,17 +68,12 @@ class Banners:
|
||||
bbox = draw.textbbox((0, 0), text, font=font)
|
||||
return bbox[2] - bbox[0], bbox[3] - bbox[1]
|
||||
|
||||
|
||||
def new(self):
|
||||
W, H = 1920, 768
|
||||
title_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_b).content), 80
|
||||
)
|
||||
artist_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_b).content), 55
|
||||
)
|
||||
time_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_b).content), 36
|
||||
)
|
||||
title_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_b).content), 80)
|
||||
artist_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_b).content), 55)
|
||||
time_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_b).content), 36)
|
||||
|
||||
track_cov = Image.open(io.BytesIO(self.track_cover)).convert("RGBA")
|
||||
banner = (
|
||||
@@ -109,19 +105,19 @@ class Banners:
|
||||
lines = title_lines + artist_lines
|
||||
lines_sizes = [
|
||||
self.measure(
|
||||
line, artist_font if (i == len(lines) - 1) else title_font, draw
|
||||
line, artist_font if (i == len(lines)-1) else title_font, draw
|
||||
)
|
||||
for i, line in enumerate(lines)
|
||||
]
|
||||
heights = [h for _, h in lines_sizes]
|
||||
total_sizes = [sum(w for w, _ in lines_sizes), sum(h for _, h in lines_sizes)]
|
||||
spacing = title_font.size + 10
|
||||
y_start = space[1] + (space[3] - space[1]) / 2
|
||||
y_start = space[1] + ((space[3]-space[1]-total_sizes[1]) / 2)
|
||||
for i, line in enumerate(lines):
|
||||
w, _ = lines_sizes[i]
|
||||
draw.text(
|
||||
(space[0] + (space[2] - space[0] - w) / 2, y_start),
|
||||
(space[0] + (space[2]-space[0]-w) / 2, y_start),
|
||||
line,
|
||||
font=(artist_font if (i == (len(lines) - 1)) else title_font),
|
||||
font=(artist_font if (i == (len(lines)-1)) else title_font),
|
||||
fill="#FFFFFF",
|
||||
)
|
||||
y_start += spacing
|
||||
@@ -151,17 +147,12 @@ class Banners:
|
||||
by.name = "banner.png"
|
||||
return by
|
||||
|
||||
|
||||
def old(self):
|
||||
w, h = 1920, 768
|
||||
title_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_b).content), 80
|
||||
)
|
||||
art_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_r).content), 55
|
||||
)
|
||||
time_font = ImageFont.truetype(
|
||||
io.BytesIO(requests.get(self.onest_b).content), 36
|
||||
)
|
||||
title_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_b).content), 80)
|
||||
art_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_r).content), 55)
|
||||
time_font = ImageFont.truetype(io.BytesIO(requests.get(self.onest_b).content), 36)
|
||||
|
||||
track_cov = Image.open(io.BytesIO(self.track_cover)).convert("RGBA")
|
||||
banner = (
|
||||
@@ -387,9 +378,7 @@ class YaMusicMod(loader.Module):
|
||||
)
|
||||
await utils.answer(message, out + self.strings("downloading_track"))
|
||||
|
||||
info = await ym_client.tracks_download_info(search.tracks.results[0].id, True)
|
||||
audio = io.BytesIO(requests.get(info[0].direct_link).content)
|
||||
audio.name = "audio.mp3"
|
||||
audio = await self.__download_track(ym_client, search.tracks.results[0].id)
|
||||
await utils.answer(
|
||||
message=message,
|
||||
response=out,
|
||||
@@ -538,12 +527,10 @@ class YaMusicMod(loader.Module):
|
||||
except:
|
||||
pass
|
||||
|
||||
audio = io.BytesIO(requests.get(now["track"]["download_link"]).content)
|
||||
audio.name = "audio.mp3"
|
||||
await utils.answer(
|
||||
message=message,
|
||||
response=out,
|
||||
file=audio,
|
||||
file=now["track"]["bytes_io"],
|
||||
attributes=(
|
||||
[
|
||||
telethon.types.DocumentAttributeAudio(
|
||||
@@ -653,6 +640,29 @@ class YaMusicMod(loader.Module):
|
||||
)
|
||||
|
||||
|
||||
async def __download_track(
|
||||
self,
|
||||
client: yandex_music.ClientAsync,
|
||||
track_id: typing.Union[int, str],
|
||||
link_only: bool = False,
|
||||
):
|
||||
last_exception = None
|
||||
for attempt in range(5):
|
||||
try:
|
||||
info = await client.tracks_download_info(
|
||||
track_id, get_direct_links=True
|
||||
)
|
||||
if link_only:
|
||||
return info[0].direct_link
|
||||
by = io.BytesIO(await info[0].download_bytes_async())
|
||||
by.name = "audio.mp3"
|
||||
return by
|
||||
except Exception as e:
|
||||
if attempt != 4:
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
raise e
|
||||
|
||||
# Original code: https://raw.githubusercontent.com/MIPOHBOPOHIH/YMMBFA/main/main.py
|
||||
async def __get_ynison(self):
|
||||
async def create_ws(token, ws_proto):
|
||||
@@ -778,12 +788,8 @@ class YaMusicMod(loader.Module):
|
||||
"duration": track_object.duration_ms // 1000,
|
||||
"minutes": round(track_object.duration_ms / 1000) // 60,
|
||||
"seconds": round(track_object.duration_ms / 1000) % 60,
|
||||
"download_link": (
|
||||
(
|
||||
await ym_client.tracks_download_info(
|
||||
track_object.track_id, get_direct_links=True
|
||||
)
|
||||
)[0].direct_link
|
||||
"bytes_io": (
|
||||
await self.__download_track(ym_client, track_object.track_id)
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user