start proj

This commit is contained in:
naxim 2024-10-24 22:19:30 +03:00
parent 5f8eeb9604
commit 1359a151bc
13 changed files with 440 additions and 0 deletions

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM python:3.10
WORKDIR /app
COPY ./requirements.txt /app/requirements.txt
RUN pip install --upgrade pip
RUN pip install --no-cache-dir -r requirements.txt
ENV TZ=Europe/Moscow
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
COPY . .

42
bot/auth/auth.py Normal file
View File

@ -0,0 +1,42 @@
import aiohttp # Для выполнения HTTP-запросов
from loguru import logger
from database.db_main import User, session
from datetime import datetime
from .crypto import decrypt_password
# Функция для отправки POST-запроса к API и получения токена
async def get_token_from_api(email, password):
url = "https://api.otask.ru/api/v1/auth/login"
headers = {
"Content-Type": "application/json",
"Accept": "application/json"
}
body = {
"email": email,
"password": password
}
async with aiohttp.ClientSession() as session:
async with session.post(url, headers=headers, json=body) as response:
if response.status == 200:
return await response.json() # Возвращаем токен, если запрос успешен
else:
logger.error(f"Failed to get token. Status code: {response.status}")
return None
# Функция для обновления токена
async def update_token(user: User):
# Выполняем запрос на обновление токена
new_tokens = await get_token_from_api(user.name, decrypt_password(user.password)) # Получаем новый токен
if new_tokens:
user.token = new_tokens['token']
user.refresh_token = new_tokens['refresh_token']
user.expires_in = new_tokens['expires_in'] # Если у вас есть это поле в модели
user.last_token_update = datetime.now() # Обновляем дату последнего обновления токена
session.commit()
logger.info(f"Токен обновлён для пользователя: {user.telegram_id}")
else:
logger.warning(f"Не удалось обновить токен для пользователя: {user.telegram_id}")

28
bot/auth/crypto.py Normal file
View File

@ -0,0 +1,28 @@
from cryptography.fernet import Fernet
PATH_TO_FILE_CRYPT = "./secret.key"
# Функция для генерации ключа шифрования и записи его в файл
def generate_key_to_file():
key = Fernet.generate_key()
with open(PATH_TO_FILE_CRYPT, 'wb') as key_file:
key_file.write(key)
# Функция для чтения ключа из файла
def load_key_from_file() -> bytes:
with open(PATH_TO_FILE_CRYPT, 'rb') as key_file:
return key_file.read()
# Функция для шифрования пароля
def encrypt_password(password: str) -> str:
fernet = Fernet(load_key_from_file())
encrypted_password = fernet.encrypt(password.encode())
return encrypted_password.decode()
# Функция для расшифрования пароля
def decrypt_password(encrypted_password: str) -> str:
fernet = Fernet(load_key_from_file())
decrypted_password = fernet.decrypt(encrypted_password.encode())
return decrypted_password.decode()

82
bot/handlers/users.py Normal file
View File

@ -0,0 +1,82 @@
from aiogram.filters import Command
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
from aiogram.types import Message
from loguru import logger
from datetime import datetime
from bot.auth.auth import get_token_from_api
from database.db_main import User, session # Модель и сессия базы данных
from aiogram import F, Router
from database.db_main import UserManager
router = Router(name=__name__)
user_manager = UserManager()
# Определяем состояния для машины состояний
class AuthState(StatesGroup):
waiting_for_login = State()
waiting_for_password = State()
# Команда /start для запуска регистрации
@router.message(Command(commands=["start"]))
async def start_command(message: Message, state: FSMContext):
user = user_manager.get_user_by_telegram_id(message.from_user.id)
if not user:
# Если пользователь не зарегистрирован, запускаем процесс регистрации
prompt_message = await message.answer("Введите почту:")
# Сохраняем ID сообщения-подсказки в состоянии
await state.update_data(prompt_message_id=prompt_message.message_id)
await state.set_state(AuthState.waiting_for_login) # Устанавливаем состояние ожидания логина
else:
await message.answer("Вы уже зарегистрированы.")
# Обработка ввода логина
@router.message(AuthState.waiting_for_login)
async def process_login(message: Message, state: FSMContext):
await state.update_data(login=message.text) # Сохраняем логин в состоянии
await message.delete() # Удаляем сообщение с логином
# Удаляем сообщение с подсказкой
data = await state.get_data()
prompt_message_id = data.get("prompt_message_id")
if prompt_message_id:
await message.bot.delete_message(message.chat.id, prompt_message_id)
# Отправляем новое сообщение с просьбой ввести пароль и сохраняем его ID
password_prompt_message = await message.answer("Введите пароль:")
await state.update_data(password_prompt_message_id=password_prompt_message.message_id) # Сохраняем ID нового сообщения
await state.set_state(AuthState.waiting_for_password) # Переход в состояние ожидания пароля
# Обработка ввода пароля
@router.message(AuthState.waiting_for_password)
async def process_password(message: Message, state: FSMContext):
user_data = await state.get_data() # Получаем данные из состояния
login = user_data['login']
password = message.text
await message.delete() # Удаляем сообщение с паролем
# Удаляем сообщение с подсказкой для ввода пароля
password_prompt_message_id = user_data.get("password_prompt_message_id")
if password_prompt_message_id:
await message.bot.delete_message(message.chat.id, password_prompt_message_id)
# Отправляем запрос на получение токена
token_response = await get_token_from_api(login, password)
if token_response:
user_manager.save_user(
telegram_id=message.from_user.id,
login=login,
token_response=token_response,
password=password
)
await message.answer("Вы успешно зарегистрированы и токен сохранен!")
else:
await message.answer("Не удалось получить токен. Проверьте логин и пароль.")
await state.clear()

104
bot/utils/otask_api.py Normal file
View File

@ -0,0 +1,104 @@
from datetime import datetime
import aiohttp # Для выполнения HTTP-запросов
from loguru import logger
async def get_user_teams(token):
url = "https://api.otask.ru/api/v1/teams"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
teams = await response.json()
# logger.info(teams)
slugs = []
for group in teams['data']['teams']:
# logger.info(group)
# logger.info(group['slug'])
slugs.append(group['slug'])
logger.info(slugs)
return slugs
else:
print(f"Failed to get teams. Status code: {response.status}")
return None
async def get_client_name(token, ws_slug, client_id):
"""Получаем имя клиента по его идентификатору (client_id)."""
url = f"https://api.otask.ru/api/v1/ws/{ws_slug}/clients"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers) as response:
if response.status == 200:
data = await response.json()
clients = data.get('data', {}).get('clients', [])
# Ищем клиента по client_id в списке клиентов
for client in clients:
if client.get('id') == client_id:
return client
else:
logger.error(f"Failed to get client info for workspace {ws_slug}. Status code: {response.status}")
return None
async def get_regular_projects(token):
slugs = await get_user_teams(token) # Получаем все слуги команд пользователя
regular_projects = [] # Список для хранения регулярных проектов
for slug in slugs:
url = f"https://api.otask.ru/api/v1/ws/{slug}/panel/projects"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json",
"Accept": "application/json",
}
params = {
"is_regular": "true"
}
async with aiohttp.ClientSession() as session:
async with session.get(url, headers=headers, params=params) as response:
if response.status == 200:
data = await response.json() # Получаем все проекты
# Извлекаем проекты из ответа
projects = data.get('data', {}).get('projects', []).get('data', [])
for p in projects:
if p['end_at'] is not None:
end_at = datetime.fromisoformat(p['end_at'].replace("Z", "+00:00"))
difference_in_days = end_at.date().day - datetime.utcnow().date().day
if difference_in_days == 0: # Наступила дата платежа
project_info = {
'name': p.get('name', 'Неизвестный проект'),
'amount': p.get('amount', 'Неизвестная сумма')}
client_id = p.get('client_id','')
if client_id:
client = await get_client_name(token, slug, client_id)
project_info['first_name'] = client.get('first_name', 'Неизвестный клиент')
project_info["description"] = ''
if client['description']:
project_info["description"] = client["description"]
regular_projects.append(project_info)
else:
pass # логика для регулярного проекта в котором нет даты платежа
# Фильтруем регулярные проекты
else:
logger.error(f"Failed to get projects for workspace {slug}. Status code: {response.status}")
return regular_projects

57
database/db_main.py Normal file
View File

@ -0,0 +1,57 @@
from sqlalchemy import Column, Integer, String, DateTime, create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from datetime import datetime
from typing import Optional, List
from bot.auth.crypto import encrypt_password
Base = declarative_base()
class User(Base):
__tablename__ = 'users'
id = Column(Integer, primary_key=True)
telegram_id = Column(Integer, unique=True, nullable=False)
name = Column(String, nullable=False)
token = Column(String, nullable=False)
refresh_token = Column(String, nullable=False)
expires_in = Column(String, nullable=False)
password = Column(String, nullable=False)
last_token_update = Column(DateTime, default=datetime.now())
# Подключение к базе данных SQLite
engine = create_engine('sqlite:///reminder_bot.db')
Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
class UserManager:
def __init__(self):
self.session = Session()
def save_user(self, telegram_id: int, login: str, token_response, password: str):
"""Сохраняет нового пользователя в базу данных"""
new_user = User(
telegram_id=telegram_id,
name=login,
token=token_response['token'],
refresh_token=token_response['refresh_token'],
expires_in=token_response['expires_in'],
password=self.encrypt_password(password)
)
self.session.add(new_user)
self.session.commit()
def get_all_users(self) -> List[User]:
"""Получает всех пользователей из базы данных"""
return self.session.query(User).all()
def get_user_by_telegram_id(self, telegram_id: int) -> Optional[User]:
"""Получает пользователя по telegram_id"""
return self.session.query(User).filter_by(telegram_id=telegram_id).first()
@staticmethod
def encrypt_password(password: str) -> str:
return encrypt_password(password)
def close(self):
self.session.close()

10
database/prepare.py Normal file
View File

@ -0,0 +1,10 @@
import sqlite3
from .db_main import UsersDB
from settings import DB_PATH
con = sqlite3.connect(DB_PATH)
db = UsersDB(con)
db._drop_tables()
db._create_table()

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
services:
bot:
build: .
command: python -u start.py
restart: always
container_name: reminder
volumes:
- ./:/app
volumes:
reminder_vol:

BIN
reminder_bot.db Normal file

Binary file not shown.

10
requirements.txt Normal file
View File

@ -0,0 +1,10 @@
httpx==0.27
aiogram==3.13.1
python-telegram-bot==21.6
requests
loguru==0.7.2
SQLAlchemy==2.0.20
aioschedule==0.5.2
asyncpg==0.27.0
aiohttp==3.10.10
cryptography

1
secret.key Normal file
View File

@ -0,0 +1 @@
a5u-uciZjHWkeb21ggv3slYfZQyDYy3hvPPX0NnMRGI=

10
settings.py Normal file
View File

@ -0,0 +1,10 @@
import os
BOT_TOKEN = "8013236729:AAGJ8rT-6PH44SUd7j2NbZt3PJ2-iFsFzlA"
ROOTDIR = os.path.abspath(__file__).replace('/settings.py', '')
DB_PATH = os.path.join(ROOTDIR, 'db.sqlite3')
NOTIFICATION_TIMES = [
"09:00",
"12:00",
"15:00"
]

71
start.py Normal file
View File

@ -0,0 +1,71 @@
import asyncio
from datetime import datetime, timedelta
from math import trunc
from aiogram import Bot, Dispatcher
from aiogram.fsm.storage.memory import MemoryStorage
from loguru import logger
from bot.handlers import users
from bot.utils.otask_api import get_regular_projects
from bot.auth.auth import update_token
from bot.auth.crypto import generate_key_to_file
from database.db_main import UserManager
from settings import BOT_TOKEN, NOTIFICATION_TIMES
# Инициализация бота и диспетчера
bot = Bot(token=BOT_TOKEN)
storage = MemoryStorage() # Хранилище для машины состояний
dp = Dispatcher(storage=storage)
dp.include_routers(users.router)
user_manager = UserManager()
# генерирует новый ключ шифрования для паролей
# generate_key_to_file()
# Функция для запуска уведомлений
# Функция для отправки уведомлений пользователям
async def send_notifications():
users_query = user_manager.get_all_users()
for user in users_query:
if (datetime.now() - user.last_token_update) > timedelta(days=4):
await update_token(user)
regular_projects = await get_regular_projects(user.token)
for project in regular_projects:
await bot.send_message(user.telegram_id,
f"Регулярный платеж:\n"
f"{project['name']}\n"
f"{project['amount']}\n"
f"{project['first_name']}\n"
f"{project['description']}\n")
# else:
# await bot.send_message(user.telegram_id, "Нет проектов с регулярными платежами на данный момент.")
def should_send_notifications():
current_time = datetime.now().strftime("%H:%M")
return current_time in NOTIFICATION_TIMES
# Функция для проверки, нужно ли отправлять уведомления в текущее время
async def scheduler():
while True:
if should_send_notifications():
await send_notifications()
await asyncio.sleep(45)
# Асинхронная функция для запуска polling и планировщика
async def main():
# Запуск планировщика
asyncio.create_task(scheduler()) # Параллельно запускаем планировщик
logger.info("Планировщик запущен")
# Запуск polling бота
await dp.start_polling(bot)
logger.info("Polling бота запущен")
if __name__ == '__main__':
# Запуск основного цикла событий
asyncio.run(main())