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

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