This commit is contained in:
2025-10-03 09:31:12 +07:00
commit 26c59ffb82
15 changed files with 573 additions and 0 deletions

4
code/__init__.py Normal file
View File

@@ -0,0 +1,4 @@
from .config import *
from .logs import *
from .media import *
from .sender import *

84
code/config.py Normal file
View File

@@ -0,0 +1,84 @@
from typing import Dict, Optional
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
__all__ = ("settings",)
class Settings(BaseSettings):
"""Конфигурация основных режимов и параметров с валидацией"""
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
)
# Режимы и базовые параметры
PYTHONUNBUFFERED: str = "1"
API_ID: Optional[int] = None
API_HASH: Optional[str] = None
SOURCE_CHANNEL: Optional[int] = None
BOT_TOKEN: Optional[str] = None
BOT_USERNAME: Optional[str] = None
PHONE_NUMBER: Optional[str] = None
PASSWORD: Optional[str] = None
ACCOUNT_MODE: bool = True # True = аккаунт, False = бот
MSG_PHOTO: bool = True # True = фото, False = inline
PERIOD: int = 3600
# Файл по умолчанию для отправки
DEFAULT_PHOTO: str = "assets/image.jpg"
TEXT_MESSAGE: str = (
"Приветствую, меня зовут Инокендий\n"
"#флуд #ролевая #геншинимпакт #геншин #flood #rp #genshin"
)
# Словарь групп: {chat_id: reply_to_message_id}
GROUP_IDS: Dict[int, Optional[int]] = {}
# ================== Валидаторы ==================
@field_validator('PYTHONUNBUFFERED')
def validate_unbuffered(cls, v: str) -> str:
if v not in ('0', '1'):
raise ValueError("PYTHONUNBUFFERED должен быть '0' или '1'")
return v
@field_validator('ACCOUNT_MODE')
def validate_account_mode(cls, v: bool) -> bool:
if not isinstance(v, bool):
raise ValueError("ACCOUNT_MODE должен быть булевым: True = аккаунт, False = бот")
return v
@field_validator('PERIOD')
def validate_period(cls, v: int) -> int:
if v <= 0:
raise ValueError("PERIOD должен быть положительным числом")
return v
@field_validator('API_ID')
def validate_api_id(cls, v: int) -> int:
if v is None or v <= 0:
raise ValueError("API_ID должен быть положительным числом")
return v
@field_validator('GROUP_IDS', mode='before')
def parse_group_ids(cls, v):
"""
Конвертирует строку вида "-1003057872759:0,-1002417346920:2"
в словарь {chat_id: reply_to_message_id}
"""
if isinstance(v, str):
try:
return {int(k): int(val) for k, val in (x.split(":") for x in v.split(","))}
except Exception:
raise ValueError(
"Неправильный формат GROUP_IDS. "
"Пример: -100123:0,-100456:2"
)
return v
# Экземпляр класса
settings: Settings = Settings()

30
code/logs.py Normal file
View File

@@ -0,0 +1,30 @@
from sys import stderr as console
from loguru import logger
_all__ = ("setup_logger",)
def setup_logger(max_size: str = "500 MB") -> None:
"""Настройка логгера для приложения"""
logger.remove()
info_format: str = (
"<green>{time:YYYY-MM-DD HH:mm:ss}</green> | "
"<blue>PRIMO-Message</blue> | "
"<cyan>{extra[user]}</cyan> | <level>{message}</level>"
)
error_format: str = (
"<red>{time:YYYY-MM-DD HH:mm:ss}</red> | "
"<bold>PRIMO-ERROR</bold> | "
"{extra[user]} | {message}"
)
# INFO
logger.add(console, colorize=True, format=info_format, level="INFO")
logger.add("start.log", rotation=max_size, format=info_format, level="INFO")
# ERROR
logger.add(console, colorize=True, format=error_format, level="ERROR")
logger.add("error.log", rotation=max_size, format=error_format, level="ERROR")
logger.bind(user="@Console").info("Программа запущена!")

37
code/media.py Normal file
View File

@@ -0,0 +1,37 @@
from glob import glob
from loguru import logger
from typing import Optional
from .config import settings
__all__ = ("find_photo",)
class PhotoCache:
_cache: Optional[bytes] = None
@classmethod
async def find_photo(cls, file: str = None) -> bytes:
"""
Загружает фото в память и возвращает его как байты.
"""
if cls._cache:
return cls._cache
pattern: str = file or settings.DEFAULT_PHOTO
files: list[str] = glob(pattern)
if not files:
logger.bind(user="@Console").error(f"Файл {pattern} не найден.")
raise FileNotFoundError(f"Файл {pattern} не найден.")
chosen_file: str = files[0]
logger.bind(user="@Console").info(f"Выбран файл: {chosen_file}")
with open(chosen_file, "rb") as f:
cls._cache = f.read()
return cls._cache
# Создаем функцию для обратной совместимости
find_photo = PhotoCache.find_photo

79
code/sender.py Normal file
View File

@@ -0,0 +1,79 @@
from typing import Optional
from asyncio import sleep
from pyrogram import Client
from pyrogram.types import Message
from loguru import logger
from .config import settings
__all__ = ("send_inline_request", "copy_channel_message", "periodic_send",)
async def send_inline_request(client: Client) -> None:
"""Отправка inline-запроса от имени бота."""
for group_id in settings.GROUP_IDS.keys():
try:
inline_results = await client.get_inline_bot_results(
settings.BOT_USERNAME, "Реклама"
)
if not inline_results.results:
logger.bind(user=group_id).warning(
f"Нет inline-результатов для группы {group_id}"
)
continue
result_id = inline_results.results[0].id
await client.send_inline_bot_result(
chat_id=group_id,
query_id=inline_results.query_id,
result_id=result_id,
)
logger.bind(user=group_id).info(f"Inline результат отправлен в {group_id}")
except Exception as e:
logger.bind(user=group_id).error(f"Ошибка inline: {e}")
async def copy_channel_message(client: Client) -> None:
"""Копирование последнего сообщения с канала и отправка в группы без авторства."""
message: Optional[Message] = None
try:
# Получаем последнее сообщение с канала
async for msg in client.get_chat_history(settings.SOURCE_CHANNEL, limit=1):
message = msg
break # берём только первое (последнее) сообщение
if not message:
logger.bind(user="@Console").warning("Нет сообщений для копирования")
return
except Exception as e:
logger.bind(user="@Console").error(f"Не удалось получить сообщение с канала: {e}")
return
for group_id, reply_id in settings.GROUP_IDS.items():
try:
# Копируем сообщение без авторства
await client.copy_message(
chat_id=group_id,
from_chat_id=settings.SOURCE_CHANNEL,
message_id=message.id, # <-- используем id вместо message_id
reply_to_message_id=reply_id,
)
logger.bind(user=group_id).info(f"Сообщение скопировано в {group_id}")
except Exception as e:
logger.bind(user=group_id).error(f"Ошибка при отправке сообщения: {e}")
async def periodic_send(client: Client) -> None:
"""Цикл отправки сообщений с заданным периодом."""
while True:
if settings.MSG_PHOTO:
# Старый функционал фотографий заменяем на копирование сообщений
await copy_channel_message(client)
else:
await send_inline_request(client)
await sleep(settings.PERIOD)