Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.env
__pycache__/
*.pyc
*.sqlite
*.db
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Project Alerts Telegram Bot

MVP Telegram-бот для менеджеров проектов: задачи, напоминания по дедлайнам, роли и отчёт. Весь UX — через команды и inline-кнопки.

## Возможности
- CRUD задач: добавление, список, карточка, редактирование, завершение.
- Напоминания о дедлайнах с индикаторами 🟢🟡🔴🟣.
- Участники задач и упоминания (@username или user_id).
- Ссылки на документы.
- Роли (admin/editor/viewer) и доступы.

## Быстрый старт
1. Создайте бота в @BotFather и получите токен.
2. Создайте `.env`:
```bash
BOT_TOKEN=your-token
DATABASE_URL=sqlite+aiosqlite:///./data.db
DEFAULT_TIMEZONE=Europe/Berlin
```
3. Установите зависимости:
```bash
pip install -r requirements.txt
```
4. Запустите:
```bash
python -m app.main
```

## Команды
- `/start` — регистрация, меню.
- `/menu` — главное меню.
- `/help` — справка.
- `/admin` — управление ролями.

## Напоминания
Планировщик ежедневно проверяет задачи:
- 7–6 дней — 🟢
- 5–3 дня — 🟡
- 2–1 день — 🔴
- просрочка — 🟣 (раз в день)

Логи отправок фиксируются в `notifications_log`.
Empty file added app/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions app/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")

bot_token: str
database_url: str = "sqlite+aiosqlite:///./data.db"
default_timezone: str = "Europe/Berlin"


settings = Settings()
7 changes: 7 additions & 0 deletions app/db.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker, create_async_engine

from app.config import settings


engine: AsyncEngine = create_async_engine(settings.database_url, echo=False)
SessionLocal = async_sessionmaker(engine, expire_on_commit=False, class_=AsyncSession)
3 changes: 3 additions & 0 deletions app/handlers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from app.handlers import admin, notifications, report, start, tasks

__all__ = ["admin", "notifications", "report", "start", "tasks"]
48 changes: 48 additions & 0 deletions app/handlers/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from sqlalchemy import select

from app.db import SessionLocal
from app.models import User

router = Router()


async def is_admin(tg_user_id: str) -> bool:
async with SessionLocal() as session:
result = await session.execute(select(User).where(User.tg_user_id == tg_user_id))
user = result.scalar_one_or_none()
return bool(user and user.role == "admin")


@router.message(Command("admin"))
async def admin_panel(message: Message) -> None:
if not await is_admin(str(message.from_user.id)):
await message.answer("Недостаточно прав.")
return

parts = message.text.split()
if len(parts) == 4 and parts[1] == "set_role":
tg_user_id, role = parts[2], parts[3]
if role not in {"admin", "editor", "viewer"}:
await message.answer("Роль должна быть admin/editor/viewer")
return
async with SessionLocal() as session:
result = await session.execute(select(User).where(User.tg_user_id == tg_user_id))
user = result.scalar_one_or_none()
if not user:
await message.answer("Пользователь не найден.")
return
user.role = role
await session.commit()
await message.answer(f"Роль обновлена: {tg_user_id} -> {role}")
return

async with SessionLocal() as session:
result = await session.execute(select(User))
users = result.scalars().all()
lines = ["Использование: /admin set_role <tg_user_id> <role>"]
for user in users:
lines.append(f"{user.tg_user_id} — {user.role}")
await message.answer("\n".join(lines))
17 changes: 17 additions & 0 deletions app/handlers/notifications.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery

router = Router()


@router.callback_query(F.data == "notifications")
async def notifications_info(query: CallbackQuery) -> None:
await query.message.answer(
"Напоминания отправляются ежедневно по дедлайнам: \n"
"🟢 7–6 дней, 🟡 5–3 дня, 🔴 2–1 день, 🟣 просрочка."
)


@router.callback_query(F.data == "settings")
async def settings_info(query: CallbackQuery) -> None:
await query.message.answer("Настройки часового пояса пока фиксированы (Europe/Berlin).")
14 changes: 14 additions & 0 deletions app/handlers/report.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
from aiogram import F, Router
from aiogram.types import CallbackQuery

from app.db import SessionLocal
from app.services.reports import build_report

router = Router()


@router.callback_query(F.data == "report")
async def report_callback(query: CallbackQuery) -> None:
async with SessionLocal() as session:
report = await build_report(session)
await query.message.answer(report)
41 changes: 41 additions & 0 deletions app/handlers/start.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from aiogram import Router
from aiogram.filters import Command
from aiogram.types import Message
from app.db import SessionLocal
from app.keyboards import main_menu_keyboard
from app.models import User
from app.services.tasks import get_or_create_user

router = Router()


@router.message(Command("start"))
async def start_handler(message: Message) -> None:
async with SessionLocal() as session:
user = await get_or_create_user(
session,
tg_user_id=str(message.from_user.id),
tg_username=message.from_user.username,
name=message.from_user.full_name,
)
await session.commit()
text = "Привет! Это Project Alerts.\n\nГлавное меню ниже."
if user.role == "admin":
text += "\n\nВы назначены админом (первый пользователь)."
await message.answer(text, reply_markup=main_menu_keyboard())


@router.message(Command("menu"))
async def menu_handler(message: Message) -> None:
await message.answer("Главное меню:", reply_markup=main_menu_keyboard())


@router.message(Command("help"))
async def help_handler(message: Message) -> None:
await message.answer(
"Команды:\n"
"/start — регистрация и меню\n"
"/menu — главное меню\n"
"/help — справка\n"
"/admin — управление ролями (admin)"
)
Loading