commit 26c59ffb82de9db35ff09e2c5225dec9adf40fe1 Author: Whyverum Date: Fri Oct 3 09:31:12 2025 +0700 commit diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..59012c9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +# Исключить скрытые системные каталоги, но не всё подряд +.git/ +.gitattributes +.gitignore + +# Виртуальные окружения и Python-кэш +.venv/ +venv/ +__pycache__/ +*.py[cod] +*.pyo + +# IDE-файлы +.idea/ +.vscode/ + +# Тесты и документация +tests/ +test/ +docs/ +examples/ + +# Логи и артефакты сборки +*.log +*.logs +*.log.* +*.logs.* +Logs/ +Log/ +dist/ +build/ + +# Примеры и шаблоны +.env +env +*.session diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..99b1586 --- /dev/null +++ b/.env.example @@ -0,0 +1,32 @@ +# Общие параметры Python +PYTHONUNBUFFERED=1 + +# API для клиента Telegram +API_ID=1234567 +API_HASH=abcdef1234567890abcdef1234567890 + +# Телефонный аккаунт (если используется) +PHONE_NUMBER=+71234567890 +PASSWORD=your_password_here + +# Настройки бота +BOT_TOKEN=1234567890:ABCDefGhIJKlmNoPQRsTUVwxyZ +BOT_USERNAME=my_bot_username + +# Режим работы: user или bot +ACCOUNT_MODE=bot + +# Отправлять фото (True или False) +MSG_PHOTO=True + +# Период работы в секундах +PERIOD=3600 + +# Файл по умолчанию для отправки +DEFAULT_PHOTO=image.png + +# Текст сообщения +TEXT_MESSAGE='Приветствую, меня зовут Инокендий\n#флуд #ролевая #геншинимпакт #геншин #flood #rp #genshin' + +# Список ID групп (можно оставить пустым, если не используется) +GROUP_IDS=-1003057872759:0,-1002417346920:2,-1003019408279:0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..5a679a5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,97 @@ +# ============================================================================= +# Git LFS: большие бинарные файлы, модели, архивы +# ============================================================================= +*.7z filter=lfs diff=lfs merge=lfs -text +*.arrow filter=lfs diff=lfs merge=lfs -text +*.bin filter=lfs diff=lfs merge=lfs -text +*.bz2 filter=lfs diff=lfs merge=lfs -text +*.ckpt filter=lfs diff=lfs merge=lfs -text +*.ftz filter=lfs diff=lfs merge=lfs -text +*.gz filter=lfs diff=lfs merge=lfs -text +*.h5 filter=lfs diff=lfs merge=lfs -text +*.joblib filter=lfs diff=lfs merge=lfs -text +*.lfs.* filter=lfs diff=lfs merge=lfs -text +*.mlmodel filter=lfs diff=lfs merge=lfs -text +*.model filter=lfs diff=lfs merge=lfs -text +*.msgpack filter=lfs diff=lfs merge=lfs -text +*.npy filter=lfs diff=lfs merge=lfs -text +*.npz filter=lfs diff=lfs merge=lfs -text +*.onnx filter=lfs diff=lfs merge=lfs -text +*.ot filter=lfs diff=lfs merge=lfs -text +*.parquet filter=lfs diff=lfs merge=lfs -text +*.pb filter=lfs diff=lfs merge=lfs -text +*.pickle filter=lfs diff=lfs merge=lfs -text +*.pkl filter=lfs diff=lfs merge=lfs -text +*.pt filter=lfs diff=lfs merge=lfs -text +*.pth filter=lfs diff=lfs merge=lfs -text +*.rar filter=lfs diff=lfs merge=lfs -text +*.safetensors filter=lfs diff=lfs merge=lfs -text +saved_model/**/* filter=lfs diff=lfs merge=lfs -text +*.tar.* filter=lfs diff=lfs merge=lfs -text +*.tar filter=lfs diff=lfs merge=lfs -text +*.tflite filter=lfs diff=lfs merge=lfs -text +*.tgz filter=lfs diff=lfs merge=lfs -text +*.wasm filter=lfs diff=lfs merge=lfs -text +*.xz filter=lfs diff=lfs merge=lfs -text +*.zip filter=lfs diff=lfs merge=lfs -text +*.zst filter=lfs diff=lfs merge=lfs -text +*tfevents* filter=lfs diff=lfs merge=lfs -text + +# ============================================================================= +# Автоопределение текста, окончания строк +# ============================================================================= +* text=auto eol=lf + +# ============================================================================= +# Текстовые файлы (Python, конфиги, документы) +# ============================================================================= +*.py text +*.pyi text +*.ipynb text +*.html text +*.css text +*.js text +*.json text +*.md text +*.yml text +*.yaml text +*.xml text +*.txt text +*.cfg text +*.toml text +*.ini text +*.env text + +# ============================================================================= +# Изображения +# ============================================================================= +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.bmp binary +*.webp binary +*.ico binary +*.svg text + +# ============================================================================= +# Шрифты +# ============================================================================= +*.eot binary +*.ttf binary +*.woff binary +*.woff2 binary +*.otf binary + +# ============================================================================= +# GitHub Linguist (указание языка для отображения) +# ============================================================================= +*.py linguist-language=Python +*.ipynb linguist-language=Jupyter Notebook +*.html linguist-language=HTML +*.css linguist-language=CSS +*.js linguist-language=JavaScript +*.json linguist-language=JSON +*.md linguist-language=Markdown +*.yml linguist-language=YAML +*.yaml linguist-language=YAML diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5db36f8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,74 @@ +# .gitignore: Игнорируемые файлы для Python проектов +# Подробнее: https://github.com/github/gitignore/blob/main/Python.gitignore + +### Python ### +# Виртуальные окружения и настройки +.venv +.env +env +venv/ +env/ +env.bak/ +venv.bak/ + +# Кэш интерпретатора +__pycache__/ +*.py[cod] +*$py.class + +# Пакеты и сборки +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.eg +*.egg +*.eggs + +# Poetry +poetry.lock +.pypoetry/ + +### Логи и БД ### +*.log +*.logs +*.log.* +*.logs.* +log/ +logs/ +*.sqlite +*.db +*.session + +### IDE ### +.idea/ +.vscode/ +*.swp +*.sublime-* + +### OS ### +.DS_Store +Thumbs.db + +### Тестирование ### +.coverage +htmlcov/ +.tox/ +.nox/ +.pytest_cache/ +.mypy_cache/ +test/ +tests/ +Test/ +Tests/ +count.py diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..4cbd8d7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,26 @@ +# Используем официальный образ Python с подходящей версией +FROM python:3.12-slim + +# Устанавливаем Poetry +RUN pip install poetry + +# Устанавливаем рабочую директорию внутри контейнера +WORKDIR /app + +# Копируем файлы Poetry +COPY pyproject.toml poetry.lock* ./ + +# Настраиваем Poetry (не создавать виртуальное окружение внутри контейнера) +RUN poetry config virtualenvs.create false + +# Устанавливаем зависимости через Poetry +RUN poetry install --no-interaction --no-ansi --no-root + +# Копируем все файлы проекта внутрь контейнера +COPY . . + +# Устанавливаем переменную окружения для буферизации +ENV PYTHONUNBUFFERED=1 + +# Команда запуска — запуск скрипта main.py +CMD ["python", "main.py"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..889866f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2025] [Verum] + +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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e963fd8 Binary files /dev/null and b/README.md differ diff --git a/assets/image.jpg b/assets/image.jpg new file mode 100644 index 0000000..2fb9564 Binary files /dev/null and b/assets/image.jpg differ diff --git a/code/__init__.py b/code/__init__.py new file mode 100644 index 0000000..2583371 --- /dev/null +++ b/code/__init__.py @@ -0,0 +1,4 @@ +from .config import * +from .logs import * +from .media import * +from .sender import * diff --git a/code/config.py b/code/config.py new file mode 100644 index 0000000..53fd1bd --- /dev/null +++ b/code/config.py @@ -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() diff --git a/code/logs.py b/code/logs.py new file mode 100644 index 0000000..388e720 --- /dev/null +++ b/code/logs.py @@ -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 = ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "PRIMO-Message | " + "{extra[user]} | {message}" + ) + error_format: str = ( + "{time:YYYY-MM-DD HH:mm:ss} | " + "PRIMO-ERROR | " + "{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("Программа запущена!") diff --git a/code/media.py b/code/media.py new file mode 100644 index 0000000..5838b53 --- /dev/null +++ b/code/media.py @@ -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 diff --git a/code/sender.py b/code/sender.py new file mode 100644 index 0000000..003bf1a --- /dev/null +++ b/code/sender.py @@ -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) diff --git a/main.py b/main.py new file mode 100644 index 0000000..4e1f843 --- /dev/null +++ b/main.py @@ -0,0 +1,30 @@ +from asyncio import run +from pyrogram import Client + +from code import * + +async def main() -> None: + setup_logger() + + if settings.ACCOUNT_MODE: + async with Client( + name="user_session", + api_id=settings.API_ID, + api_hash=settings.API_HASH, + phone_number=settings.PHONE_NUMBER, + password=settings.PASSWORD, + ) as client: + await periodic_send(client) + + else: + async with Client( + name="bot_session", + api_id=settings.API_ID, + api_hash=settings.API_HASH, + bot_token=settings.BOT_TOKEN, + ) as client: + await periodic_send(client) + + +if __name__ == "__main__": + run(main()) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..21be85d --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "reklamabot" +version = "0.1.0" +description = "None" +authors = [ + {name = "Verum"} +] +license = {text = "MIT License"} +readme = "README.md" +requires-python = ">=3.10,<4.0" +dependencies = [ + "loguru (>=0.7.3,<0.8.0)", + "pyrogram (>=2.0.106,<3.0.0)", + "dotenv (>=0.9.9,<0.10.0)", + "python-dotenv (>=1.1.1,<2.0.0)", + "pydantic (>=2.11.9,<3.0.0)", + "pydantic-settings (>=2.11.0,<3.0.0)" +] + + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api"