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

36
.dockerignore Normal file
View File

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

32
.env.example Normal file
View File

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

97
.gitattributes vendored Normal file
View File

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

74
.gitignore vendored Normal file
View File

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

26
Dockerfile Normal file
View File

@@ -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"]

21
LICENSE Normal file
View File

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

BIN
README.md Normal file

Binary file not shown.

BIN
assets/image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

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)

30
main.py Normal file
View File

@@ -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())

23
pyproject.toml Normal file
View File

@@ -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"