It's version 0.5
This commit is contained in:
+4
-1
@@ -13,5 +13,8 @@ utils/__pycache__/
|
|||||||
storage/message.txt
|
storage/message.txt
|
||||||
/addons/dowloadmp4_to_youtube/gettoken.py
|
/addons/dowloadmp4_to_youtube/gettoken.py
|
||||||
/drevo.py
|
/drevo.py
|
||||||
/addons/example_addon/cookies.txt
|
/addons/download_mp3_to_youtube/cookies.txt
|
||||||
/storage/message.db
|
/storage/message.db
|
||||||
|
/addons/todo/todo.db
|
||||||
|
/addons/rule34/urls.db
|
||||||
|
/addons/x_days_to/days_to_new_year.db
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Все значимые изменения проекта.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
- Добавлена проверка MD5-хеша скриншота в `services/watcher_service.py` — бот теперь отправляет/закрепляет фото только при первом обнаружении и при изменениях.
|
||||||
|
- Исправлен импорт в `addons/x_days_to/__init__.py` (модуль `x_days_to.py` теперь импортируется корректно).
|
||||||
|
- (Замечание) Найдена потенциальная ошибка: `addons/send_message/handlers.py` использует `aiohttp` без явного импорта — нужно поправить.
|
||||||
|
|
||||||
|
## 0.1.0 - WIP
|
||||||
|
- Начальная версия проекта. Планируется приватный релиз после исправлений и добавления CI.
|
||||||
|
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# MyFirstProgramm
|
||||||
|
|
||||||
|
Лёгкий Telegram-бот на Python (aiogram + Playwright) для автоматической проверки и рассылки расписаний, а также набора аддонов.
|
||||||
|
|
||||||
|
## Краткое описание
|
||||||
|
Проект периодически проверяет расписание на сайте (через Playwright), делает скриншоты нужных фрагментов, вычисляет хеш (MD5) и отправляет/закрепляет изображение в целевых чатах Telegram только при изменении (чтобы не флудить чат). Также в `addons/` есть дополнительные модули (мини-приложения).
|
||||||
|
|
||||||
|
## Структура (важные файлы/папки)
|
||||||
|
- `main.py` — точка входа приложения (запуск бота).
|
||||||
|
- `config.py` — конфигурация (переменные окружения).
|
||||||
|
- `services/` — сервисы: `schedule_service.py`, `watcher_service.py` (логика слежки/хэшей).
|
||||||
|
- `models/state.py` — in-memory состояние бота (кэши, last_clip_hash и т.д.).
|
||||||
|
- `addons/` — набор дополнительных модулей (каждый аддон экспортирует `register` / `unregister`).
|
||||||
|
- `requirements.txt` — зависимости.
|
||||||
|
|
||||||
|
## Быстрый старт (локально)
|
||||||
|
1. Установите зависимости (рекомендуется виртуальное окружение):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Создайте `.env` (или экспортируйте переменные окружения):
|
||||||
|
- `TELEGRAM_BOT_TOKEN` — токен бота
|
||||||
|
- (опционально) `DEEPGRAM_API_KEY`, `ACCESS_TOKEN` — если используются аддоны
|
||||||
|
|
||||||
|
Создайте файл `.env` рядом с `config.py`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
TELEGRAM_BOT_TOKEN=your_bot_token_here
|
||||||
|
#DEEPGRAM_API_KEY=...
|
||||||
|
#ACCESS_TOKEN=...
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Запустите бота (пример):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
(В вашем окружении запуск может быть оформлен скриптом — например `./Desktop/my.sh`.)
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
Основные настройки находятся в `config.py`. Обратите внимание, что `Config` сейчас поднимает исключение, если `TELEGRAM_BOT_TOKEN` не задан.
|
||||||
|
|
||||||
|
## Что изменилось (коротко)
|
||||||
|
- Добавлена проверка MD5-хеша скриншота в `services/watcher_service.py` — теперь бот отправляет фото и закрепляет сообщение только при первом обнаружении и при изменениях.
|
||||||
|
- Исправлен импорт в `addons/x_days_to/__init__.py` (модуль назывался `x_days_to.py`, вместо отсутствующего `handlers`).
|
||||||
|
|
||||||
|
(Более полный список изменений — в `CHANGELOG.md`.)
|
||||||
|
|
||||||
|
## Чек-лист готовности к релизу (private GitHub)
|
||||||
|
Ниже — рекомендации и текущий статус (на основе быстрого статического анализа):
|
||||||
|
|
||||||
|
- [ ] Тесты (unit/integration) — отсутствуют. Рекомендую добавить хотя бы базовые тесты для критичных сервисов (например мок ScheduleService и проверка логики хеширования).
|
||||||
|
- [x] README (есть) — ✅
|
||||||
|
- [x] CHANGELOG (есть) — ✅
|
||||||
|
- [ ] Линт/статический анализ — есть предупреждения/ошибки (см. ниже). Нужно исправить перед релизом.
|
||||||
|
- [ ] Безопасность/секреты — убедитесь, что `TELEGRAM_BOT_TOKEN` и другие ключи не коммитятся; добавьте `.env.example` и `.gitignore`.
|
||||||
|
- [ ] CI — рекомендую GitHub Actions: lint (ruff), тесты (pytest), security scan.
|
||||||
|
- [ ] Версионирование — добавьте `__version__` в `pyproject`/`setup.py` или тег релиза в Git.
|
||||||
|
|
||||||
|
### Проблемы, найденные статически
|
||||||
|
- `addons/send_message/handlers.py`: используется `aiohttp` без импорта (нужно `import aiohttp`). Это вызовет ошибку при импорте этого аддона.
|
||||||
|
- Возможны другие runtime-ошибки — рекомендую прогнать `ruff check . --fix` и запустить бота в dev окружении.
|
||||||
|
|
||||||
|
## Рекомендации перед релизом
|
||||||
|
1. Исправить импорт/синтаксисные ошибки (см. `ruff` / `get_errors`).
|
||||||
|
2. Добавить `.env.example` и `.gitignore` (включить `.venv/`, `storage/` с чувствительными файлами и логами).
|
||||||
|
3. Добавить базовые тесты и настроить GitHub Actions (lint + tests).
|
||||||
|
4. Решить поведение state-персистенции: `BotState` хранит состояние в памяти — после рестарта всё теряется. Если важно сохранять историю/фиксированный pinned_id — добавить simple JSON/SQLite storage.
|
||||||
|
5. Проверить, что все аддоны регистрируют хендлеры корректно (в `x_days_to` ранее не было регистрации а-ля `@dp.message(...)` — возможно требуется доработить).
|
||||||
|
|
||||||
|
## Как выпустить на приватный GitHub
|
||||||
|
1. Создайте репозиторий и инициализируйте git:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git init
|
||||||
|
git add .
|
||||||
|
git commit -m "Initial commit"
|
||||||
|
# создайте приватный репо в GitHub и затем
|
||||||
|
git remote add origin git@github.com:yourorg/yourrepo.git
|
||||||
|
git branch -M main
|
||||||
|
git push -u origin main
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Настройте GitHub Actions (workflow) для lint & tests.
|
||||||
|
3. Добавьте релизный тег и changelog entry:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag -a v0.1.0 -m "Initial private release"
|
||||||
|
git push origin v0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Если хотите, я могу сразу:
|
||||||
|
- добавить `.env.example` и `.gitignore`;
|
||||||
|
- исправить `addons/send_message/handlers.py` (добавить импорт `aiohttp`) и/или добавить базовый тест для `watcher_service`.
|
||||||
|
|
||||||
|
Скажите, что выполнить следующим шагом — добавлять `.env.example`/`.gitignore`, исправлять найденную ошибку или настроить CI (создать пример workflow).
|
||||||
+2
-2
@@ -21,7 +21,7 @@ async def get_video_info(url: str) -> dict:
|
|||||||
"--dump-json",
|
"--dump-json",
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
"--cookies",
|
"--cookies",
|
||||||
"~/myfirstprogramm/addons/example_addon/cookies.txt",
|
"/addons/download_mp3_to_youtube/cookies.txt",
|
||||||
url,
|
url,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.PIPE,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
@@ -116,7 +116,7 @@ async def download_mp3_isolated(url: str) -> tuple[str, dict]:
|
|||||||
"--audio-format",
|
"--audio-format",
|
||||||
"mp3",
|
"mp3",
|
||||||
"--cookies",
|
"--cookies",
|
||||||
"~/myfirstprogramm/addons/example_addon/cookies.txt",
|
"/addons/download_mp3_to_youtube/cookies.txt",
|
||||||
"--audio-quality",
|
"--audio-quality",
|
||||||
"320K",
|
"320K",
|
||||||
"--no-playlist",
|
"--no-playlist",
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def register(dp, state, bot):
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
handlers.register_handlers(dp, state, bot)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(dp):
|
||||||
|
# Здесь можно удалить хендлеры, если нужно
|
||||||
|
dp.message_handlers.handlers.clear()
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
from aiogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton, WebAppInfo
|
||||||
|
from models.state import BotState
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from logging import getLogger
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_day() -> int:
|
||||||
|
day = datetime.now().day
|
||||||
|
if day == 6:
|
||||||
|
return day + 1
|
||||||
|
return day
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
|
||||||
|
@dp.message(Command("app"))
|
||||||
|
async def send_welcome(message: Message):
|
||||||
|
# Создаём инлайн-кнопку для открытия Web App
|
||||||
|
keyboard = InlineKeyboardMarkup(inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="Открыть мини-приложение", web_app=WebAppInfo(url=f"https://college.by/accounts/raspis/{datetime.now().month}/{get_day()}-PODNAM.htm"))]
|
||||||
|
])
|
||||||
|
|
||||||
|
await message.answer(
|
||||||
|
f"Расписание на {get_day()} число месяца:",
|
||||||
|
reply_markup=keyboard
|
||||||
|
)
|
||||||
@@ -20,7 +20,7 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
|
|||||||
poll_msg = await bot.send_poll(
|
poll_msg = await bot.send_poll(
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
question="Кто опоздает?",
|
question="Кто опоздает?",
|
||||||
options=["Я", "Я очень сильно опоздаю", "Наверное"],
|
options=["Я", "Я очень сильно опоздаю", "Я пиздец как опоздаю", "Наверное"],
|
||||||
is_anonymous=False,
|
is_anonymous=False,
|
||||||
allows_multiple_answers=False,
|
allows_multiple_answers=False,
|
||||||
)
|
)
|
||||||
@@ -56,6 +56,11 @@ def register_handlers(dp: Dispatcher, state: BotState, bot: Bot):
|
|||||||
)
|
)
|
||||||
save_message(msg.chat.id, msg.message_id)
|
save_message(msg.chat.id, msg.message_id)
|
||||||
elif option_ids and option_ids[0] == 2:
|
elif option_ids and option_ids[0] == 2:
|
||||||
|
msg = await bot.send_message(
|
||||||
|
chat_id=6394047531, text=f"{username} Пиздец опоздаеты"
|
||||||
|
)
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
elif option_ids[0] == 3:
|
||||||
msg = await bot.send_message(
|
msg = await bot.send_message(
|
||||||
chat_id=6394047531, text=f"{username} возможно опоздает"
|
chat_id=6394047531, text=f"{username} возможно опоздает"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def register(dp, state, bot):
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
handlers.register_handlers(dp, state, bot)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(dp):
|
||||||
|
# Здесь можно удалить хендлеры, если нужно
|
||||||
|
dp.message_handlers.handlers.clear()
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import requests
|
||||||
|
from random import randint, choice
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
|
BASE_URL = "https://rule34.xxx"
|
||||||
|
HEADERS = {
|
||||||
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)"
|
||||||
|
}
|
||||||
|
URL = f"{BASE_URL}/index.php?page=post&s=list"
|
||||||
|
MAXIMUM = 999
|
||||||
|
|
||||||
|
# Хранилище тегов в памяти
|
||||||
|
TAGS = set()
|
||||||
|
|
||||||
|
def add_tags(tags: list[str]):
|
||||||
|
TAGS.update(tags)
|
||||||
|
|
||||||
|
def del_tags(tags: list[str]):
|
||||||
|
for t in tags:
|
||||||
|
TAGS.discard(t)
|
||||||
|
|
||||||
|
def get_tags_str() -> str:
|
||||||
|
return "+".join(TAGS) if TAGS else "(нет тегов)"
|
||||||
|
|
||||||
|
def get_tags() -> str:
|
||||||
|
return "+".join(TAGS) if TAGS else ""
|
||||||
|
|
||||||
|
def get_url():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
tags = get_tags()
|
||||||
|
pid = randint(1, MAXIMUM)
|
||||||
|
|
||||||
|
# Формируем корректный URL с query‑параметрами
|
||||||
|
if tags:
|
||||||
|
url_page = f"{URL}&tags={tags}&pid={pid}"
|
||||||
|
else:
|
||||||
|
url_page = f"{URL}&pid={pid}"
|
||||||
|
|
||||||
|
r = requests.get(url_page, headers=HEADERS, timeout=5)
|
||||||
|
soup = BeautifulSoup(r.text, 'lxml')
|
||||||
|
block = soup.find(class_="image-list")
|
||||||
|
if not block:
|
||||||
|
continue
|
||||||
|
block = block.find_all("span")
|
||||||
|
if not block:
|
||||||
|
continue
|
||||||
|
|
||||||
|
link = choice(block).find("a")
|
||||||
|
if not link:
|
||||||
|
continue
|
||||||
|
|
||||||
|
r2 = requests.get(f"{BASE_URL}{link.get('href')}", headers=HEADERS, timeout=10)
|
||||||
|
soup_two = BeautifulSoup(r2.text, 'lxml')
|
||||||
|
|
||||||
|
flexi = soup_two.find(class_="flexi")
|
||||||
|
if not flexi:
|
||||||
|
continue
|
||||||
|
|
||||||
|
img = flexi.find("img")
|
||||||
|
if not img:
|
||||||
|
continue
|
||||||
|
|
||||||
|
url = img.get("src")
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[get_url ERROR] {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto, Message, CallbackQuery
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.exceptions import TelegramBadRequest, TelegramRetryAfter
|
||||||
|
from aiogram import F
|
||||||
|
from logging import getLogger
|
||||||
|
from storage.message_storage import save_message
|
||||||
|
from .get_post import get_url, add_tags, del_tags, get_tags_str
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def get_keyboard():
|
||||||
|
keyboard = InlineKeyboardMarkup(
|
||||||
|
inline_keyboard=[
|
||||||
|
[InlineKeyboardButton(text="Следующее фото", callback_data="next")]
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return keyboard
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(dp: Dispatcher, state, bot: Bot):
|
||||||
|
|
||||||
|
@dp.message(Command("rule34"))
|
||||||
|
async def rule34(message: Message):
|
||||||
|
msg = await message.answer_photo(
|
||||||
|
photo=get_url(),
|
||||||
|
caption="Вот фото 📷",
|
||||||
|
reply_markup=get_keyboard()
|
||||||
|
)
|
||||||
|
|
||||||
|
# сохраняем id сообщения, если нужно
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
|
||||||
|
@dp.callback_query(F.data == "next")
|
||||||
|
async def next_photo(callback: CallbackQuery):
|
||||||
|
for attempt in range(3): # максимум 3 попытки
|
||||||
|
try:
|
||||||
|
media = InputMediaPhoto(
|
||||||
|
media=get_url(),
|
||||||
|
caption=f"Новое фото 🌄 (попытка {attempt + 1})"
|
||||||
|
)
|
||||||
|
await callback.message.edit_media(
|
||||||
|
media=media,
|
||||||
|
reply_markup=get_keyboard()
|
||||||
|
)
|
||||||
|
break # успех — выходим из цикла
|
||||||
|
|
||||||
|
except TelegramRetryAfter as e:
|
||||||
|
# Telegram сказал подождать e.retry_after секунд
|
||||||
|
logger.warning(f"Flood control: ждем {e.retry_after} сек")
|
||||||
|
await asyncio.sleep(e.retry_after)
|
||||||
|
continue # пробуем снова
|
||||||
|
|
||||||
|
except TelegramBadRequest as e:
|
||||||
|
logger.warning(f"Ошибка при загрузке фото (попытка {attempt + 1}): {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
# если все попытки неудачные
|
||||||
|
logger.error("Не удалось загрузить фото после 3 попыток")
|
||||||
|
|
||||||
|
# закрываем "часики" в любом случае
|
||||||
|
await callback.answer()
|
||||||
|
|
||||||
|
@dp.message(Command("addteg"))
|
||||||
|
async def cmd_addteg(message: Message):
|
||||||
|
# Разбиваем текст после команды на теги
|
||||||
|
parts = message.text.split()[1:]
|
||||||
|
if not parts:
|
||||||
|
await message.answer("❌ Укажи теги через пробел: /addteg tag1 tag2")
|
||||||
|
return
|
||||||
|
add_tags(parts)
|
||||||
|
await message.answer(f"✅ Добавлены теги: {', '.join(parts)}\nТекущие: {get_tags_str()}")
|
||||||
|
|
||||||
|
@dp.message(Command("delteg"))
|
||||||
|
async def cmd_delteg(message: Message):
|
||||||
|
parts = message.text.split()[1:]
|
||||||
|
if not parts:
|
||||||
|
await message.answer("❌ Укажи теги для удаления: /delteg tag1 tag2")
|
||||||
|
return
|
||||||
|
del_tags(parts)
|
||||||
|
await message.answer(f"🗑 Удалены теги: {', '.join(parts)}\nТекущие: {get_tags_str()}")
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import sqlite3
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
DIR = "/Users/mac/myfirstprogramm/addons/todo/todo.db" # лучше указать полный путь
|
||||||
|
|
||||||
|
# создаём подключение
|
||||||
|
db = sqlite3.connect(DIR)
|
||||||
|
cursor = db.cursor()
|
||||||
|
|
||||||
|
# создаём таблицу, если её ещё нет
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS tasks (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
who INTEGER NOT NULL,
|
||||||
|
task TEXT NOT NULL,
|
||||||
|
due_date TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# функция для добавления задачи
|
||||||
|
def add_task(who: int, task: str, due_date: str):
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO tasks (who, task, due_date) VALUES (?, ?, ?)",
|
||||||
|
(who, task, due_date)
|
||||||
|
)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# функция для получения всех задач пользователя
|
||||||
|
def get_tasks(who: int):
|
||||||
|
cursor.execute("SELECT id, task, due_date FROM tasks WHERE who = ?", (who,))
|
||||||
|
return cursor.fetchall()
|
||||||
|
|
||||||
|
# функция для удаления всех задач пользователя
|
||||||
|
def delete_tasks_by_who(who: int):
|
||||||
|
cursor.execute("DELETE FROM tasks WHERE who = ?", (who,))
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
# пример использования
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# добавляем задачи
|
||||||
|
add_task(123456789, "Сделать бота", datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||||
|
add_task(123456789, "Проверить базу", datetime.now().strftime("%d.%m.%Y %H:%M:%S"))
|
||||||
|
|
||||||
|
print("Задачи до удаления:")
|
||||||
|
tasks = get_tasks(123456789)
|
||||||
|
for t in tasks:
|
||||||
|
print(t)
|
||||||
|
|
||||||
|
# удаляем все задачи пользователя
|
||||||
|
delete_tasks_by_who(123456789)
|
||||||
|
|
||||||
|
print("\nЗадачи после удаления:")
|
||||||
|
tasks = get_tasks(123456789)
|
||||||
|
for t in tasks:
|
||||||
|
print(t)
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def register(dp, state, bot):
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
handlers.register_handlers(dp, state, bot)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(dp):
|
||||||
|
# Здесь можно удалить хендлеры, если нужно
|
||||||
|
dp.message_handlers.handlers.clear()
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
from aiogram.types import Message
|
||||||
|
from aiogram.filters import Command
|
||||||
|
from aiogram.fsm.state import State, StatesGroup
|
||||||
|
from aiogram.fsm.context import FSMContext
|
||||||
|
import logging
|
||||||
|
from .DB import get_tasks, add_task, delete_tasks_by_who
|
||||||
|
from storage.message_storage import save_message
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class TodoForm(StatesGroup):
|
||||||
|
waiting_for_task = State()
|
||||||
|
|
||||||
|
def register_handlers(dp: Dispatcher, state, bot: Bot):
|
||||||
|
# /todos — начать добавление задачи
|
||||||
|
@dp.message(Command("todos"))
|
||||||
|
async def cmd_todos(message: Message, state: FSMContext):
|
||||||
|
save_message(message.chat.id, message.message_id) # сохраняем сообщение
|
||||||
|
await message.answer("Введите текст задачи:")
|
||||||
|
await state.set_state(TodoForm.waiting_for_task)
|
||||||
|
|
||||||
|
# обработка текста задачи
|
||||||
|
@dp.message(TodoForm.waiting_for_task)
|
||||||
|
async def process_task(message: Message, state: FSMContext):
|
||||||
|
save_message(message.chat.id, message.message_id) # сохраняем сообщение
|
||||||
|
task_text = message.text
|
||||||
|
user_id = message.from_user.id
|
||||||
|
try:
|
||||||
|
created_at = datetime.now().strftime("%d.%m.%Y %H:%M:%S")
|
||||||
|
add_task(user_id, task_text, created_at)
|
||||||
|
reply = await message.answer(f"Задача сохранена ✅\n{task_text}")
|
||||||
|
save_message(reply.chat.id, reply.message_id) # сохраняем ответ бота
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при добавлении задачи: {e}")
|
||||||
|
reply = await message.answer("Не удалось сохранить задачу ❌")
|
||||||
|
save_message(reply.chat.id, reply.message_id)
|
||||||
|
finally:
|
||||||
|
await state.clear()
|
||||||
|
|
||||||
|
# /todor — показать список задач
|
||||||
|
@dp.message(Command("todor"))
|
||||||
|
async def cmd_todor(message: Message):
|
||||||
|
save_message(message.chat.id, message.message_id) # сохраняем сообщение
|
||||||
|
user_id = message.from_user.id
|
||||||
|
try:
|
||||||
|
tasks = get_tasks(user_id)
|
||||||
|
if not tasks:
|
||||||
|
reply = await message.answer("У вас пока нет задач 📝")
|
||||||
|
save_message(reply.chat.id, reply.message_id)
|
||||||
|
return
|
||||||
|
|
||||||
|
text = "Ваши задачи:\n\n"
|
||||||
|
for tid, task, created_at in tasks:
|
||||||
|
text += f"• {task} (создана {created_at})\n"
|
||||||
|
reply = await message.answer(text)
|
||||||
|
save_message(reply.chat.id, reply.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при чтении задач: {e}")
|
||||||
|
reply = await message.answer("Не удалось получить список задач ❌")
|
||||||
|
save_message(reply.chat.id, reply.message_id)
|
||||||
|
|
||||||
|
@dp.message(Command("todod"))
|
||||||
|
async def del_todo(message: Message):
|
||||||
|
delete_tasks_by_who(message.from_user.id)
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def register(dp, state, bot):
|
||||||
|
from . import handlers
|
||||||
|
|
||||||
|
handlers.register_handlers(dp, state, bot)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister(dp):
|
||||||
|
# Здесь можно удалить хендлеры, если нужно
|
||||||
|
dp.message_handlers.handlers.clear()
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import asyncio
|
||||||
|
import aiosqlite
|
||||||
|
from datetime import datetime
|
||||||
|
from logging import getLogger
|
||||||
|
from aiogram import Dispatcher, Bot
|
||||||
|
from config import Config
|
||||||
|
from models.state import BotState
|
||||||
|
from utils.antispam import save_message
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def register_handlers(dp: Dispatcher, state: BotState, bot: Bot) -> int:
|
||||||
|
async def init_db():
|
||||||
|
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
|
||||||
|
await db.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS days_to_new_year (
|
||||||
|
user_id INTEGER PRIMARY KEY,
|
||||||
|
days INTEGER NOT NULL,
|
||||||
|
timestamp TEXT NOT NULL
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
await db.commit()
|
||||||
|
logger.info("База данных инициализирована")
|
||||||
|
|
||||||
|
async def save_days_to_db(user_id: int, days: int):
|
||||||
|
logger.debug(f"Сохраняем user_id={user_id}, days={days}")
|
||||||
|
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
|
||||||
|
await db.execute("""
|
||||||
|
INSERT OR REPLACE INTO days_to_new_year (user_id, days, timestamp)
|
||||||
|
VALUES (?, ?, ?)
|
||||||
|
""", (int(user_id), int(days), datetime.now().strftime("%Y-%m-%d %H:%M:%S")))
|
||||||
|
await db.commit()
|
||||||
|
logger.info(f"Запись сохранена: user_id={user_id}, days={days}")
|
||||||
|
|
||||||
|
async def get_last_days(user_id: int) -> int | None:
|
||||||
|
async with aiosqlite.connect(Config.DAYS_TO_DB_PATH) as db:
|
||||||
|
async with db.execute(
|
||||||
|
"SELECT days FROM days_to_new_year WHERE user_id = ?", (int(user_id),)
|
||||||
|
) as cursor:
|
||||||
|
row = await cursor.fetchone()
|
||||||
|
return row[0] if row else None
|
||||||
|
|
||||||
|
async def days_to_new_years() -> int:
|
||||||
|
now = datetime.now()
|
||||||
|
new_year = datetime(now.year + 1, 1, 1)
|
||||||
|
delta = (new_year - now).days
|
||||||
|
logger.debug(f"До Нового года осталось {delta} дней")
|
||||||
|
return delta
|
||||||
|
|
||||||
|
async def days_to_summer() -> int:
|
||||||
|
"""Считает дни до 1 июня текущего года (или следующего, если уже лето прошло)."""
|
||||||
|
now = datetime.now()
|
||||||
|
summer = datetime(now.year, 6, 1)
|
||||||
|
if now >= summer:
|
||||||
|
summer = datetime(now.year + 1, 6, 1)
|
||||||
|
delta = (summer - now).days
|
||||||
|
logger.debug(f"До лета осталось {delta} дней")
|
||||||
|
return delta
|
||||||
|
|
||||||
|
async def send_days_to_new_years(user_id: int):
|
||||||
|
days_ny = await days_to_new_years()
|
||||||
|
days_summer = await days_to_summer()
|
||||||
|
last_days = await get_last_days(user_id)
|
||||||
|
|
||||||
|
if last_days == days_ny:
|
||||||
|
logger.info(f"user_id={user_id}: запись уже есть ({days_ny} дней), пропускаем")
|
||||||
|
return
|
||||||
|
|
||||||
|
await save_days_to_db(user_id, days_ny)
|
||||||
|
|
||||||
|
message_text = (
|
||||||
|
f"🌞 До лета осталось {days_summer} дней!\n"
|
||||||
|
f"🎄 До Нового Года осталось {days_ny} дней!"
|
||||||
|
)
|
||||||
|
|
||||||
|
for chat_id in Config.CHAT_IDS:
|
||||||
|
try:
|
||||||
|
logger.info(f"Отправляем сообщение в чат {chat_id} для user_id={user_id}")
|
||||||
|
await bot.send_message(chat_id, message_text)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при отправке в чат {chat_id}: {e}")
|
||||||
|
|
||||||
|
async def periodic_task():
|
||||||
|
await init_db()
|
||||||
|
while True:
|
||||||
|
logger.info("Запуск цикла periodic_task")
|
||||||
|
for uid in Config.CHAT_IDS:
|
||||||
|
try:
|
||||||
|
msg = await send_days_to_new_years(int(uid))
|
||||||
|
if msg:
|
||||||
|
save_message(msg.chat.id, msg.message_id)
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Ошибка при обработке uid={uid}: {e}")
|
||||||
|
logger.info("Завершён цикл periodic_task, спим 6 часов")
|
||||||
|
await asyncio.sleep(21600) # каждые 6 часов
|
||||||
|
|
||||||
|
asyncio.create_task(periodic_task())
|
||||||
|
return 0
|
||||||
+7
-3
@@ -22,13 +22,17 @@ class TelegramBot:
|
|||||||
# common.register_handlers(self.dp, self.state, self.bot)
|
# common.register_handlers(self.dp, self.state, self.bot)
|
||||||
|
|
||||||
# add addons
|
# add addons
|
||||||
self.addons.load("example_addon")
|
self.addons.load("download_mp3_to_youtube")
|
||||||
self.addons.load("id")
|
self.addons.load("id")
|
||||||
self.addons.load("send_message")
|
self.addons.load("send_message")
|
||||||
self.addons.load("poll")
|
self.addons.load("poll")
|
||||||
self.addons.load("hello")
|
self.addons.load("hello")
|
||||||
self.addons.load("draw")
|
# self.addons.load("draw")
|
||||||
self.addons.load("gpt")
|
# self.addons.load("gpt")
|
||||||
|
# self.addons.load("rule34")
|
||||||
|
# self.addons.load("todo")
|
||||||
|
self.addons.load("miniapps")
|
||||||
|
self.addons.load("x_days_to")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""Запуск бота"""
|
"""Запуск бота"""
|
||||||
|
|||||||
Reference in New Issue
Block a user