start proj
This commit is contained in:
parent
5f8eeb9604
commit
1359a151bc
14
Dockerfile
Normal file
14
Dockerfile
Normal 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
42
bot/auth/auth.py
Normal 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
28
bot/auth/crypto.py
Normal 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
82
bot/handlers/users.py
Normal 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
104
bot/utils/otask_api.py
Normal 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
57
database/db_main.py
Normal 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
10
database/prepare.py
Normal 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
11
docker-compose.yml
Normal 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
BIN
reminder_bot.db
Normal file
Binary file not shown.
10
requirements.txt
Normal file
10
requirements.txt
Normal 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
1
secret.key
Normal file
@ -0,0 +1 @@
|
||||
a5u-uciZjHWkeb21ggv3slYfZQyDYy3hvPPX0NnMRGI=
|
10
settings.py
Normal file
10
settings.py
Normal 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
71
start.py
Normal 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())
|
Loading…
Reference in New Issue
Block a user