diff --git a/.gitignore b/.gitignore index f67c040..2ca8c94 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,27 @@ -auth.ini -userdata -logs*.txt -*.log -*.logs -.env \ No newline at end of file +# Text backup files +*.bak + +# Database +*.sqlite3 +*.db + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +tests/.cache/ + +# virtualenv +venv/ +static/** + +# PyCharm +.idea + +# dotenv +.env + +# CI/CD +.venv/ +.wheels/ +.core diff --git a/migrations/env.py b/migrations/env.py index 19c985c..1be270f 100644 --- a/migrations/env.py +++ b/migrations/env.py @@ -2,11 +2,9 @@ from alembic import context from sqlalchemy import engine_from_config, pool - from src.db import Base from src.settings import Settings - # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/migrations/versions/14de3a5684ea_bigint.py b/migrations/versions/14de3a5684ea_bigint.py index 2277f0f..ab11753 100644 --- a/migrations/versions/14de3a5684ea_bigint.py +++ b/migrations/versions/14de3a5684ea_bigint.py @@ -9,7 +9,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = "14de3a5684ea" down_revision = "bd42bc6088a1" @@ -19,7 +18,9 @@ def upgrade(): # ### commands auto generated by Alembic - please adjust! ### - op.execute("ALTER TABLE tg_user ALTER COLUMN tg_id TYPE bigint USING tg_id::bigint;") + op.execute( + "ALTER TABLE tg_user ALTER COLUMN tg_id TYPE bigint USING tg_id::bigint;" + ) # ### end Alembic commands ### diff --git a/migrations/versions/56584d8792af_init.py b/migrations/versions/56584d8792af_init.py index 4634c87..2c64f37 100644 --- a/migrations/versions/56584d8792af_init.py +++ b/migrations/versions/56584d8792af_init.py @@ -1,7 +1,7 @@ """Init Revision ID: 56584d8792af -Revises: +Revises: Create Date: 2023-01-12 18:59:05.426265 """ @@ -9,7 +9,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = "56584d8792af" down_revision = None diff --git a/migrations/versions/bd42bc6088a1_nullable.py b/migrations/versions/bd42bc6088a1_nullable.py index e77da55..4b2f63d 100644 --- a/migrations/versions/bd42bc6088a1_nullable.py +++ b/migrations/versions/bd42bc6088a1_nullable.py @@ -9,7 +9,6 @@ import sqlalchemy as sa from alembic import op - # revision identifiers, used by Alembic. revision = "bd42bc6088a1" down_revision = "56584d8792af" diff --git a/src/__main__.py b/src/__main__.py index ff7f86c..b6661c8 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -1,27 +1,22 @@ # Marakulin Andrey https://github.com/Annndruha # 2023 +import asyncio import logging -from telegram.ext import ApplicationBuilder, CallbackQueryHandler, CommandHandler, MessageHandler, filters - from src.errors_solver import native_error_handler -from src.handlers import ( - handler_auth, - handler_button_browser, - handler_help, - handler_mismatch_doctype, - handler_other_messages, - handler_print, - handler_register, - handler_start, - handler_unknown_command, -) -from src.settings import Settings - +from src.handlers import (handler_auth, handler_button_browser, handler_help, + handler_mismatch_doctype, handler_other_messages, + handler_print, handler_register, handler_start, + handler_unknown_command) +from src.settings import get_settings, sync_from_server +from telegram.ext import (ApplicationBuilder, CallbackQueryHandler, + CommandHandler, MessageHandler, filters) tg_log_handler = logging.FileHandler("tgbot_telegram_updater.log") -tg_log_handler.setFormatter(logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")) +tg_log_handler.setFormatter( + logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +) tg_logger = logging.getLogger("telegram.ext._updater") tg_logger.propagate = False tg_logger.addHandler(tg_log_handler) @@ -35,16 +30,29 @@ ) if __name__ == "__main__": - settings = Settings() + asyncio.run(sync_from_server()) + settings = get_settings() application = ApplicationBuilder().token(settings.BOT_TOKEN).build() application.add_handler(CallbackQueryHandler(handler_button_browser)) - application.add_handler(CommandHandler("start", handler_start, filters=filters.UpdateType.MESSAGE)) - application.add_handler(CommandHandler("help", handler_help, filters=filters.UpdateType.MESSAGE)) - application.add_handler(CommandHandler("auth", handler_auth, filters=filters.UpdateType.MESSAGE)) + application.add_handler( + CommandHandler("start", handler_start, filters=filters.UpdateType.MESSAGE) + ) + application.add_handler( + CommandHandler("help", handler_help, filters=filters.UpdateType.MESSAGE) + ) + application.add_handler( + CommandHandler("auth", handler_auth, filters=filters.UpdateType.MESSAGE) + ) application.add_handler(MessageHandler(filters.COMMAND, handler_unknown_command)) application.add_handler(MessageHandler(filters.Document.PDF, handler_print)) - application.add_handler(MessageHandler(filters.Document.ALL, handler_mismatch_doctype)) - application.add_handler(MessageHandler(filters.UpdateType.MESSAGE & filters.TEXT, handler_register)) - application.add_handler(MessageHandler(filters.UpdateType.MESSAGE, handler_other_messages)) + application.add_handler( + MessageHandler(filters.Document.ALL, handler_mismatch_doctype) + ) + application.add_handler( + MessageHandler(filters.UpdateType.MESSAGE & filters.TEXT, handler_register) + ) + application.add_handler( + MessageHandler(filters.UpdateType.MESSAGE, handler_other_messages) + ) application.add_error_handler(native_error_handler) application.run_polling() diff --git a/src/answers.py b/src/answers.py index 09f39ca..10fd02c 100644 --- a/src/answers.py +++ b/src/answers.py @@ -3,78 +3,97 @@ from dataclasses import dataclass +from src.settings import get_settings + +settings = get_settings() + @dataclass class Answers: - auth = '🔑 Авторизация' - about = '📄 Описание' - back = '◀️ Назад' - qr = '📷 Печать по QR' - kb_print = '⚙️ Настройки печати' - kb_print_copies = '📑 Копий:' - kb_print_side = '📎 Односторонняя печать' - kb_print_two_side = '🖇 Двухсторонняя печать' - hello = '👋🏻 Привет! Я телеграм-бот бесплатного принтера.\n' 'Отправьте PDF файл и получите PIN для печати.' + auth = "🔑 Авторизация" + about = "📄 Описание" + back = "◀️ Назад" + qr = "📷 Печать по QR" + kb_print = "⚙️ Настройки печати" + kb_print_copies = "📑 Копий:" + kb_print_side = "📎 Односторонняя печать" + kb_print_two_side = "🖇 Двухсторонняя печать" + hello = ( + "👋🏻 Привет! Я телеграм-бот бесплатного принтера.\n" + f"Отправьте файл формата {settings.CONTENT_TYPES} и получите PIN для печати." + ) help = ( - 'Я телеграм-бот бесплатного принтера профкома студентов физического факультета МГУ!\n\n' - '❔ Отправьте PDF файл и получите PIN для печати. ' - 'Поддерживаются только .pdf файлы не более 3МБ.\n' - 'С этим PIN необходимо подойти к принтеру и ввести его в терминал печати. ' - 'Либо отсканировать QR-код на принтере с помощью кнопки. После этого начнётся печать.' - '\n\n' - '⚙️ Настройки печати можно изменять после отправки файла, они сохраняются автоматически. ' - 'В момент печати используются самые последние настройки.\n\n' - '❗️ Файлы, которые вы отправляете через бота, будут храниться в течение нескольких месяцев' - ' на сервере в Москве, а также в этом чате Telegram.\n' - 'Доступ к файлам имеет узкий круг лиц, ответственных за работоспособность сервиса печати.\n' - 'Мы НЕ рекомендуем использовать данный сервис для печати конфиденциальных документов!\n\n' - '💻 Бот разработан группой программистов профкома, ' + "Я телеграм-бот бесплатного принтера профкома студентов физического факультета МГУ!\n\n" + f"❔ Отправьте файл формата {settings.CONTENT_TYPES} и получите PIN для печати. " + f"Поддерживаются только .pdf файлы не более {settings.MAX_PDF_SIZE_MB} .\n" + "С этим PIN необходимо подойти к принтеру и ввести его в терминал печати. " + "Либо отсканировать QR-код на принтере с помощью кнопки. После этого начнётся печать." + "\n\n" + "⚙️ Настройки печати можно изменять после отправки файла, они сохраняются автоматически. " + "В момент печати используются самые последние настройки.\n\n" + "❗️ Файлы, которые вы отправляете через бота, будут храниться в течение нескольких месяцев" + " на сервере в Москве, а также в этом чате Telegram.\n" + "Доступ к файлам имеет узкий круг лиц, ответственных за работоспособность сервиса печати.\n" + "Мы НЕ рекомендуем использовать данный сервис для печати конфиденциальных документов!\n\n" + "💻 Бот разработан группой программистов профкома, " 'как и приложение Твой ФФ! ' - 'В приложении вы сможете найти больше настроек печати, расписание и много других возможностей.\n' + "В приложении вы сможете найти больше настроек печати, расписание и много других возможностей.\n" 'Так же есть бот для печати ВКонтакте.' ) val_fail = ( - '⚠️ Проверка не пройдена. Удостоверьтесь что вы состоите в профсоюзе и правильно ввели данные.\n\n' - 'Введите фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567' + "⚠️ Проверка не пройдена. Удостоверьтесь что вы состоите в профсоюзе и правильно ввели данные.\n\n" + "Введите фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567" ) - val_pass = '🥳 Поздравляю! Проверка пройдена и данные сохранены для этого телеграм-аккаунта. Можете присылать pdf.' + val_pass = "🥳 Поздравляю! Проверка пройдена и данные сохранены для этого телеграм-аккаунта. Можете присылать pdf." val_need = ( - '👤 Для использования принтера необходимо авторизоваться.\n' - 'Отправьте фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567' + "👤 Для использования принтера необходимо авторизоваться.\n" + "Отправьте фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567" ) val_update_fail = ( - 'Сообщение не распознано.\nЧтобы открыть инструкцию введите: /help\n' - 'Для того чтобы обновить данные авторизации введите фамилию и номер' - 'профсоюзного билета в формате:\n\nИванов\n1234567' + "Сообщение не распознано.\nЧтобы открыть инструкцию введите: /help\n" + "Для того чтобы обновить данные авторизации введите фамилию и номер" + "профсоюзного билета в формате:\n\nИванов\n1234567" ) - val_update_pass = '🥳 Поздравляю! Проверка пройдена и данные обновлены.' - val_addition = '\n\nНо для начала нужно авторизоваться. Нажмите на кнопку ниже:' + val_update_pass = "🥳 Поздравляю! Проверка пройдена и данные обновлены." + val_addition = "\n\nНо для начала нужно авторизоваться. Нажмите на кнопку ниже:" val_info = ( - 'Вы авторизованы!\n' - 'Ваш id в телеграм: {}\n' - 'Фамилия: {}\n' - 'Номер профсоюзного билета: {}' + "Вы авторизованы!\n" + "Ваш id в телеграм: {}\n" + "Фамилия: {}\n" + "Номер профсоюзного билета: {}" + ) + unknown_command = ( + "Неизвестная команда.\n" "У бота лишь три команды: /start /help /auth" ) - unknown_command = 'Неизвестная команда.\n' 'У бота лишь три команды: /start /help /auth' - only_pdf = 'Документы на печать принимаются только в формате PDF' + only_pdf = "Документы на печать принимаются только в формате PDF" doc_not_accepted = ( - '⚠️ Документ не принят, сначала авторизуйтесь.\n' - 'Отправьте фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567' + "⚠️ Документ не принят, сначала авторизуйтесь.\n" + "Отправьте фамилию и номер профсоюзного билета в формате:\n\nИванов\n1234567" + ) + file_size_error = ( + f"⚠️ Принимаются только файлы размером меньше {settings.MAX_PDF_SIZE_MB} MB.\n" + "Файл {} не принят." ) - file_size_error = '⚠️ Принимаются только файлы размером меньше 3 MB.\n' 'Файл {} не принят.' send_to_print = ( - '✅ Файл {} успешно загружен. Для печати подойдите к принтеру и введите PIN:\n\n' - '{}\n\n' - 'Для быстрой печати отсканируйте QR код на экране принтера.' + "✅ Файл {} успешно загружен. Для печати подойдите к принтеру и введите PIN:\n\n" + "{}\n\n" + "Для быстрой печати отсканируйте QR код на экране принтера." + ) + unreadable_file_error = ( + "⚠️ Я не смог прочитать файл {}." + f"Проверьте его целостность и формат, я работаю только с {settings.CONTENT_TYPES}." + ) + qr_print = "{}{}" + settings_warning = "Настройки сохраняются автоматически." + settings_change_fail = ( + "Что-то сломалось, настройки печати не изменены, попробуйте через пару минут." + ) + unknown_keyboard_payload = "Видимо бот обновился, выполните команду /start" + im_broken = "Глубоко внутри меня что-то сломалось...\nПопробуйте через пару минут." + download_error = "Ошибка получения файла, попробуйте позже." + print_err = "😵 Ошибка сервера печати. Попробуйте позже." + db_err = "😵 Ошибка базы данных. Попробуйте ещё раз, если не получилось, то попробуйте позже." + err_message_type = ( + "Сообщение не распознано.\nЧтобы открыть инструкцию введите: /help" ) - unreadable_file_error = '⚠️ Я не смог прочитать файл {}. Проверьте его целостность и формат, я работаю только с pdf.' - qr_print = '{}{}' - settings_warning = 'Настройки сохраняются автоматически.' - settings_change_fail = 'Что-то сломалось, настройки печати не изменены, попробуйте через пару минут.' - unknown_keyboard_payload = 'Видимо бот обновился, выполните команду /start' - im_broken = 'Глубоко внутри меня что-то сломалось...\nПопробуйте через пару минут.' - download_error = 'Ошибка получения файла, попробуйте позже.' - print_err = '😵 Ошибка сервера печати. Попробуйте позже.' - db_err = '😵 Ошибка базы данных. Попробуйте ещё раз, если не получилось, то попробуйте позже.' - err_message_type = 'Сообщение не распознано.\nЧтобы открыть инструкцию введите: /help' diff --git a/src/errors_solver.py b/src/errors_solver.py index 958d70f..708a61c 100644 --- a/src/errors_solver.py +++ b/src/errors_solver.py @@ -6,13 +6,11 @@ import psycopg2 from sqlalchemy.exc import SQLAlchemyError +from src.answers import Answers from telegram import Update from telegram.error import TelegramError from telegram.ext import ContextTypes -from src.answers import Answers - - ans = Answers() @@ -31,11 +29,13 @@ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): try: await func(update, context) except TelegramError as err: - logging.error(f'TelegramError: {str(err.message)}') + logging.error(f"TelegramError: {str(err.message)}") except (SQLAlchemyError, psycopg2.Error) as err: logging.error(err) traceback.print_tb(err.__traceback__) - await context.bot.send_message(chat_id=update.message.chat.id, text=ans.db_err) + await context.bot.send_message( + chat_id=update.message.chat.id, text=ans.db_err + ) except Exception as err: logging.error(err) traceback.print_tb(err.__traceback__) diff --git a/src/handlers.py b/src/handlers.py index b44c7a5..b399c0a 100644 --- a/src/handlers.py +++ b/src/handlers.py @@ -7,37 +7,42 @@ import requests from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, WebAppInfo -from telegram.constants import ParseMode -from telegram.error import TelegramError -from telegram.ext import CallbackContext, ContextTypes - from src import marketing from src.answers import Answers from src.db import TgUser from src.errors_solver import errors_solver from src.log_formatter import log_actor, log_formatter -from src.settings import Settings - +from src.settings import get_settings +from telegram import (InlineKeyboardButton, InlineKeyboardMarkup, Update, + WebAppInfo) +from telegram.constants import ParseMode +from telegram.error import TelegramError +from telegram.ext import CallbackContext, ContextTypes ans = Answers() -settings = Settings() -engine = create_engine(url=str(settings.DB_DSN), pool_pre_ping=True, isolation_level='AUTOCOMMIT') +settings = get_settings() +engine = create_engine( + url=str(settings.DB_DSN), pool_pre_ping=True, isolation_level="AUTOCOMMIT" +) Session = sessionmaker(bind=engine) @errors_solver @log_formatter async def handler_start(update: Update, context: ContextTypes.DEFAULT_TYPE): - keyboard_base = [[InlineKeyboardButton(ans.about, callback_data='to_about')]] + keyboard_base = [[InlineKeyboardButton(ans.about, callback_data="to_about")]] text, reply_markup = __change_message_by_auth(update, ans.hello, keyboard_base) - await update.message.reply_text(text=text, reply_markup=reply_markup, disable_web_page_preview=True) + await update.message.reply_text( + text=text, reply_markup=reply_markup, disable_web_page_preview=True + ) @errors_solver @log_formatter async def handler_help(update: Update, context: ContextTypes.DEFAULT_TYPE): - await update.message.reply_text(ans.help, disable_web_page_preview=True, parse_mode=ParseMode('HTML')) + await update.message.reply_text( + ans.help, disable_web_page_preview=True, parse_mode=ParseMode("HTML") + ) @errors_solver @@ -47,25 +52,27 @@ async def handler_auth(update: Update, context: ContextTypes.DEFAULT_TYPE): if requisites is None: await update.message.reply_text(ans.val_need) else: - await update.message.reply_text(ans.val_info.format(*requisites), parse_mode=ParseMode('HTML')) + await update.message.reply_text( + ans.val_info.format(*requisites), parse_mode=ParseMode("HTML") + ) @errors_solver @log_formatter async def handler_button_browser(update: Update, context: CallbackContext) -> None: - if update.callback_query.data == 'to_hello': - keyboard_base = [[InlineKeyboardButton(ans.about, callback_data='to_about')]] + if update.callback_query.data == "to_hello": + keyboard_base = [[InlineKeyboardButton(ans.about, callback_data="to_about")]] text, reply_markup = __change_message_by_auth(update, ans.hello, keyboard_base) - elif update.callback_query.data == 'to_about': - keyboard_base = [[InlineKeyboardButton(ans.back, callback_data='to_hello')]] + elif update.callback_query.data == "to_about": + keyboard_base = [[InlineKeyboardButton(ans.back, callback_data="to_hello")]] text, reply_markup = __change_message_by_auth(update, ans.help, keyboard_base) - elif update.callback_query.data == 'to_auth': - keyboard_base = [[InlineKeyboardButton(ans.back, callback_data='to_hello')]] + elif update.callback_query.data == "to_auth": + keyboard_base = [[InlineKeyboardButton(ans.back, callback_data="to_hello")]] text, reply_markup = ans.val_need, InlineKeyboardMarkup(keyboard_base) - elif update.callback_query.data.startswith('print_'): + elif update.callback_query.data.startswith("print_"): await __print_settings_solver(update, context) return @@ -76,7 +83,7 @@ async def handler_button_browser(update: Update, context: CallbackContext) -> No text=text, reply_markup=reply_markup, disable_web_page_preview=True, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) @@ -91,50 +98,65 @@ async def handler_unknown_command(update: Update, context: ContextTypes.DEFAULT_ async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): requisites = __auth(update) if requisites is None: - await context.bot.send_message(chat_id=update.message.chat.id, text=ans.doc_not_accepted) - logging.warning(f'{log_actor(update)} try print with no auth') + await context.bot.send_message( + chat_id=update.message.chat.id, text=ans.doc_not_accepted + ) + logging.warning(f"{log_actor(update)} try print with no auth") return try: filebytes, filename = await __get_attachments(update, context) - logging.info(f'{log_actor(update)} get attachments OK') + logging.info(f"{log_actor(update)} get attachments OK") except FileSizeError: await update.message.reply_text( text=ans.file_size_error.format(update.message.document.file_name), reply_to_message_id=update.message.id, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) - logging.warning(f'{log_actor(update)} get attachments FileSizeError') + logging.warning(f"{log_actor(update)} get attachments FileSizeError") return except TelegramError: - await update.message.reply_text(text=ans.download_error, reply_to_message_id=update.message.id) - logging.warning(f'{log_actor(update)} get attachments download_error') + await update.message.reply_text( + text=ans.download_error, reply_to_message_id=update.message.id + ) + logging.warning(f"{log_actor(update)} get attachments download_error") return r = requests.post( - settings.PRINT_URL + '/file', - json={'surname': requisites[1], 'number': requisites[2], 'filename': filename, 'source': 'tgbot'}, + settings.PRINT_URL + "/file", + json={ + "surname": requisites[1], + "number": requisites[2], + "filename": filename, + "source": "tgbot", + }, ) if r.status_code == 200: - pin = r.json()['pin'] + pin = r.json()["pin"] files = { - 'file': ( + "file": ( filename, filebytes.getvalue(), - 'application/pdf', - {'Expires': '0'}, + "application/pdf", + {"Expires": "0"}, ) } - rfile = requests.post(settings.PRINT_URL + '/file/' + pin, files=files) + rfile = requests.post(settings.PRINT_URL + "/file/" + pin, files=files) if rfile.status_code == 200: reply_markup = InlineKeyboardMarkup( [ [ InlineKeyboardButton( text=ans.qr, - web_app=WebAppInfo(ans.qr_print.format(settings.PRINT_URL_QR, pin)), + web_app=WebAppInfo( + ans.qr_print.format(settings.PRINT_URL_QR, pin) + ), + ) + ], + [ + InlineKeyboardButton( + ans.kb_print, callback_data=f"print_settings_{pin}" ) ], - [InlineKeyboardButton(ans.kb_print, callback_data=f'print_settings_{pin}')], ] ) await update.message.reply_text( @@ -142,9 +164,9 @@ async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): reply_markup=reply_markup, reply_to_message_id=update.message.id, disable_web_page_preview=True, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) - logging.info(f'{log_actor(update)} print success') + logging.info(f"{log_actor(update)} print success") marketing.print_success( tg_id=update.message.chat.id, surname=requisites[1], @@ -153,28 +175,30 @@ async def handler_print(update: Update, context: ContextTypes.DEFAULT_TYPE): return elif rfile.status_code == 415: await update.message.reply_text( - text=ans.unreadable_file_error.format(update.message.document.file_name), + text=ans.unreadable_file_error.format( + update.message.document.file_name + ), reply_to_message_id=update.message.id, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) - logging.warning(f'{log_actor(update)} print api 415 UnreadableErr') + logging.warning(f"{log_actor(update)} print api 415 UnreadableErr") return elif r.status_code == 413: await update.message.reply_text( text=ans.file_size_error.format(update.message.document.file_name), reply_to_message_id=update.message.id, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) - logging.warning(f'{log_actor(update)} print api 413 SizeErr') + logging.warning(f"{log_actor(update)} print api 413 SizeErr") return await context.bot.send_message( chat_id=update.effective_user.id, text=ans.print_err, - parse_mode=ParseMode('HTML'), + parse_mode=ParseMode("HTML"), ) - logging.warning(f'{log_actor(update)} print unknown error') + logging.warning(f"{log_actor(update)} print unknown error") @errors_solver @@ -196,65 +220,86 @@ async def handler_register(update: Update, context: ContextTypes.DEFAULT_TYPE): text = update.message.text chat_id = update.message.chat.id - if text is None or len(text.split('\n')) != 2: + if text is None or len(text.split("\n")) != 2: with Session() as session: - if session.query(TgUser).filter(TgUser.tg_id == chat_id).one_or_none() is None: + if ( + session.query(TgUser).filter(TgUser.tg_id == chat_id).one_or_none() + is None + ): await context.bot.send_message(chat_id=chat_id, text=ans.val_need) - logging.warning(f'{log_actor(update)} val_need') + logging.warning(f"{log_actor(update)} val_need") else: - await context.bot.send_message(chat_id=chat_id, text=ans.val_update_fail) - logging.warning(f'{log_actor(update)} val_update_fail') + await context.bot.send_message( + chat_id=chat_id, text=ans.val_update_fail + ) + logging.warning(f"{log_actor(update)} val_update_fail") return - if len(text.split('\n')) == 2: - surname = text.split('\n')[0].strip() - number = text.split('\n')[1].strip() + if len(text.split("\n")) == 2: + surname = text.split("\n")[0].strip() + number = text.split("\n")[1].strip() r = requests.get( - settings.PRINT_URL + '/is_union_member', + settings.PRINT_URL + "/is_union_member", params=dict(surname=surname, v=1, number=number), ) with Session() as session: - data: TgUser | None = session.query(TgUser).filter(TgUser.tg_id == update.effective_user.id).one_or_none() + data: TgUser | None = ( + session.query(TgUser) + .filter(TgUser.tg_id == update.effective_user.id) + .one_or_none() + ) if r.json() and data is None: session.add(TgUser(tg_id=chat_id, surname=surname, number=number)) session.commit() await context.bot.send_message(chat_id=chat_id, text=ans.val_pass) marketing.register(tg_id=chat_id, surname=surname, number=number) - logging.info(f'{log_actor(update)} register OK: {surname} {number}') + logging.info(f"{log_actor(update)} register OK: {surname} {number}") return True elif r.json() and data is not None: data.surname = surname data.number = number session.commit() - await context.bot.send_message(chat_id=chat_id, text=ans.val_update_pass) + await context.bot.send_message( + chat_id=chat_id, text=ans.val_update_pass + ) marketing.re_register(tg_id=chat_id, surname=surname, number=number) - logging.info(f'{log_actor(update)} register repeat OK: {surname} {number}') + logging.info( + f"{log_actor(update)} register repeat OK: {surname} {number}" + ) return True elif r.json() is False: await context.bot.send_message(chat_id=chat_id, text=ans.val_fail) - marketing.register_exc_wrong(tg_id=chat_id, surname=surname, number=number) - logging.info(f'{log_actor(update)} register val_fail: {surname} {number}') + marketing.register_exc_wrong( + tg_id=chat_id, surname=surname, number=number + ) + logging.info( + f"{log_actor(update)} register val_fail: {surname} {number}" + ) async def __print_settings_solver(update: Update, context: CallbackContext): - _, button, pin = update.callback_query.data.split('_') + _, button, pin = update.callback_query.data.split("_") - r = requests.get(settings.PRINT_URL + f'''/file/{pin}''') + r = requests.get(settings.PRINT_URL + f"""/file/{pin}""") if r.status_code == 200: - options = r.json()['options'] + options = r.json()["options"] else: await update.callback_query.message.reply_text(ans.settings_change_fail) return - if button == 'copies': - options['copies'] = options['copies'] % 5 + 1 - elif button == 'twosided': - options['two_sided'] = not options['two_sided'] + if button == "copies": + options["copies"] = options["copies"] % 5 + 1 + elif button == "twosided": + options["two_sided"] = not options["two_sided"] else: - await context.bot.answer_callback_query(update.callback_query.id, ans.settings_warning) + await context.bot.answer_callback_query( + update.callback_query.id, ans.settings_warning + ) - r = requests.patch(settings.PRINT_URL + f'''/file/{pin}''', json={'options': options}) + r = requests.patch( + settings.PRINT_URL + f"""/file/{pin}""", json={"options": options} + ) if r.status_code != 200: await update.callback_query.message.reply_text(ans.settings_change_fail) return @@ -269,13 +314,13 @@ async def __print_settings_solver(update: Update, context: CallbackContext): [ InlineKeyboardButton( f'{ans.kb_print_copies} {options["copies"]}', - callback_data=f'print_copies_{pin}', + callback_data=f"print_copies_{pin}", ) ], [ InlineKeyboardButton( - ans.kb_print_two_side if options['two_sided'] else ans.kb_print_side, - callback_data=f'print_twosided_{pin}', + ans.kb_print_two_side if options["two_sided"] else ans.kb_print_side, + callback_data=f"print_twosided_{pin}", ) ], ] @@ -285,7 +330,11 @@ async def __print_settings_solver(update: Update, context: CallbackContext): def __auth(update): with Session() as session: - data: TgUser | None = session.query(TgUser).filter(TgUser.tg_id == update.effective_user.id).one_or_none() + data: TgUser | None = ( + session.query(TgUser) + .filter(TgUser.tg_id == update.effective_user.id) + .one_or_none() + ) if data is not None: return data.tg_id, data.surname, data.number @@ -293,7 +342,7 @@ def __auth(update): def __change_message_by_auth(update, text, keyboard): if __auth(update) is None: text += ans.val_addition - keyboard.append([InlineKeyboardButton(ans.auth, callback_data='to_auth')]) + keyboard.append([InlineKeyboardButton(ans.auth, callback_data="to_auth")]) return text, InlineKeyboardMarkup(keyboard) diff --git a/src/log_formatter.py b/src/log_formatter.py index 145331c..cab9c1e 100644 --- a/src/log_formatter.py +++ b/src/log_formatter.py @@ -16,18 +16,26 @@ def log_formatter(func): @functools.wraps(func) async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): - actor_handler = f'[{update.effective_user.id} {update.effective_user.full_name}] [{func.__name__}]' + actor_handler = f"[{update.effective_user.id} {update.effective_user.full_name}] [{func.__name__}]" if update.callback_query is not None: - logging.info(f'{actor_handler} [callback {update.callback_query.message.id}]: {update.callback_query.data}') + logging.info( + f"{actor_handler} [callback {update.callback_query.message.id}]: {update.callback_query.data}" + ) elif update.message is not None: if update.message.text is not None: - logging.info(f'{actor_handler} [text]: {repr(update.message.text)}') + logging.info(f"{actor_handler} [text]: {repr(update.message.text)}") elif update.message.document is not None: - logging.info(f'{actor_handler} [document]: {repr(update.message.document.file_name)}') + logging.info( + f"{actor_handler} [document]: {repr(update.message.document.file_name)}" + ) else: - logging.info(f'{actor_handler} [UNKNOWN MESSAGE TYPE: {effective_message_type(update)}]') + logging.info( + f"{actor_handler} [UNKNOWN MESSAGE TYPE: {effective_message_type(update)}]" + ) else: - logging.info(f'{actor_handler} [UNKNOWN UPDATE TYPE: {effective_message_type(update)}]') + logging.info( + f"{actor_handler} [UNKNOWN UPDATE TYPE: {effective_message_type(update)}]" + ) await func(update, context) @@ -35,4 +43,4 @@ async def wrapper(update: Update, context: ContextTypes.DEFAULT_TYPE): def log_actor(update): - return f'[{update.effective_user.id} {update.effective_user.full_name}] >' + return f"[{update.effective_user.id} {update.effective_user.full_name}] >" diff --git a/src/marketing.py b/src/marketing.py index bec207f..a96980b 100644 --- a/src/marketing.py +++ b/src/marketing.py @@ -3,11 +3,9 @@ import traceback import requests +from src.settings import get_settings -from src.settings import Settings - - -settings = Settings() +settings = get_settings() def pass_if_exc(func): diff --git a/src/settings.py b/src/settings.py index 104b350..144b588 100644 --- a/src/settings.py +++ b/src/settings.py @@ -1,15 +1,50 @@ +from functools import lru_cache +from typing import List, Optional + +import requests from pydantic import ConfigDict, PostgresDsn from pydantic_settings import BaseSettings class Settings(BaseSettings): - """Application settings""" + """Application settings - DATA ONLY""" BOT_TOKEN: str DB_DSN: PostgresDsn MARKETING_URL: str PRINT_URL: str PRINT_URL_QR: str - MAX_PDF_SIZE_MB: float + MAX_PDF_SIZE_MB: int + MAX_PAGE_COUNT: int + CONTENT_TYPES: List[str] model_config = ConfigDict(case_sensitive=True, env_file=".env", extra="allow") + + +sync_settings: Optional[Settings] + + +@lru_cache(maxsize=1) +def get_settings() -> Settings: + global sync_settings + if sync_settings is not None: + return sync_settings + return Settings() + + +async def sync_from_server(): + """Syncs the settings with server""" + settings = get_settings() + response = requests.get( + "app.profcomff.com/admin/settings", # Вот тут не уверен с адресом... + headers={"Authorization": f"Bearer {settings.BOT_TOKEN}"}, + timeout=10, + ) + + if response.status_code == 200: + server_data = response.json() + current_data = get_settings().model_dump() + updated_data = {**current_data, **server_data} + + sync_settings = Settings(**updated_data) + get_settings.cache_clear()