mirror of
https://github.com/MuRuLOSE/limoka.git
synced 2026-06-16 14:34:17 +02:00
Added and updated repositories 2025-11-22 08:13:29
This commit is contained in:
1094
SenkoGuardian/SenModules/Gemini.py
Normal file
1094
SenkoGuardian/SenModules/Gemini.py
Normal file
File diff suppressed because it is too large
Load Diff
134
SenkoGuardian/SenModules/GiftFinder.py
Normal file
134
SenkoGuardian/SenModules/GiftFinder.py
Normal file
@@ -0,0 +1,134 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
import asyncio
|
||||
import random
|
||||
import re
|
||||
|
||||
from .. import loader, utils
|
||||
from herokutl.tl.functions.payments import GetSavedStarGiftsRequest
|
||||
from herokutl.tl.functions.channels import GetFullChannelRequest
|
||||
from herokutl.tl.types import Message, StarGiftUnique, Channel
|
||||
from herokutl.errors.rpcerrorlist import DocumentInvalidError, FloodWaitError, ChatAdminRequiredError
|
||||
from telethon.utils import get_display_name
|
||||
|
||||
@loader.tds
|
||||
class GiftFinderMod(loader.Module):
|
||||
"""Парсер пользователей с NFT-подарками в чате."""
|
||||
strings = {
|
||||
"name": "GiftFinder",
|
||||
"not_a_chat": "🚫 <b>Не удалось найти указанный чат.</b>",
|
||||
"scanning": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Сканирую участников...</b>",
|
||||
"scanning_supplement": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Список участников неполон. Дополнительно сканирую сообщения...</b>",
|
||||
"scanning_messages_only": "<emoji document_id=5464429933543628237>⏳</emoji> <b>Участники скрыты. Сканирую только сообщения...</b>",
|
||||
"header": "<emoji document_id=5237868881267153432>🔖</emoji> Те у кого есть НФТ подарки:",
|
||||
"premium_star": "<emoji document_id=5274026806477857971>⭐️</emoji>",
|
||||
"flood_wait": "\n<emoji document_id=5212102117953384237>😖</emoji> Поймал FloodWait на {} секунд. Увеличиваю задержку и жду...",
|
||||
"scanning_safe": "⏳ <b>Сканирую участников...</b>",
|
||||
"scanning_supplement_safe": "⏳ <b>Список участников неполон. Дополнительно сканирую сообщения...</b>",
|
||||
"scanning_messages_only_safe": "⏳ <b>Участники скрыты. Сканирую только сообщения...</b>",
|
||||
"flood_wait_safe": "\n😖 Поймал FloodWait на {} секунд. Увеличиваю задержку и жду...",
|
||||
"no_users_found": "🚫 <b>В этом чате не найдено пользователей с NFT-подарками.</b>",
|
||||
}
|
||||
|
||||
async def _safe_edit(self, msg: Message, text_premium: str, text_safe: str):
|
||||
try:
|
||||
await msg.edit(text_premium)
|
||||
except DocumentInvalidError:
|
||||
await msg.edit(text_safe)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def giftscancmd(self, message: Message):
|
||||
"""
|
||||
Ищет пользователей с NFT-подарками в чате.
|
||||
Использование: .giftscan [лимит] или .giftscan [ID чата] [лимит]
|
||||
"""
|
||||
args = utils.get_args_raw(message)
|
||||
chat_arg = None
|
||||
msgs_limit = 3000
|
||||
if args:
|
||||
parts = args.split()
|
||||
first_arg = parts[0]
|
||||
if first_arg.lstrip('-').isdigit():
|
||||
chat_arg = int(first_arg)
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
msgs_limit = int(parts[1])
|
||||
else:
|
||||
chat_arg = first_arg
|
||||
if len(parts) > 1 and parts[1].isdigit():
|
||||
msgs_limit = int(parts[1])
|
||||
if not chat_arg and args and args.isdigit():
|
||||
msgs_limit = int(args)
|
||||
try:
|
||||
msg = await utils.answer(message, self.strings("scanning"))
|
||||
except DocumentInvalidError:
|
||||
msg = await utils.answer(message, self.strings("scanning_safe"))
|
||||
try:
|
||||
chat = await self.client.get_entity(chat_arg) if chat_arg is not None else await message.get_chat()
|
||||
except Exception:
|
||||
await self._safe_edit(msg, self.strings("not_a_chat"), self.strings("not_a_chat"))
|
||||
return
|
||||
user_ids = set()
|
||||
scan_messages_mode = False
|
||||
try:
|
||||
if isinstance(chat, Channel):
|
||||
full_chat = await self.client(GetFullChannelRequest(channel=chat))
|
||||
total_participants = full_chat.full_chat.participants_count
|
||||
else:
|
||||
total_participants = chat.participants_count
|
||||
participants = await self.client.get_participants(chat, limit=None)
|
||||
user_ids.update(user.id for user in participants)
|
||||
if len(participants) < total_participants:
|
||||
scan_messages_mode = True
|
||||
await self._safe_edit(msg, self.strings("scanning_supplement"), self.strings("scanning_supplement_safe"))
|
||||
except (ChatAdminRequiredError, AttributeError, TypeError, ValueError):
|
||||
scan_messages_mode = True
|
||||
await self._safe_edit(msg, self.strings("scanning_messages_only"), self.strings("scanning_messages_only_safe"))
|
||||
if scan_messages_mode:
|
||||
async for m in self.client.iter_messages(chat, limit=msgs_limit):
|
||||
if m.from_id and hasattr(m.from_id, 'user_id'):
|
||||
user_ids.add(m.from_id.user_id)
|
||||
found_users = []
|
||||
base_delay_min, base_delay_max, flood_penalty = 0.5, 1.5, 0.0
|
||||
for user_id in user_ids:
|
||||
try:
|
||||
user = await self.client.get_entity(user_id)
|
||||
if user.bot or user.is_self: continue
|
||||
except Exception: continue
|
||||
await asyncio.sleep(random.uniform(base_delay_min + flood_penalty, base_delay_max + flood_penalty))
|
||||
while True:
|
||||
try:
|
||||
all_gifts = await self.client(GetSavedStarGiftsRequest(peer=user, offset="", limit=100))
|
||||
if gifts := [g for g in all_gifts.gifts if isinstance(g.gift, StarGiftUnique)]:
|
||||
raw_name = get_display_name(user)
|
||||
s_name = re.sub(r'[\u2066-\u2069\u200e\u200f\u202a-\u202e\u3164\u115f\u2800]', '', raw_name).strip()
|
||||
link_text = f"@{user.username}" if not s_name and user.username else (f"User ID: {user.id}" if not s_name else utils.escape_html(s_name))
|
||||
link = f'<a href="https://t.me/{user.username}">{link_text}</a>' if user.username else f'<a href="tg://user?id={user.id}">{link_text}</a>'
|
||||
p_icon = self.strings('premium_star') if getattr(user, 'premium', False) else ""
|
||||
found_users.append(f"• {p_icon} {link} - {len(gifts)}")
|
||||
break
|
||||
except FloodWaitError as e:
|
||||
current_text = (await self.client.get_messages(msg.chat_id, ids=msg.id)).text
|
||||
premium_text = current_text + self.strings("flood_wait").format(e.seconds)
|
||||
safe_text = current_text + self.strings("flood_wait_safe").format(e.seconds)
|
||||
await self._safe_edit(msg, premium_text, safe_text)
|
||||
flood_penalty += 0.2
|
||||
await asyncio.sleep(e.seconds)
|
||||
continue
|
||||
except Exception: break
|
||||
if not found_users:
|
||||
await self._safe_edit(msg, self.strings("no_users_found"), self.strings("no_users_found"))
|
||||
return
|
||||
user_list = "\n".join(found_users)
|
||||
response_text = f"{self.strings('header')}\n<blockquote expandable>{user_list}</blockquote>"
|
||||
safe_header = "🔖 " + self.strings("header").split("</emoji>")[1]
|
||||
safe_list = [line.replace(self.strings("premium_star"), "⭐️") for line in found_users]
|
||||
safe_user_list = '\n'.join(safe_list)
|
||||
response_text_safe = f"{safe_header}\n<blockquote expandable>{safe_user_list}</blockquote>"
|
||||
await self._safe_edit(msg, response_text, response_text_safe)
|
||||
# горе кодер
|
||||
21
SenkoGuardian/SenModules/LICENSE.md
Normal file
21
SenkoGuardian/SenModules/LICENSE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Senko
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
705
SenkoGuardian/SenModules/MaillingChatGT99.py
Normal file
705
SenkoGuardian/SenModules/MaillingChatGT99.py
Normal file
@@ -0,0 +1,705 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
__version__ = (1, 3, 0)
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import io
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
from telethon import errors
|
||||
from telethon.tl import types as tl_types
|
||||
from telethon.utils import get_display_name, get_peer_id
|
||||
|
||||
from .. import loader, utils
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class SpecificWarningFilter(logging.Filter):
|
||||
def filter(self, record):
|
||||
if record.name == 'hikkatl.hikkatl.client.users' and \
|
||||
'PersistentTimestampOutdatedError' in record.getMessage() and \
|
||||
'GetChannelDifferenceRequest' in record.getMessage():
|
||||
return False
|
||||
return True
|
||||
|
||||
class ChatTarget:
|
||||
def __init__(self, raw_input: str, context_message: Optional[tl_types.Message] = None):
|
||||
self.raw = raw_input
|
||||
self.context = context_message
|
||||
self.entity_to_find: any = raw_input
|
||||
self.topic_id: Optional[int] = None
|
||||
self._parse()
|
||||
|
||||
def _parse(self):
|
||||
match = re.match(r"https://t\.me/(?:c/)?([\w\d_.-]+)/(\d+)", self.raw)
|
||||
if match:
|
||||
chat_identifier = match.group(1)
|
||||
if "/c/" in self.raw and chat_identifier.isdigit():
|
||||
self.entity_to_find = int(f"-100{chat_identifier}")
|
||||
else:
|
||||
self.entity_to_find = chat_identifier
|
||||
try:
|
||||
self.topic_id = int(match.group(2))
|
||||
except ValueError:
|
||||
pass
|
||||
elif self.context:
|
||||
self.entity_to_find = self.context.chat_id
|
||||
if getattr(self.context, 'is_topic_message', False):
|
||||
self.topic_id = getattr(self.context, 'reply_to_top_id', self.context.id)
|
||||
else:
|
||||
try:
|
||||
self.entity_to_find = int(self.raw)
|
||||
except ValueError:
|
||||
self.entity_to_find = self.raw
|
||||
|
||||
@loader.tds
|
||||
class MailChats(loader.Module):
|
||||
"""Модуль для массовой рассылки сообщений по чатам (Поддерживает все типы сообщений)"""
|
||||
strings = {
|
||||
"name": "MailChats",
|
||||
"add_chat": "➕ Добавить текущий чат/тему. Используйте .add_chat или .add_chat <ID/Username/Ссылка> (Можно сразу несколько ссылкок в 1 комманду).",
|
||||
"remove_chat": "🗑️ Удалить чат/тему по номеру из списка. Используйте .remove_chat <номер>.",
|
||||
"list_chats": "📜 Показать список чатов/тем для рассылки.",
|
||||
"add_msg": "➕ Добавить сообщение (ответом).",
|
||||
"remove_msg": "➖ Удалить сообщение по номеру. Используйте .remove_msg <номер>.",
|
||||
"clear_msgs": "🗑️ Очистить список сообщений.",
|
||||
"list_msgs": "📜 Показать список сообщений для рассылки.",
|
||||
"set_seller": "⚙️ Установить ID чата/пользователя продавца для уведомлений. Используйте .set_seller <ID/Username/Ссылка/'me'>.",
|
||||
"mail_status": "📊 Показать статус рассылки.",
|
||||
"start_mail": "🚀 Запустить рассылку. Использование: .start_mail <время_сек> <интервал_цикла_от-до_сек>.",
|
||||
"stop_mail": "⏹️ Остановить рассылку.",
|
||||
"error_getting_entity": "⚠️ Не удалось получить информацию о чате/сущности: {}",
|
||||
"error_sending_message": "⚠️ Ошибка при отправке сообщения ({}) в чат {} ({}): {}",
|
||||
"notification_sent": "✅ Уведомление отправлено.",
|
||||
"invalid_arguments": "⚠️ Неверные аргументы.",
|
||||
"chats_empty": "⚠️ Сначала добавьте чаты.",
|
||||
"messages_empty": "⚠️ Сначала добавьте сообщения.",
|
||||
"already_running": "⚠️ Рассылка уже запущена.",
|
||||
"started_mailing": "✅ Рассылка начата.\n⏳ Общее время: {} сек.\n⏱️ Интервал между циклами: {}-{} сек.\n⏱️ Интервал между чатами: ~{}-{} сек\n⏱️ Интервал между сообщениями в чате: ~{}-{} сек",
|
||||
"stopped_mailing": "✅ Рассылка остановлена.",
|
||||
"not_running": "⚠️ Рассылка не активна.",
|
||||
"chat_added": "✅ Чат/тема '{}' добавлен в список рассылки.",
|
||||
"chat_already_added": "⚠️ Чат/тема '{}' уже в списке.",
|
||||
"chat_removed": "✅ #{} '{}' удален из списка рассылки.",
|
||||
"invalid_chat_selection": "⛔️ Неверный номер чата.",
|
||||
"chats_cleared": "✅ Все чаты удалены из списка.",
|
||||
"messages_cleared": "✅ Список сообщений очищен.",
|
||||
"no_chats": "📃 Список чатов пуст.",
|
||||
"no_messages": "✍️ Ответьте на сообщение, чтобы добавить его в список. Список сообщений пуст.",
|
||||
"message_added": "✅ Сообщение добавлено (Snippet: {}).",
|
||||
"message_removed": "✅ Сообщение №{} удалено (Snippet: {}).",
|
||||
"invalid_message_number": "✍️ Укажите корректный номер сообщения.",
|
||||
"seller_set": "✅ Установлен чат продавца.",
|
||||
"duration_invalid": "✍️ Использование: .start_mail <время_сек> <интервал_цикла_от-до_сек>. Укажите целое число для времени и интервал между циклами (например: 45-70).",
|
||||
"seller_notification": "Автоматическое уведомление: рассылка завершена",
|
||||
"mailing_complete": "✅ Рассылка завершена!",
|
||||
"safe_mode_enabled": "🟢 <b>Безопасный режим ВКЛЮЧЁН</b>\n• Только группы/каналы\n• Макс {} чатов/цикл\n• Интервал между чатами: ~{}-{} сек\n• Интервал между циклами: ~{}-{} сек\n• Интервал между сообщениями в чате: ~{}-{} сек",
|
||||
"safe_mode_disabled": "🔴 <b>Безопасный режим ВЫКЛЮЧЕН</b>",
|
||||
"mail_not_running": "⚠️ Рассылка не активна.",
|
||||
"no_permission": "️️️️️️️️️️️️⚠️ Нет прав на отправку в чат {} ({}), пропускаем.",
|
||||
"processing_entity": "⏳ Обработка сущности...",
|
||||
"failed_to_send_message": "⚠️ Не удалось отправить сообщение {} в чат {}. Причина: {}",
|
||||
"failed_perm_check": "⚠️ Не удалось проверить права в чатe {} ({}) из-за ошибки: {}. Пропускаем.",
|
||||
"permission_denied_skip": "🚫 Пропуск чата {} (ID: {}, Topic: {}) из-за отсутствия прав на отправку. Причина: {}",
|
||||
"cfg_safe_mode": "Включить безопасный режим (Отправка только по группам/каналам, больше задержка)",
|
||||
"cfg_max_chats_safe": "Максимальное кол-во чатов за цикл в безопасном режиме",
|
||||
"cfg_chats_interval": "Интервал между чатами (сек, от-до). Пример: 2,5",
|
||||
"cfg_safe_chats_interval": "Интервал между чатами в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 10,20",
|
||||
"cfg_safe_cycle_interval": "Интервал между циклами в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 180,300",
|
||||
"cfg_safe_message_interval": "Интервал между сообщениями в 1 чат в БЕЗОПАСНОМ режиме (сек, от-до). Пример: 5,10",
|
||||
"cfg_message_interval": "Интервал между сообщениями в 1 чат (сек, от-до). Пример: 1,3",
|
||||
"cfg_delete_replies_delay": "⏱️ Задержка автоудаления для ответов команд (сек, 0 - не удалять)",
|
||||
"cfg_randomize_messages": "Рандомизировать сообщения (1 случайное сообщение на чат за цикл)",
|
||||
"add_chat_summary_title": "<b>Результаты добавления чатов:</b>\n\n",
|
||||
"add_chat_success_header": "<b>✅ Добавлено:</b>\n",
|
||||
"add_chat_already_exists_header": "<b>⚠️ Уже существуют:</b>\n",
|
||||
"add_chat_errors_header": "<b>❌ Ошибки:</b>\n",
|
||||
"no_valid_chats_provided": "⚠️ Не предоставлено валидных идентификаторов чатов или произошли ошибки при их обработке.",
|
||||
}
|
||||
PERMISSION_ERRORS = {
|
||||
"ChatForbiddenError", "UserBannedInChannelError", "ChatWriteForbiddenError",
|
||||
"ChatAdminRequiredError", "UserBlocked", "TopicClosedError",
|
||||
"TopicEditedError", "ForumTopicDeletedError",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
try:
|
||||
logger.setLevel(logging.WARNING)
|
||||
h_logger = logging.getLogger('hikkatl.hikkatl.client.users')
|
||||
if not any(isinstance(f, SpecificWarningFilter) for f in h_logger.filters):
|
||||
h_logger.addFilter(SpecificWarningFilter())
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to apply SpecificWarningFilter: {e}")
|
||||
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue("safe_mode", False, self.strings["cfg_safe_mode"], validator=loader.validators.Boolean()),
|
||||
loader.ConfigValue("max_chats_safe", 10, self.strings["cfg_max_chats_safe"], validator=loader.validators.Integer(minimum=1)),
|
||||
loader.ConfigValue("chats_interval", "2,5", self.strings["cfg_chats_interval"]),
|
||||
loader.ConfigValue("safe_chats_interval", "10,20", self.strings["cfg_safe_chats_interval"]),
|
||||
loader.ConfigValue("safe_cycle_interval", "180,300", self.strings["cfg_safe_cycle_interval"]),
|
||||
loader.ConfigValue("safe_message_interval", "5,10", self.strings["cfg_safe_message_interval"]),
|
||||
loader.ConfigValue("message_interval", "1,3", self.strings["cfg_message_interval"]),
|
||||
loader.ConfigValue("delete_replies_delay", 5, self.strings["cfg_delete_replies_delay"], validator=loader.validators.Integer(minimum=0)),
|
||||
loader.ConfigValue("randomize_messages", False, self.strings["cfg_randomize_messages"], validator=loader.validators.Boolean()),
|
||||
)
|
||||
self.chats: Dict[Tuple[int, Optional[int]], str] = {}
|
||||
self.messages: List[Dict] = []
|
||||
self.mail_task: Optional[asyncio.Task] = None
|
||||
self.seller_chat_id: Optional[int] = None
|
||||
self.total_messages_sent = 0
|
||||
self.start_time: Optional[datetime] = None
|
||||
self.end_time: Optional[datetime] = None
|
||||
self.is_running = False
|
||||
self.lock = asyncio.Lock()
|
||||
self._current_cycle_start_time: Optional[datetime] = None
|
||||
self._processed_chats_in_cycle = 0
|
||||
|
||||
async def client_ready(self, client, db):
|
||||
self.client = client
|
||||
self.db = db
|
||||
await self._load_data()
|
||||
|
||||
def _get_db_chats(self):
|
||||
return {str(k): v for k, v in self.chats.items()}
|
||||
|
||||
def _save_db_chats(self):
|
||||
self.db.set(self.strings["name"], "chats", self._get_db_chats())
|
||||
|
||||
async def _load_data(self):
|
||||
stored_chats = self.db.get(self.strings["name"], "chats", {})
|
||||
migrated_chats = {}
|
||||
needs_resave = False
|
||||
if isinstance(stored_chats, dict):
|
||||
for key, name in stored_chats.items():
|
||||
try:
|
||||
chat_tuple = eval(key)
|
||||
if isinstance(chat_tuple, tuple) and len(chat_tuple) == 2:
|
||||
migrated_chats[chat_tuple] = name
|
||||
else:
|
||||
migrated_chats[(int(key), None)] = name
|
||||
needs_resave = True
|
||||
except Exception:
|
||||
try:
|
||||
migrated_chats[(int(key), None)] = name
|
||||
needs_resave = True
|
||||
except Exception:
|
||||
logger.warning(f"Could not migrate chat key '{key}'")
|
||||
elif isinstance(stored_chats, list):
|
||||
for chat_id in stored_chats:
|
||||
migrated_chats[(int(chat_id), None)] = f"Chat {chat_id}"
|
||||
needs_resave = True
|
||||
self.chats = migrated_chats
|
||||
if needs_resave:
|
||||
self._save_db_chats()
|
||||
self.messages = self.db.get(self.strings["name"], "messages", [])
|
||||
self.seller_chat_id = self.db.get(self.strings["name"], "seller_chat_id")
|
||||
|
||||
async def _edit_or_reply_and_handle_deletion(self, message_event, text: str, delay: Optional[int] = None):
|
||||
if delay is None:
|
||||
delay = self.config["delete_replies_delay"]
|
||||
processed_message = None
|
||||
can_edit = message_event and hasattr(message_event, "edit") and callable(message_event.edit)
|
||||
try:
|
||||
if can_edit:
|
||||
try:
|
||||
if getattr(message_event, "deleted", False):
|
||||
can_edit = False
|
||||
else:
|
||||
processed_message = await message_event.edit(text, parse_mode='html')
|
||||
except errors.MessageNotModifiedError:
|
||||
processed_message = message_event
|
||||
except errors.MessageIdInvalidError:
|
||||
can_edit = False
|
||||
except errors.RPCError as e:
|
||||
can_edit = False
|
||||
logger.warning(f"RPC ошибка при попытке ({type(e).__name__}) редактировать {getattr(message_event, 'id', 'N/A')}: {e}. Попытка отправить новое.")
|
||||
if not processed_message or not can_edit:
|
||||
chat_to_reply = None
|
||||
if message_event and hasattr(message_event, "chat_id") and message_event.chat_id is not None: chat_to_reply = message_event.chat_id
|
||||
elif message_event and hasattr(message_event, "chat") and message_event.chat is not None: chat_to_reply = utils.get_peer_id(message_event.chat)
|
||||
if chat_to_reply:
|
||||
processed_message = await self.client.send_message(chat_to_reply, text, parse_mode='html')
|
||||
else:
|
||||
return None
|
||||
except Exception as e_edit_reply_outer:
|
||||
logger.error(f"Критическая ошибка на этапе редактирования/отправки сообщения: {e_edit_reply_outer}")
|
||||
return None
|
||||
if not processed_message:
|
||||
return None
|
||||
if delay > 0:
|
||||
self.client.loop.create_task(self._delete_message_after_delay(processed_message, delay))
|
||||
return processed_message
|
||||
|
||||
async def _delete_message_after_delay(self, message, delay):
|
||||
await asyncio.sleep(delay)
|
||||
try:
|
||||
if hasattr(message, 'delete') and not getattr(message, 'deleted', False):
|
||||
await message.delete()
|
||||
except errors.MessageDeleteForbiddenError:
|
||||
logger.warning(f"Нет прав на удаление сообщения {message.id}.")
|
||||
except Exception as e_del:
|
||||
logger.warning(f"Произошла ошибка при удалении сообщения {message.id}: {e_del}")
|
||||
|
||||
async def _find_chat(self, target: ChatTarget) -> Optional[dict]:
|
||||
try:
|
||||
entity = await self.client.get_entity(target.entity_to_find)
|
||||
chat_id = get_peer_id(entity)
|
||||
topic_id = target.topic_id if getattr(entity, 'forum', False) else None
|
||||
display_name = utils.escape_html(get_display_name(entity))
|
||||
if topic_id:
|
||||
try:
|
||||
topic_msg = await self.client.get_messages(entity, ids=topic_id)
|
||||
if topic_msg and isinstance(getattr(topic_msg, "action", None), tl_types.MessageActionTopicCreate):
|
||||
display_name += f" | Тема: '{utils.escape_html(topic_msg.action.title)}'"
|
||||
else:
|
||||
display_name += f" | Тема ID: {topic_id}"
|
||||
except Exception:
|
||||
display_name += f" | Тема ID: {topic_id}"
|
||||
return {"key": (chat_id, topic_id), "name": display_name}
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось найти чат '{target.raw}': {e}")
|
||||
return None
|
||||
|
||||
@loader.command()
|
||||
async def mail_help(self, message):
|
||||
"""📋 Показать пошаговую инструкцию по настройке рассылки."""
|
||||
help_text = """
|
||||
<blockquote expandable>
|
||||
<b>📋 Инструкция по настройке рассылки:</b>
|
||||
|
||||
<b>Шаг 1: Добавьте чаты для рассылки</b>
|
||||
• <b>Вручную:</b> Перейдите в нужный чат и напишите <code>.add_chat</code>.
|
||||
• <b>По ссылке/ID:</b> <code>.add_chat @username https://t.me/channel/123</code>
|
||||
|
||||
<b>✨ Бэкап и восстановление списка:</b>
|
||||
• <code>.dump_chats</code> — <b>Бэкап.</b> Модуль выгрузит в файл только те чаты, что уже есть в списке рассылки.
|
||||
• <code>.load_chats</code> — <b>Загрузка.</b> Ответьте этой командой на полученный файл, чтобы добавить чаты в рассылку.
|
||||
|
||||
<b>Шаг 2: Добавьте сообщения</b>
|
||||
• Ответьте на любое сообщение (текст, фото, видео) командой <code>.add_msg</code>.
|
||||
• Можно добавить несколько сообщений для рассылки.
|
||||
|
||||
<b>Шаг 3: Проверьте списки</b>
|
||||
• <code>.list_chats</code> — посмотреть список чатов. Если их больше 50, отправит файлом.
|
||||
• <code>.list_msgs</code> — посмотреть список сообщений.
|
||||
|
||||
<b>Шаг 4: Тонкая настройка (по желанию)</b>
|
||||
Откройте конфиг командой <code>.cfg MailChats</code>. Вот что значат основные параметры:
|
||||
|
||||
<b>-- Режимы работы --</b>
|
||||
• <code>safe_mode</code>: <b>Безопасный режим.</b> Если включить, рассылка будет идти медленнее и только в группы/каналы, чтобы снизить риск спам-блока.
|
||||
• <code>randomize_messages</code>: <b>Случайные сообщения.</b> Если включить, в каждый чат будет отправляться только ОДНО случайное сообщение из вашего списка. Если выключить — отправляются ВСЕ по порядку.
|
||||
|
||||
<b>-- Настройка пауз (формат: <code>min,max</code> секунд) --</b>
|
||||
• <code>chats_interval</code>: Пауза между отправкой в <b>разные чаты</b> (обычный режим). Пример: <code>2,5</code>.
|
||||
• <code>message_interval</code>: Пауза между отправкой <b>нескольких сообщений</b> в ОДИН чат (обычный режим).
|
||||
• <code>safe_chats_interval</code>: Пауза между чатами в <b>безопасном режиме</b> (больше для безопасности).
|
||||
• <code>safe_message_interval</code>: Пауза между сообщениями в <b>безопасном режиме</b>.
|
||||
• <code>safe_cycle_interval</code>: Пауза между <b>кругами рассылки</b> в безопасном режиме (например <code>180,300</code> = 3-5 минут).
|
||||
|
||||
<b>-- Прочее --</b>
|
||||
• <code>delete_replies_delay</code>: Через сколько секунд удалять ответы модуля (например, "✅ Чат добавлен"). Поставьте <code>0</code>, чтобы не удалять.
|
||||
• <code>max_chats_safe</code>: Сколько максимум чатов обрабатывать за один круг в <b>безопасном режиме</b>.
|
||||
|
||||
<b>Шаг 5: Запустите рассылку</b>
|
||||
• Используйте команду <code>.start_mail <время> <пауза></code>
|
||||
• <b>Пример:</b> <code>.start_mail 3600 180-300</code>
|
||||
<i>(Это запустит рассылку на 1 час (3600 сек) с паузой между кругами от 3 до 5 минут).</i>
|
||||
|
||||
<b>Другие команды:</b>
|
||||
• <code>.stop_mail</code> — остановить рассылку.
|
||||
• <code>.mail_status</code> — проверить, сколько времени осталось.
|
||||
• <code>.remove_chat <номер></code> — удалить чат из списка.
|
||||
• <code>.remove_msg <номер></code> — удалить сообщение.
|
||||
• <code>.clear_chats</code> / <code>.clear_msgs</code> - полная очистка списков.
|
||||
</blockquote>
|
||||
"""
|
||||
await self._edit_or_reply_and_handle_deletion(message, help_text, delay=240)
|
||||
|
||||
@loader.command()
|
||||
async def add_chat(self, message):
|
||||
"""➕ Добавить чат. Можно несколько: .add_chat @user1 ссылка ..."""
|
||||
args = utils.get_args_raw(message)
|
||||
targets_to_find = []
|
||||
if args:
|
||||
targets_to_find = [ChatTarget(raw) for raw in args.split()]
|
||||
elif message.chat:
|
||||
targets_to_find = [ChatTarget(str(message.chat_id), context_message=message)]
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_arguments"]); return
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(
|
||||
message,
|
||||
self.strings["processing_entity"],
|
||||
delay=0
|
||||
)
|
||||
tasks = [self._find_chat(target) for target in targets_to_find]
|
||||
results = await asyncio.gather(*tasks)
|
||||
added, exists, errors_list = [], [], []
|
||||
async with self.lock:
|
||||
for i, res in enumerate(results):
|
||||
if res:
|
||||
if res["key"] in self.chats:
|
||||
exists.append(f"• {res['name']}")
|
||||
else:
|
||||
self.chats[res["key"]] = res["name"]
|
||||
added.append(f"• {res['name']}")
|
||||
else:
|
||||
errors_list.append(f"• {utils.escape_html(targets_to_find[i].raw)}")
|
||||
if added:
|
||||
self._save_db_chats()
|
||||
if len(targets_to_find) > 50:
|
||||
summary = self.strings["add_chat_summary_title"]
|
||||
if added: summary += f"<b>✅ Добавлено:</b> {len(added)}\n"
|
||||
if exists: summary += f"<b>⚠️ Уже существуют:</b> {len(exists)}\n"
|
||||
if errors_list: summary += f"<b>❌ Ошибки:</b> {len(errors_list)}\n"
|
||||
final_summary = summary.strip()
|
||||
else:
|
||||
summary = ""
|
||||
if added: summary += self.strings["add_chat_success_header"] + "\n".join(added) + "\n\n"
|
||||
if exists: summary += self.strings["add_chat_already_exists_header"] + "\n".join(exists) + "\n\n"
|
||||
if errors_list: summary += self.strings["add_chat_errors_header"] + "\n".join(errors_list)
|
||||
if not summary.strip():
|
||||
final_summary = self.strings["no_valid_chats_provided"]
|
||||
else:
|
||||
final_summary = self.strings["add_chat_summary_title"] + summary.strip()
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, final_summary)
|
||||
|
||||
@loader.command()
|
||||
async def remove_chat(self, message):
|
||||
"""🗑️ Удалить чат по номеру."""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args or not args.isdigit():
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_chat_selection"]); return
|
||||
idx_to_remove = int(args) - 1
|
||||
async with self.lock:
|
||||
sorted_keys = sorted(self.chats.keys(), key=lambda k: (self.chats[k], k[0], k[1] or -1))
|
||||
if 0 <= idx_to_remove < len(sorted_keys):
|
||||
key_to_remove = sorted_keys[idx_to_remove]
|
||||
removed_name = self.chats.pop(key_to_remove)
|
||||
self._save_db_chats()
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chat_removed"].format(idx_to_remove + 1, removed_name))
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_chat_selection"])
|
||||
|
||||
@loader.command()
|
||||
async def clear_chats(self, message):
|
||||
"""🗑️ Очистить список чатов."""
|
||||
async with self.lock:
|
||||
self.chats.clear()
|
||||
self.db.set(self.strings["name"], "chats", {})
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chats_cleared"])
|
||||
|
||||
@loader.command()
|
||||
async def list_chats(self, message):
|
||||
"""📜 Показать список чатов."""
|
||||
async with self.lock:
|
||||
current_chats_copy = dict(self.chats)
|
||||
if not current_chats_copy:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_chats"])
|
||||
return
|
||||
output_header = "Список чатов для рассылки:\n\n"
|
||||
sorted_items = sorted(current_chats_copy.items(), key=lambda item: (item[1], item[0][0], item[0][1] or -1))
|
||||
if len(sorted_items) > 50:
|
||||
file_content = output_header
|
||||
for i, ((cid, tid), name) in enumerate(sorted_items):
|
||||
topic_str = f' | Тема: {tid}' if tid is not None else ''
|
||||
file_content += f"{i+1}. {name} ({cid}{topic_str})\n"
|
||||
file = io.BytesIO(file_content.encode("utf-8"))
|
||||
file.name = "Mailing_Chat_List.txt"
|
||||
await self._edit_or_reply_and_handle_deletion(message, "📝 <b>Список чатов слишком большой, отправляю файлом...</b>", delay=0)
|
||||
await self.client.send_file(message.chat_id, file, caption=f"✅ <b>Список из {len(sorted_items)} чатов.</b>")
|
||||
return
|
||||
output = "<b>" + output_header.strip() + "</b>\n\n"
|
||||
for i, ((cid, tid), name) in enumerate(sorted_items):
|
||||
topic_str = f' | Тема: <code>{tid}</code>' if tid is not None else ''
|
||||
output += f"<b>{i+1}.</b> {utils.escape_html(name)} (<code>{cid}</code>{topic_str})\n"
|
||||
await self._edit_or_reply_and_handle_deletion(message, output, delay=60)
|
||||
|
||||
@loader.command()
|
||||
async def add_msg(self, message):
|
||||
"""➕ Добавить сообщение (ответом)."""
|
||||
reply = await message.get_reply_message()
|
||||
if not reply:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_messages"].split(". ")[0] + "."); return
|
||||
if reply.text: snippet_text = reply.text.replace("\n", " ")
|
||||
elif reply.photo: snippet_text = "[Фото]"
|
||||
elif reply.video: snippet_text = "[Видео]"
|
||||
elif reply.sticker:
|
||||
alt = next((attr.alt for attr in reply.sticker.attributes if isinstance(attr, tl_types.DocumentAttributeSticker)), "?")
|
||||
snippet_text = f"[Стикер: {alt}]"
|
||||
else: snippet_text = "[Медиа/Файл]"
|
||||
snippet = snippet_text[:100] + "..." if len(snippet_text) > 100 else snippet_text
|
||||
async with self.lock:
|
||||
self.messages.append({"id": reply.id, "chat_id": get_peer_id(reply.peer_id), "snippet": snippet})
|
||||
self.db.set(self.strings["name"], "messages", self.messages)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["message_added"].format(utils.escape_html(snippet)))
|
||||
|
||||
@loader.command()
|
||||
async def remove_msg(self, message):
|
||||
"""➖ Удалить сообщение по номеру."""
|
||||
args = utils.get_args_raw(message)
|
||||
if not args or not args.isdigit():
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_message_number"]); return
|
||||
idx = int(args) - 1
|
||||
async with self.lock:
|
||||
if 0 <= idx < len(self.messages):
|
||||
removed = self.messages.pop(idx)
|
||||
self.db.set(self.strings["name"], "messages", self.messages)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["message_removed"].format(idx + 1, utils.escape_html(removed["snippet"])))
|
||||
else:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["invalid_message_number"])
|
||||
|
||||
@loader.command()
|
||||
async def clear_msgs(self, message):
|
||||
"""🗑️ Очистить список сообщений."""
|
||||
async with self.lock:
|
||||
self.messages.clear()
|
||||
self.db.set(self.strings["name"], "messages", [])
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["messages_cleared"])
|
||||
|
||||
@loader.command()
|
||||
async def list_msgs(self, message):
|
||||
"""📜 Показать список сообщений."""
|
||||
if not self.messages:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["no_messages"]); return
|
||||
text = "<b>Список сообщений для рассылки:</b>\n\n"
|
||||
for i, msg in enumerate(self.messages):
|
||||
text += f"<b>{i + 1}.</b> {utils.escape_html(msg['snippet'])}\n"
|
||||
await self._edit_or_reply_and_handle_deletion(message, text, delay=60)
|
||||
|
||||
@loader.command()
|
||||
async def set_seller(self, message):
|
||||
"""⚙️ Установить ID для уведомлений."""
|
||||
args = utils.get_args_raw(message).strip()
|
||||
if not args:
|
||||
await self._edit_or_reply_and_handle_deletion(message, "✍️ Укажите ID чата, username, ссылку или 'me'."); return
|
||||
identifier = self.client.tg_id if args.lower() == 'me' else args
|
||||
try:
|
||||
entity = await self.client.get_entity(identifier)
|
||||
seller_id = get_peer_id(entity)
|
||||
async with self.lock:
|
||||
self.seller_chat_id = seller_id
|
||||
self.db.set(self.strings["name"], "seller_chat_id", seller_id)
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["seller_set"] + f": {get_display_name(entity)} (<code>{seller_id}</code>)")
|
||||
except Exception as e:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["error_getting_entity"].format(e))
|
||||
|
||||
@loader.command()
|
||||
async def mail_status(self, message):
|
||||
"""📊 Показать статус рассылки."""
|
||||
async with self.lock:
|
||||
if not self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["not_running"]); return
|
||||
now = datetime.now()
|
||||
elapsed = now - self.start_time
|
||||
remaining = self.end_time - now
|
||||
status = (
|
||||
f"📊 <b>Статус рассылки:</b> Активна ✅\n"
|
||||
f"⏳ <b>Прошло:</b> {str(elapsed).split('.')[0]}\n"
|
||||
f"⏱️ <b>Осталось:</b> {str(remaining).split('.')[0] if remaining.total_seconds() > 0 else '0:00:00'}\n"
|
||||
f"✉️ <b>Отправлено сообщений:</b> {self.total_messages_sent}\n"
|
||||
f"🔄 <b>Цикл:</b> {self._processed_chats_in_cycle} чатов обработано"
|
||||
)
|
||||
await self._edit_or_reply_and_handle_deletion(message, status, delay=30)
|
||||
|
||||
@loader.command()
|
||||
async def start_mail(self, message):
|
||||
"""🚀 Запустить рассылку."""
|
||||
args = utils.get_args(message)
|
||||
if len(args) != 2:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["duration_invalid"]); return
|
||||
try:
|
||||
duration = int(args[0])
|
||||
min_interval, max_interval = map(float, args[1].replace(",", ".").split("-"))
|
||||
if not (duration > 0 and 0 <= min_interval <= max_interval): raise ValueError
|
||||
cycle_interval = (min_interval, max_interval)
|
||||
except Exception:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["duration_invalid"]); return
|
||||
async with self.lock:
|
||||
if self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["already_running"]); return
|
||||
if not self.chats:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["chats_empty"]); return
|
||||
if not self.messages:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["messages_empty"]); return
|
||||
self.is_running = True
|
||||
self.total_messages_sent = 0
|
||||
self.start_time = datetime.now()
|
||||
self.end_time = self.start_time + timedelta(seconds=duration)
|
||||
self._current_cycle_start_time = None
|
||||
self._processed_chats_in_cycle = 0
|
||||
self.mail_task = self.client.loop.create_task(self._mail_loop(duration, cycle_interval, message))
|
||||
await self._edit_or_reply_and_handle_deletion(message, f"✅ Рассылка запущена на {duration} секунд.")
|
||||
|
||||
@loader.command()
|
||||
async def stop_mail(self, message):
|
||||
"""⏹️ Остановить рассылку."""
|
||||
async with self.lock:
|
||||
if not self.is_running:
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["not_running"]); return
|
||||
self.is_running = False
|
||||
if self.mail_task:
|
||||
self.mail_task.cancel()
|
||||
await self._edit_or_reply_and_handle_deletion(message, self.strings["stopped_mailing"])
|
||||
def _validate_interval_tuple(self, value, default_tuple: Tuple[float, float]) -> Tuple[float, float]:
|
||||
try:
|
||||
v_min, v_max = map(float, str(value).replace("-",",").split(','))
|
||||
if 0 <= v_min <= v_max: return (v_min, v_max)
|
||||
except Exception:
|
||||
pass
|
||||
return default_tuple
|
||||
|
||||
async def _is_safe_chat(self, entity: tl_types.TypePeer) -> bool:
|
||||
return isinstance(entity, (tl_types.Chat, tl_types.Channel)) and get_peer_id(entity) < -1000000000
|
||||
|
||||
async def _send_to_chat(self, target_chat_id: int, msg_info: dict, target_topic_id: Optional[int]) -> Tuple[bool, str]:
|
||||
try:
|
||||
original_msg = await self.client.get_messages(msg_info["chat_id"], ids=msg_info["id"])
|
||||
if not original_msg:
|
||||
return False, "Original message not found"
|
||||
for attempt in range(3):
|
||||
try:
|
||||
await self.client.send_message(entity=target_chat_id, message=original_msg, reply_to=target_topic_id)
|
||||
async with self.lock:
|
||||
self.total_messages_sent += 1
|
||||
return True, "OK" # :/
|
||||
except errors.FloodWaitError as e:
|
||||
if attempt == 2: return False, f"FloodWait ({e.seconds}s)"
|
||||
await asyncio.sleep(e.seconds + random.uniform(1, 3))
|
||||
except errors.SlowModeWaitError as e:
|
||||
await asyncio.sleep(e.seconds + random.uniform(0.2, 0.5))
|
||||
except Exception as e:
|
||||
if type(e).__name__ in self.PERMISSION_ERRORS:
|
||||
return False, type(e).__name__
|
||||
if attempt == 2: return False, str(e)
|
||||
await asyncio.sleep(random.uniform(2, 5))
|
||||
return False, "Max retries"
|
||||
except Exception as e:
|
||||
return False, f"Get message error: {e}"
|
||||
|
||||
async def _mail_loop(self, duration_seconds: int, cycle_interval_seconds_range: Tuple[float, float], initial_command_message_event):
|
||||
"""Оригинальный, надежный цикл рассылки"""
|
||||
end_time_loop = self.start_time + timedelta(seconds=duration_seconds)
|
||||
final_status_for_user = self.strings["mailing_complete"]
|
||||
try:
|
||||
while self.is_running and datetime.now() < end_time_loop:
|
||||
self._current_cycle_start_time = datetime.now()
|
||||
self._processed_chats_in_cycle = 0
|
||||
async with self.lock:
|
||||
current_chats = list(self.chats.keys())
|
||||
current_messages_list = list(self.messages)
|
||||
is_safe_mode = self.config["safe_mode"]
|
||||
randomize_messages_cfg = self.config["randomize_messages"]
|
||||
max_c_per_cycle = self.config["max_chats_safe"]
|
||||
chats_interval_key = "safe_chats_interval" if is_safe_mode else "chats_interval"
|
||||
short_interval = self._validate_interval_tuple(self.config[chats_interval_key], (10, 20) if is_safe_mode else (2, 5))
|
||||
message_interval_key = "safe_message_interval" if is_safe_mode else "message_interval"
|
||||
message_interval_val = self._validate_interval_tuple(self.config[message_interval_key], (5, 10) if is_safe_mode else (1, 3))
|
||||
if not current_chats or not current_messages_list:
|
||||
final_status_for_user = "Рассылка остановлена: список чатов или сообщений пуст."
|
||||
break
|
||||
random.shuffle(current_chats)
|
||||
chats_for_this_cycle = current_chats[:min(max_c_per_cycle if is_safe_mode else len(current_chats), len(current_chats))]
|
||||
for i, (chat_id_target, topic_id_target) in enumerate(chats_for_this_cycle):
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
messages_to_send_now = [random.choice(current_messages_list)] if randomize_messages_cfg else current_messages_list
|
||||
for message_detail in messages_to_send_now:
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
success_send, reason_send = await self._send_to_chat(chat_id_target, message_detail, topic_id_target)
|
||||
if not success_send:
|
||||
if reason_send in self.PERMISSION_ERRORS:
|
||||
logger.warning(f"Permission issue in {chat_id_target}, skipping chat.")
|
||||
else:
|
||||
logger.warning(f"Failed to send to {chat_id_target}: {reason_send}")
|
||||
break
|
||||
if len(messages_to_send_now) > 1:
|
||||
await asyncio.sleep(random.uniform(*message_interval_val))
|
||||
self._processed_chats_in_cycle += 1
|
||||
if i < len(chats_for_this_cycle) - 1:
|
||||
await asyncio.sleep(random.uniform(*short_interval))
|
||||
if not self.is_running or datetime.now() >= end_time_loop: break
|
||||
await asyncio.sleep(random.uniform(*cycle_interval_seconds_range))
|
||||
except asyncio.CancelledError:
|
||||
final_status_for_user = self.strings["stopped_mailing"]
|
||||
except Exception as e_loop:
|
||||
logger.exception("Критическая ошибка в цикле рассылки:")
|
||||
final_status_for_user = f"❌ Критическая ошибка: {type(e_loop).__name__}"
|
||||
finally:
|
||||
final_report = f"{final_status_for_user} (Отправлено: {self.total_messages_sent})"
|
||||
await self.client.send_message(initial_command_message_event.chat_id, final_report)
|
||||
if self.seller_chat_id:
|
||||
await self.client.send_message(self.seller_chat_id, f"🔔 Уведомление: {final_report}")
|
||||
async with self.lock:
|
||||
self.is_running = False
|
||||
self.mail_task = None
|
||||
|
||||
@loader.command()
|
||||
async def dump_chats(self, message):
|
||||
"""📤 Выгрузить список чатов рассылки в .txt файл (для бэкапа)."""
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(message, "⏳ <b>Экспорт списка рассылки...</b>", delay=0)
|
||||
async with self.lock:
|
||||
if not self.chats:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "⚠️ <b>Список чатов для рассылки пуст.</b>")
|
||||
return
|
||||
export_list = []
|
||||
for (cid, tid), name in self.chats.items():
|
||||
if tid is not None and cid < -1000000000:
|
||||
chat_id_for_link = str(cid)[4:]
|
||||
export_list.append(f"https://t.me/c/{chat_id_for_link}/{tid}")
|
||||
else:
|
||||
export_list.append(str(cid))
|
||||
file_content = "\n".join(export_list)
|
||||
file = io.BytesIO(file_content.encode("utf-8"))
|
||||
file.name = "mailing_list_backup.txt"
|
||||
await self.client.send_file(
|
||||
message.chat_id,
|
||||
file,
|
||||
caption=f"✅ <b>Экспортировано {len(export_list)} чатов из списка рассылки.</b>\n\nИспользуйте <code>.load_chats</code> в ответе на этот файл, чтобы импортировать их.")
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "✅ <b>Экспорт завершен!</b>")
|
||||
|
||||
@loader.command()
|
||||
async def load_chats(self, message):
|
||||
"""📤 Загрузить чаты в рассылку из .txt файла (ответом на файл)."""
|
||||
reply = await message.get_reply_message()
|
||||
if not reply or not reply.document:
|
||||
await self._edit_or_reply_and_handle_deletion(message, "✍️ <b>Ответьте на .txt файл с ID чатов.</b>")
|
||||
return
|
||||
if reply.document.mime_type != 'text/plain':
|
||||
await self._edit_or_reply_and_handle_deletion(message, "⚠️ <b>Файл должен быть в формате .txt</b>")
|
||||
return
|
||||
status_msg = await self._edit_or_reply_and_handle_deletion(message, "⏳ <b>Начинаю загрузку чатов из файла...</b>", delay=0)
|
||||
content = await reply.download_media(bytes)
|
||||
chat_identifiers = content.decode("utf-8").splitlines()
|
||||
chat_identifiers = [line.strip() for line in chat_identifiers if line.strip()]
|
||||
if not chat_identifiers:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, "⚠️ <b>Файл пуст или не содержит идентификаторов чатов.</b>")
|
||||
return
|
||||
added, exists, errors_list = [], [], []
|
||||
for i, identifier in enumerate(chat_identifiers):
|
||||
if i > 0 and i % 20 == 0:
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, f"⏳ <b>Обработано {i}/{len(chat_identifiers)}...</b>", delay=0)
|
||||
res = await self._find_chat(ChatTarget(identifier))
|
||||
if res:
|
||||
if res["key"] not in self.chats:
|
||||
self.chats[res["key"]] = res["name"]
|
||||
added.append(res["name"])
|
||||
else:
|
||||
exists.append(res["name"])
|
||||
else:
|
||||
errors_list.append(identifier)
|
||||
if added:
|
||||
self._save_db_chats()
|
||||
summary = f"✅ <b>Загрузка завершена!</b>\n\n"
|
||||
if added: summary += f"<b>Добавлено новых чатов:</b> {len(added)}\n"
|
||||
if exists: summary += f"<b>Уже были в списке:</b> {len(exists)}\n"
|
||||
if errors_list: summary += f"<b>Не удалось найти:</b> {len(errors_list)}\n"
|
||||
await self._edit_or_reply_and_handle_deletion(status_msg, summary)
|
||||
81
SenkoGuardian/SenModules/NekoEditorMod.py
Normal file
81
SenkoGuardian/SenModules/NekoEditorMod.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# This file is part of SenkoGuardianModules
|
||||
# Copyright (c) 2025 Senko
|
||||
# This software is released under the MIT License.
|
||||
# https://opensource.org/licenses/MIT
|
||||
|
||||
# meta developer: @SenkoGuardianModules
|
||||
|
||||
from hikkatl.types import Message
|
||||
from .. import loader, utils
|
||||
import random
|
||||
|
||||
@loader.tds
|
||||
class NekoEditorMod(loader.Module):
|
||||
"""Neko-редактор сообщений | Владелецы: @SstAngelStar × @ilovesenko """
|
||||
strings = {
|
||||
"name": "NekoEditor",
|
||||
}
|
||||
|
||||
def __init__(self):
|
||||
self.config = loader.ModuleConfig(
|
||||
loader.ConfigValue(
|
||||
"enabled",
|
||||
False,
|
||||
"Автоматическое редактирование",
|
||||
validator=loader.validators.Boolean()
|
||||
)
|
||||
)
|
||||
|
||||
async def nekoedcmd(self, message: Message):
|
||||
"""Управление Neko-режимом | .nekoed [on/off]"""
|
||||
args = utils.get_args_raw(message)
|
||||
me = await message.client.get_me()
|
||||
is_premium = getattr(me, 'premium', False)
|
||||
if not args:
|
||||
status = "включён" if self.config["enabled"] else "выключен"
|
||||
return await utils.answer(message, f"🐱 NekoEditor: {status}")
|
||||
if args.lower() in ["on", "вкл", "1"]:
|
||||
self.config["enabled"] = True
|
||||
if is_premium:
|
||||
await utils.answer(message, '<emoji document_id="5335044582218412321">☺️</emoji> Режим включён! Nya~')
|
||||
else:
|
||||
await utils.answer(message, "🐾 Режим включён! Nya~")
|
||||
elif args.lower() in ["off", "выкл", "0"]:
|
||||
self.config["enabled"] = False
|
||||
if is_premium:
|
||||
await utils.answer(message, '<emoji document_id="5377309873614627829">👌</emoji> Режим выключен... >_<')
|
||||
else:
|
||||
await utils.answer(message, "🌀 Режим выключен... >_<", parse_mode=None)
|
||||
|
||||
self.db.set("NekoEditor", "enabled", self.config["enabled"])
|
||||
|
||||
async def watcher(self, message: Message):
|
||||
if (
|
||||
not self.config["enabled"]
|
||||
or not getattr(message, "out", False)
|
||||
or getattr(message, "fwd_from", None)
|
||||
or getattr(message, "forward", None)
|
||||
or not message.text
|
||||
or "nekoed" in message.raw_text.lower()
|
||||
):
|
||||
return
|
||||
neko_words = ["Nya~", "UwU", "OwO", ">_<", "^^", "(≧▽≦)"]
|
||||
modified_text = message.text
|
||||
neko_word = random.choice(neko_words)
|
||||
if random.random() < 0.5:
|
||||
modified_text = f"{neko_word} {modified_text}"
|
||||
else:
|
||||
modified_text = f"{modified_text} {neko_word}"
|
||||
replacements = {
|
||||
"р": "w",
|
||||
"л": "w",
|
||||
"но": "ня",
|
||||
"на": "ня"
|
||||
}
|
||||
for old, new in replacements.items():
|
||||
modified_text = modified_text.replace(old, new)
|
||||
try:
|
||||
if message.text != modified_text:
|
||||
await message.edit(modified_text)
|
||||
except Exception:
|
||||
pass
|
||||
7
SenkoGuardian/SenModules/README.md
Normal file
7
SenkoGuardian/SenModules/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
- 👋 Hi, I’m @SenkoGuardian or Senko. I'm doing modules for Heroku UserBot
|
||||
- 📫 How to reach me -> Telegram: @ilovesenko
|
||||
- My Telegram chanel: @SenkoGuardianModules
|
||||
<!---
|
||||
SenkoGuardian/SenkoGuardian is a ✨ special ✨ repository because its `README.md` (this file) appears on your GitHub profile.
|
||||
You can click the Preview link to take a look at your changes.
|
||||
--->
|
||||
Reference in New Issue
Block a user