From d727ee38f2ff30c0153762b9054bfba3affeeaf9 Mon Sep 17 00:00:00 2001 From: Alexey Voronin Date: Mon, 13 Jun 2022 17:35:49 +0300 Subject: [PATCH 1/5] chore(refactoring): split to class-based version --- .pylintrc | 585 ++++++++++++++++++ answers/menu.py | 49 -- answers/settings.py | 69 --- app/__init__.py | 40 ++ forms.py => app/forms/OrioksAuthForm.py | 3 +- app/forms/__init__.py | 3 + app/handlers/AbstractCallbackHandler.py | 11 + app/handlers/AbstractCommandHandler.py | 11 + app/handlers/AbstractErrorHandler.py | 11 + app/handlers/__init__.py | 65 ++ .../callbacks/SettingsCallbackHandler.py | 27 + .../callbacks/UserAgreementCallbackHandler.py | 22 + app/handlers/callbacks/__init__.py | 4 + .../handlers/commands}/__init__.py | 0 .../admins/AdminStatisticsCommandHandler.py | 56 ++ app/handlers/commands/admins/__init__.py | 3 + .../commands/general/FAQCommandHandler.py | 16 + .../commands/general/ManualCommandHandler.py | 16 + .../commands/general/StartCommandHandler.py | 11 + app/handlers/commands/general/__init__.py | 8 + .../orioks/OrioksAuthCancelCommandHandler.py | 28 + .../OrioksAuthInputLoginCommandHandler.py | 40 ++ .../OrioksAuthInputPasswordCommandHandler.py | 87 +++ .../orioks/OrioksAuthStartCommandHandler.py | 39 ++ .../orioks/OrioksLogoutCommandHandler.py | 27 + app/handlers/commands/orioks/__init__.py | 11 + .../NotificationSettingsCommandHandler.py | 129 ++++ app/handlers/commands/settings/__init__.py | 3 + app/handlers/errors/BaseErrorHandler.py | 24 + app/handlers/errors/__init__.py | 3 + app/menus/AbstractMenu.py | 9 + app/menus/__init__.py | 0 app/menus/orioks/OrioksAuthFailedMenu.py | 23 + app/menus/orioks/__init__.py | 3 + app/menus/start/StartMenu.py | 41 ++ app/menus/start/__init__.py | 1 + app/middlewares/AdminCommandsMiddleware.py | 15 + app/middlewares/UserAgreementMiddleware.py | 34 + .../UserOrioksAttemptsMiddleware.py | 33 + app/middlewares/__init__.py | 5 + app/models/BaseModel.py | 28 + app/models/__init__.py | 7 + app/models/admins/AdminStatistics.py | 10 + app/models/admins/__init__.py | 3 + app/models/users/UserNotifySettings.py | 21 + app/models/users/UserStatus.py | 17 + app/models/users/__init__.py | 4 + checking/homeworks/get_orioks_homeworks.py | 10 +- checking/marks/get_orioks_marks.py | 8 +- checking/news/get_orioks_news.py | 16 +- checking/on_startup.py | 26 +- checking/requests/get_orioks_requests.py | 10 +- config.py | 95 +-- db/admins_statistics.py | 25 +- db/notify_settings.py | 19 +- db/user_first_add.py | 13 +- db/user_status.py | 31 +- handlers/admins.py | 51 -- handlers/callback_queries.py | 36 -- handlers/commands.py | 38 -- handlers/errors.py | 20 - handlers/notify_settings.py | 47 -- handlers/orioks_auth.py | 189 ------ handles_register.py | 51 -- images/imager.py | 9 +- images/test.py | 19 - main.py | 38 -- middlewares.py | 61 -- requirements.txt | 2 +- run-app.py | 4 + utils/handle_orioks_logout.py | 18 +- utils/make_request.py | 6 +- utils/makedirs.py | 19 +- utils/notify_to_user.py | 14 +- utils/orioks.py | 17 +- 75 files changed, 1714 insertions(+), 833 deletions(-) create mode 100644 .pylintrc delete mode 100644 answers/menu.py delete mode 100644 answers/settings.py create mode 100644 app/__init__.py rename forms.py => app/forms/OrioksAuthForm.py (75%) create mode 100644 app/forms/__init__.py create mode 100644 app/handlers/AbstractCallbackHandler.py create mode 100644 app/handlers/AbstractCommandHandler.py create mode 100644 app/handlers/AbstractErrorHandler.py create mode 100644 app/handlers/__init__.py create mode 100644 app/handlers/callbacks/SettingsCallbackHandler.py create mode 100644 app/handlers/callbacks/UserAgreementCallbackHandler.py create mode 100644 app/handlers/callbacks/__init__.py rename {handlers => app/handlers/commands}/__init__.py (100%) create mode 100644 app/handlers/commands/admins/AdminStatisticsCommandHandler.py create mode 100644 app/handlers/commands/admins/__init__.py create mode 100644 app/handlers/commands/general/FAQCommandHandler.py create mode 100644 app/handlers/commands/general/ManualCommandHandler.py create mode 100644 app/handlers/commands/general/StartCommandHandler.py create mode 100644 app/handlers/commands/general/__init__.py create mode 100644 app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py create mode 100644 app/handlers/commands/orioks/OrioksAuthInputLoginCommandHandler.py create mode 100644 app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py create mode 100644 app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py create mode 100644 app/handlers/commands/orioks/OrioksLogoutCommandHandler.py create mode 100644 app/handlers/commands/orioks/__init__.py create mode 100644 app/handlers/commands/settings/NotificationSettingsCommandHandler.py create mode 100644 app/handlers/commands/settings/__init__.py create mode 100644 app/handlers/errors/BaseErrorHandler.py create mode 100644 app/handlers/errors/__init__.py create mode 100644 app/menus/AbstractMenu.py create mode 100644 app/menus/__init__.py create mode 100644 app/menus/orioks/OrioksAuthFailedMenu.py create mode 100644 app/menus/orioks/__init__.py create mode 100644 app/menus/start/StartMenu.py create mode 100644 app/menus/start/__init__.py create mode 100644 app/middlewares/AdminCommandsMiddleware.py create mode 100644 app/middlewares/UserAgreementMiddleware.py create mode 100644 app/middlewares/UserOrioksAttemptsMiddleware.py create mode 100644 app/middlewares/__init__.py create mode 100644 app/models/BaseModel.py create mode 100644 app/models/__init__.py create mode 100644 app/models/admins/AdminStatistics.py create mode 100644 app/models/admins/__init__.py create mode 100644 app/models/users/UserNotifySettings.py create mode 100644 app/models/users/UserStatus.py create mode 100644 app/models/users/__init__.py delete mode 100644 handlers/admins.py delete mode 100644 handlers/callback_queries.py delete mode 100644 handlers/commands.py delete mode 100644 handlers/errors.py delete mode 100644 handlers/notify_settings.py delete mode 100644 handlers/orioks_auth.py delete mode 100644 handles_register.py delete mode 100644 images/test.py delete mode 100644 main.py delete mode 100644 middlewares.py create mode 100644 run-app.py diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..7579cc8 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,585 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=env,.vscode,venv + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +init-hook="from pylint.config import find_pylintrc; import os, sys; sys.path.append(os.path.dirname(find_pylintrc()))" + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + broad-except, + import-star-module-level, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape, + fixme + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=512 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/answers/menu.py b/answers/menu.py deleted file mode 100644 index fffeb84..0000000 --- a/answers/menu.py +++ /dev/null @@ -1,49 +0,0 @@ -import aiogram.utils.markdown as md - -import db.user_status -import keyboards -from main import bot -import db.user_first_add - - -async def menu_command(chat_id: int, user_id: int): - db.user_first_add.user_first_add_to_db(user_telegram_id=user_id) - if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_id): - await bot.send_message( - chat_id, - md.text( - md.text('Привет!'), - md.text('Этот Бот поможет тебе узнавать об изменениях в твоём ОРИОКС в режиме реального времени.'), - md.text(), - md.text('Ознакомиться с Информацией о проекте: /faq'), - md.text('Ознакомиться с Инструкцией: /manual'), - md.text('Выполнить вход в аккаунт ОРИОКС: /login'), - sep='\n', - ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), - ) - else: - await bot.send_message( - chat_id, - md.text( - md.text('Настроить уведомления: /notifysettings'), - md.text(), - md.text('Ознакомиться с Инструкцией: /manual'), - md.text('Выполнить выход из аккаунта ОРИОКС: /logout'), - sep='\n', - ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Настройка уведомлений') - ) - - -async def menu_if_failed_login(chat_id: int, user_id: int): - if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_id): - await bot.send_message( - chat_id, - md.text( - md.hbold('Ошибка авторизации!'), - md.text('Попробуйте ещё раз: /login'), - sep='\n', - ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация') - ) diff --git a/answers/settings.py b/answers/settings.py deleted file mode 100644 index fae37ba..0000000 --- a/answers/settings.py +++ /dev/null @@ -1,69 +0,0 @@ -import aiogram.utils.markdown as md -from aiogram import types - -import db.notify_settings -from handlers import notify_settings -from main import bot - - -async def send_user_settings(user_id: int, callback_query: types.CallbackQuery = None) -> types.Message: - is_on_off_dict = db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_id) - text = md.text( - md.text( - md.text('📓'), - md.text( - md.hbold('“Обучение”'), - md.text('изменения баллов в накопительно-балльной системе (НБС)'), - sep=': ', - ), - sep=' ', - ), - md.text( - md.text('📰'), - md.text( - md.hbold('“Новости”'), - md.text('публикация общих новостей\n(новости по дисциплинам', md.hitalic('(coming soon))')), - sep=': ', - ), - sep=' ', - ), - md.text( - md.text('📁'), - md.text( - md.hbold('“Ресурсы”'), - md.text('изменения и загрузка файлов по дисциплине', md.hitalic('(coming soon)')), - sep=': ', - ), - sep=' ', - ), - md.text( - md.text('📝'), - md.text( - md.hbold('“Домашние задания”'), - md.text('изменения статусов отправленных работ'), - sep=': ', - ), - sep=' ', - ), - md.text( - md.text('📄'), - md.text( - md.hbold('“Заявки”'), - md.text('изменения статусов заявок на обходной лист, материальную помощь, ' - 'социальную стипендию, копии документов, справки'), - sep=': ', - ), - sep=' ', - ), - sep='\n\n', - ) - if not callback_query: - return await bot.send_message( - user_id, - text=text, - reply_markup=notify_settings.init_notify_settings_inline_btns(is_on_off=is_on_off_dict), - ) - return await callback_query.message.edit_text( - text=text, - reply_markup=notify_settings.init_notify_settings_inline_btns(is_on_off=is_on_off_dict), - ) diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000..65539eb --- /dev/null +++ b/app/__init__.py @@ -0,0 +1,40 @@ +import logging + +from aiogram import Bot, Dispatcher, types +from aiogram.contrib.fsm_storage.memory import MemoryStorage +from aiogram.utils import executor +from sqlalchemy import create_engine +from sqlalchemy.orm import scoped_session, sessionmaker + +import utils.makedirs + +from app.handlers import register_handlers +from app.middlewares import UserAgreementMiddleware, UserOrioksAttemptsMiddleware, AdminCommandsMiddleware +from checking import on_startup + +from config import Config + +import db.admins_statistics + +bot = Bot(token=Config.TELEGRAM_BOT_API_TOKEN, parse_mode=types.ParseMode.HTML) +storage = MemoryStorage() +dispatcher = Dispatcher(bot, storage=storage) + +engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True) +db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) + + +def _settings_before_start() -> None: + register_handlers(dispatcher=dispatcher) + db.admins_statistics.create_and_init_admins_statistics() + dispatcher.middleware.setup(UserAgreementMiddleware()) + dispatcher.middleware.setup(UserOrioksAttemptsMiddleware()) + dispatcher.middleware.setup(AdminCommandsMiddleware()) + utils.makedirs.make_dirs() + pass + + +def run(): + logging.basicConfig(level=logging.INFO) + _settings_before_start() + executor.start_polling(dispatcher, skip_updates=True, on_startup=on_startup.on_startup) diff --git a/forms.py b/app/forms/OrioksAuthForm.py similarity index 75% rename from forms.py rename to app/forms/OrioksAuthForm.py index a5cf4f9..1aca650 100644 --- a/forms.py +++ b/app/forms/OrioksAuthForm.py @@ -1,6 +1,7 @@ from aiogram.dispatcher.filters.state import State, StatesGroup -class Form(StatesGroup): +class OrioksAuthForm(StatesGroup): login = State() password = State() + diff --git a/app/forms/__init__.py b/app/forms/__init__.py new file mode 100644 index 0000000..67139b3 --- /dev/null +++ b/app/forms/__init__.py @@ -0,0 +1,3 @@ +from .OrioksAuthForm import OrioksAuthForm + +__all__ = ['OrioksAuthForm'] diff --git a/app/handlers/AbstractCallbackHandler.py b/app/handlers/AbstractCallbackHandler.py new file mode 100644 index 0000000..2b60e93 --- /dev/null +++ b/app/handlers/AbstractCallbackHandler.py @@ -0,0 +1,11 @@ +from abc import abstractmethod + +from aiogram import types + + +class AbstractCallbackHandler: + + @staticmethod + @abstractmethod + async def process(callback_query: types.CallbackQuery, *args, **kwargs): + raise NotImplementedError() diff --git a/app/handlers/AbstractCommandHandler.py b/app/handlers/AbstractCommandHandler.py new file mode 100644 index 0000000..62fb6ac --- /dev/null +++ b/app/handlers/AbstractCommandHandler.py @@ -0,0 +1,11 @@ +from abc import abstractmethod + +from aiogram import types + + +class AbstractCommandHandler: + + @staticmethod + @abstractmethod + async def process(message: types.Message, *args, **kwargs): + raise NotImplementedError() diff --git a/app/handlers/AbstractErrorHandler.py b/app/handlers/AbstractErrorHandler.py new file mode 100644 index 0000000..4e94214 --- /dev/null +++ b/app/handlers/AbstractErrorHandler.py @@ -0,0 +1,11 @@ +from abc import abstractmethod + +from aiogram import types + + +class AbstractErrorHandler: + + @staticmethod + @abstractmethod + async def process(update: types.Update, exception): + raise NotImplementedError diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py new file mode 100644 index 0000000..3b80cd5 --- /dev/null +++ b/app/handlers/__init__.py @@ -0,0 +1,65 @@ +from typing import Type + +from aiogram import Dispatcher + +from config import Config + +from .AbstractCommandHandler import AbstractCommandHandler +from .AbstractCallbackHandler import AbstractCallbackHandler + +__all__ = ['AbstractCommandHandler', 'AbstractCallbackHandler'] + +from .errors import BaseErrorHandler +from ..forms import OrioksAuthForm + + +def register_handlers(dispatcher: Dispatcher) -> None: + from .commands.general import StartCommandHandler, ManualCommandHandler, FAQCommandHandler + from .commands.orioks import OrioksAuthStartCommandHandler, OrioksLogoutCommandHandler, \ + OrioksAuthCancelCommandHandler, OrioksAuthInputLoginCommandHandler, OrioksAuthInputPasswordCommandHandler + from .commands.settings import NotificationSettingsCommandHandler + from .commands.admins import AdminStatisticsCommandHandler + from .callbacks import UserAgreementCallbackHandler, SettingsCallbackHandler + + # General commands + _register_message_handler(dispatcher, StartCommandHandler, text=['Меню'], commands=['start']) + _register_message_handler(dispatcher, ManualCommandHandler, text=['Руководство'], commands=['manual']) + _register_message_handler(dispatcher, FAQCommandHandler, text=['О проекте'], commands=['faq']) + + # Orioks commands + _register_message_handler(dispatcher, OrioksAuthStartCommandHandler, text=['Авторизация'], commands=['login']) + _register_message_handler(dispatcher, OrioksAuthCancelCommandHandler, commands=['cancel'], state='*') + _register_message_handler(dispatcher, OrioksAuthInputLoginCommandHandler, state=OrioksAuthForm.login) + _register_message_handler(dispatcher, OrioksAuthInputPasswordCommandHandler, state=OrioksAuthForm.password) + _register_message_handler(dispatcher, OrioksLogoutCommandHandler, commands=['logout']) + + # Settings commands + _register_message_handler(dispatcher, NotificationSettingsCommandHandler, text=['Настройка уведомлений'], commands=['notifysettings']) + + # Admin commands + _register_message_handler(dispatcher, AdminStatisticsCommandHandler, commands=['stat']) + + # Callbacks + dispatcher.register_callback_query_handler( + UserAgreementCallbackHandler.process, lambda c: c.data == 'button_user_agreement_accept' + ) + dispatcher.register_callback_query_handler( + SettingsCallbackHandler.process, lambda c: c.data in Config.notify_settings_btns + ) + + # Errors + dispatcher.register_errors_handler(BaseErrorHandler.process, exception=Exception) + + +def _register_message_handler( + dispatcher_: Dispatcher, handler_class: Type[AbstractCommandHandler], + text: list = None, commands: list = None, state=None +): + if text is not None: + dispatcher_.register_message_handler(handler_class.process, text=text, state=state) + + if commands is not None: + dispatcher_.register_message_handler(handler_class.process, commands=commands, state=state) + + if text is None and commands is None and state is not None: + dispatcher_.register_message_handler(handler_class.process, state=state) diff --git a/app/handlers/callbacks/SettingsCallbackHandler.py b/app/handlers/callbacks/SettingsCallbackHandler.py new file mode 100644 index 0000000..5a77e81 --- /dev/null +++ b/app/handlers/callbacks/SettingsCallbackHandler.py @@ -0,0 +1,27 @@ +from aiogram import types + +import app +import db.notify_settings +from app.handlers import AbstractCallbackHandler +from app.handlers.commands.settings import NotificationSettingsCommandHandler + + +class SettingsCallbackHandler(AbstractCallbackHandler): + + @staticmethod + async def process(callback_query: types.CallbackQuery, *args, **kwargs): + _row_name = callback_query.data.split('-')[1] + if callback_query.data in ['notify_settings-discipline_sources']: + return await app.bot.answer_callback_query( + callback_query.id, + text='Эта категория ещё недоступна.', show_alert=True + ) + db.notify_settings.update_user_notify_settings( + user_telegram_id=callback_query.from_user.id, + row_name=_row_name, + to_value=not db.notify_settings.get_user_notify_settings_to_dict( + user_telegram_id=callback_query.from_user.id)[_row_name], + ) + await NotificationSettingsCommandHandler.send_user_settings( + user_id=callback_query.from_user.id, callback_query=callback_query + ) diff --git a/app/handlers/callbacks/UserAgreementCallbackHandler.py b/app/handlers/callbacks/UserAgreementCallbackHandler.py new file mode 100644 index 0000000..d5a8f9d --- /dev/null +++ b/app/handlers/callbacks/UserAgreementCallbackHandler.py @@ -0,0 +1,22 @@ +from aiogram import types +import db.user_status +import app +from app.handlers import AbstractCallbackHandler +from app.menus.start import StartMenu + + +class UserAgreementCallbackHandler(AbstractCallbackHandler): + + @staticmethod + async def process(callback_query: types.CallbackQuery, *args, **kwargs): + if db.user_status.get_user_agreement_status(user_telegram_id=callback_query.from_user.id): + return await app.bot.answer_callback_query( + callback_query.id, + text='Пользовательское соглашение уже принято.', show_alert=True) + db.user_status.update_user_agreement_status( + user_telegram_id=callback_query.from_user.id, + is_user_agreement_accepted=True + ) + await app.bot.answer_callback_query(callback_query.id) + answer_message = await app.bot.send_message(callback_query.from_user.id, 'Пользовательское соглашение принято!') + await StartMenu.show(chat_id=answer_message.chat.id, telegram_user_id=callback_query.from_user.id) diff --git a/app/handlers/callbacks/__init__.py b/app/handlers/callbacks/__init__.py new file mode 100644 index 0000000..32f804c --- /dev/null +++ b/app/handlers/callbacks/__init__.py @@ -0,0 +1,4 @@ +from .UserAgreementCallbackHandler import UserAgreementCallbackHandler +from .SettingsCallbackHandler import SettingsCallbackHandler + +__all__ = ['UserAgreementCallbackHandler', 'SettingsCallbackHandler'] diff --git a/handlers/__init__.py b/app/handlers/commands/__init__.py similarity index 100% rename from handlers/__init__.py rename to app/handlers/commands/__init__.py diff --git a/app/handlers/commands/admins/AdminStatisticsCommandHandler.py b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py new file mode 100644 index 0000000..eb8820b --- /dev/null +++ b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py @@ -0,0 +1,56 @@ +from aiogram import types +from aiogram.utils import markdown + +from app.handlers import AbstractCommandHandler + +import db.admins_statistics +from app.handlers.commands.settings import NotificationSettingsCommandHandler +from config import Config + + +class AdminStatisticsCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + msg = '' + for key, value in db.admins_statistics.select_count_user_status_statistics().items(): + msg += markdown.text( + markdown.text(key), + markdown.text(value), + sep=': ', + ) + '\n' + msg += '\n' + + for category in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): + msg += markdown.text( + markdown.text(NotificationSettingsCommandHandler.notify_settings_names_to_vars[category]), + markdown.text(db.admins_statistics.select_count_notify_settings_row_name(row_name=category)), + sep=': ', + ) + '\n' + msg += '\n' + + for key, value in db.admins_statistics.select_all_from_admins_statistics().items(): + msg += markdown.text( + markdown.text(key), + markdown.text(value), + sep=': ', + ) + '\n' + + requests_wave_time = ( + db.admins_statistics.select_count_notify_settings_row_name(row_name='marks') + + 2 + # marks category + db.admins_statistics.select_count_notify_settings_row_name( + row_name='discipline_sources') + + db.admins_statistics.select_count_notify_settings_row_name(row_name='homeworks') + + db.admins_statistics.select_count_notify_settings_row_name(row_name='requests') * 3 + ) * Config.ORIOKS_SECONDS_BETWEEN_REQUESTS / 60 + msg += markdown.text( + markdown.text('Примерное время выполнения одной волны запросов'), + markdown.text( + markdown.text(round(requests_wave_time, 2)), + markdown.text('минут'), + sep=' ', + ), + sep=': ', + ) + '\n' + await message.reply(msg) diff --git a/app/handlers/commands/admins/__init__.py b/app/handlers/commands/admins/__init__.py new file mode 100644 index 0000000..45d9dca --- /dev/null +++ b/app/handlers/commands/admins/__init__.py @@ -0,0 +1,3 @@ +from .AdminStatisticsCommandHandler import AdminStatisticsCommandHandler + +__all__ = ['AdminStatisticsCommandHandler'] diff --git a/app/handlers/commands/general/FAQCommandHandler.py b/app/handlers/commands/general/FAQCommandHandler.py new file mode 100644 index 0000000..706d773 --- /dev/null +++ b/app/handlers/commands/general/FAQCommandHandler.py @@ -0,0 +1,16 @@ +from aiogram import types +from aiogram.utils import markdown + +from app.handlers import AbstractCommandHandler + + +class FAQCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + await message.reply( + markdown.text( + markdown.text('https://orioks-monitoring.github.io/bot/faq'), + ), + disable_web_page_preview=True, + ) diff --git a/app/handlers/commands/general/ManualCommandHandler.py b/app/handlers/commands/general/ManualCommandHandler.py new file mode 100644 index 0000000..c3b59b7 --- /dev/null +++ b/app/handlers/commands/general/ManualCommandHandler.py @@ -0,0 +1,16 @@ +from aiogram import types +from aiogram.utils import markdown + +from app.handlers import AbstractCommandHandler + + +class ManualCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + await message.reply( + markdown.text( + markdown.text('https://orioks-monitoring.github.io/bot/documentation'), + ), + disable_web_page_preview=True, + ) diff --git a/app/handlers/commands/general/StartCommandHandler.py b/app/handlers/commands/general/StartCommandHandler.py new file mode 100644 index 0000000..77dcc40 --- /dev/null +++ b/app/handlers/commands/general/StartCommandHandler.py @@ -0,0 +1,11 @@ +from aiogram import types + +from app.handlers import AbstractCommandHandler +from app.menus.start import StartMenu + + +class StartCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs) -> None: + await StartMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) diff --git a/app/handlers/commands/general/__init__.py b/app/handlers/commands/general/__init__.py new file mode 100644 index 0000000..2db80a9 --- /dev/null +++ b/app/handlers/commands/general/__init__.py @@ -0,0 +1,8 @@ +from .FAQCommandHandler import FAQCommandHandler +from .ManualCommandHandler import ManualCommandHandler +from .StartCommandHandler import StartCommandHandler + +__all__ = [ + 'FAQCommandHandler', 'ManualCommandHandler', + 'StartCommandHandler' +] diff --git a/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py new file mode 100644 index 0000000..d369476 --- /dev/null +++ b/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py @@ -0,0 +1,28 @@ +from aiogram import types +from aiogram.utils import markdown + +import keyboards +from app.handlers import AbstractCommandHandler + + +class OrioksAuthCancelCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + state = kwargs.get('state', None) + + current_state = await state.get_state() + if current_state is None: + return + + await state.finish() + await message.reply( + markdown.text( + markdown.hbold('Авторизация отменена.'), + markdown.text( + 'Если ты боишься вводить свои данные, ознакомься со следующей информацией'), + sep='\n', + ), + reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + disable_web_page_preview=True, + ) diff --git a/app/handlers/commands/orioks/OrioksAuthInputLoginCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthInputLoginCommandHandler.py new file mode 100644 index 0000000..4a9a036 --- /dev/null +++ b/app/handlers/commands/orioks/OrioksAuthInputLoginCommandHandler.py @@ -0,0 +1,40 @@ +from aiogram import types +from aiogram.utils import markdown + +from app.forms import OrioksAuthForm +from app.handlers import AbstractCommandHandler + + +class OrioksAuthInputLoginCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + if not message.text.isdigit(): + return await message.reply( + markdown.text( + markdown.text('Логин должен состоять только из цифр.'), + markdown.text('Введи логин (только цифры):'), + sep='\n' + ), + ) + + state = kwargs.get('state', None) + async with state.proxy() as data: + data['login'] = int(message.text) + + await OrioksAuthForm.next() + await message.reply( + markdown.text( + markdown.hbold('Введи пароль ОРИОКС:'), + markdown.text(), + markdown.text( + markdown.hitalic('🔒 Пароль используется только для однократной авторизации'), + markdown.hitalic('Он не хранится на сервере и будет удалён из истории сообщений'), + markdown.text( + 'Узнать подробнее можно здесь'), + sep='. ' + ), + sep='\n', + ), + disable_web_page_preview=True, + ) diff --git a/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py new file mode 100644 index 0000000..037c934 --- /dev/null +++ b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py @@ -0,0 +1,87 @@ +import asyncio + +from aiogram import types +from aiogram.utils import markdown + +import app +from app.forms import OrioksAuthForm +from app.handlers import AbstractCommandHandler +import db.user_status +import db.admins_statistics +import utils.exceptions +import utils.orioks +import utils.handle_orioks_logout +from app.menus.orioks import OrioksAuthFailedMenu +from app.menus.start import StartMenu +from config import Config +from utils.notify_to_user import SendToTelegram + + +class OrioksAuthInputPasswordCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + state = kwargs.get('state', None) + db.user_status.update_inc_user_orioks_attempts(user_telegram_id=message.from_user.id) + if db.user_status.get_user_orioks_attempts( + user_telegram_id=message.from_user.id) > Config.ORIOKS_MAX_LOGIN_TRIES: + return await message.reply( + markdown.text( + markdown.hbold('Ошибка! Ты истратил все попытки входа в аккаунт ОРИОКС.'), + markdown.text(), + markdown.text('Связаться с поддержкой Бота: @orioks_monitoring_support'), + sep='\n', + ) + ) + await OrioksAuthForm.next() + await state.update_data(password=message.text) + if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): + await state.finish() + await app.bot.delete_message(message.chat.id, message.message_id) + return await app.bot.send_message( + chat_id=message.chat.id, + text=markdown.text('Авторизация уже выполнена') + ) + async with state.proxy() as data: + sticker_message = await app.bot.send_sticker( + message.chat.id, + Config.TELEGRAM_STICKER_LOADER, + ) + try: + await utils.orioks.orioks_login_save_cookies(user_login=data['login'], + user_password=data['password'], + user_telegram_id=message.from_user.id) + db.user_status.update_user_orioks_authenticated_status( + user_telegram_id=message.from_user.id, + is_user_orioks_authenticated=True + ) + await StartMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) + await app.bot.send_message( + message.chat.id, + markdown.text( + markdown.text('Вход в аккаунт ОРИОКС выполнен!') + ) + ) + db.admins_statistics.update_inc_admins_statistics_row_name( + row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_success_logins + ) + except utils.exceptions.OrioksInvalidLoginCredsError: + db.admins_statistics.update_inc_admins_statistics_row_name( + row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_failed_logins + ) + await OrioksAuthFailedMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) + except (asyncio.TimeoutError, TypeError): + await app.bot.send_message( + chat_id=message.chat.id, + text=markdown.text( + markdown.hbold('🔧 Сервер ОРИОКС в данный момент недоступен!'), + markdown.text('Пожалуйста, попробуй ещё раз через 15 минут.'), + sep='\n', + ) + ) + await SendToTelegram.message_to_admins(message='Сервер ОРИОКС не отвечает') + await OrioksAuthFailedMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) + await app.bot.delete_message(message.chat.id, message.message_id) + await state.finish() + + await app.bot.delete_message(sticker_message.chat.id, sticker_message.message_id) diff --git a/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py new file mode 100644 index 0000000..5b305ff --- /dev/null +++ b/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py @@ -0,0 +1,39 @@ +from aiogram import types +from aiogram.utils import markdown + +import app +from app.forms import OrioksAuthForm +from app.handlers import AbstractCommandHandler + +import db.user_status +import db.user_first_add + + +class OrioksAuthStartCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): + return await message.reply( + markdown.text( + markdown.hbold('Ты уже выполнил вход в аккаунт ОРИОКС.'), + markdown.text(), + markdown.text('Выполнить выход из аккаунта ОРИОКС: /logout'), + sep='\n', + ) + ) + await OrioksAuthForm.login.set() + await app.bot.send_message( + message.chat.id, + markdown.text( + markdown.text('Я беспокоюсь, мои данные могут быть перехвачены?'), + markdown.text(), + markdown.text('Отменить авторизацию и получить дополнительную информацию:', markdown.hbold('/cancel')), + ), + ) + await message.reply( + markdown.text( + markdown.hbold('🔒 Введи логин ОРИОКС'), + ), + reply_markup=types.ReplyKeyboardRemove() + ) diff --git a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py new file mode 100644 index 0000000..5a8c3be --- /dev/null +++ b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py @@ -0,0 +1,27 @@ +from aiogram import types +from aiogram.utils import markdown + +import keyboards +from app.handlers import AbstractCommandHandler + +import db.user_status +import utils.handle_orioks_logout + + +class OrioksLogoutCommandHandler(AbstractCommandHandler): + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + await message.reply( + markdown.text( + markdown.hbold('Выход из аккаунта ОРИОКС выполнен.'), + markdown.text('Теперь ты НЕ будешь получать уведомления от Бота.'), + sep='\n', + ), + reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + ) + db.user_status.update_user_orioks_authenticated_status( + user_telegram_id=message.from_user.id, + is_user_orioks_authenticated=False + ) + utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=message.from_user.id) diff --git a/app/handlers/commands/orioks/__init__.py b/app/handlers/commands/orioks/__init__.py new file mode 100644 index 0000000..ffa2715 --- /dev/null +++ b/app/handlers/commands/orioks/__init__.py @@ -0,0 +1,11 @@ +from .OrioksAuthStartCommandHandler import OrioksAuthStartCommandHandler +from .OrioksAuthCancelCommandHandler import OrioksAuthCancelCommandHandler +from .OrioksAuthInputLoginCommandHandler import OrioksAuthInputLoginCommandHandler +from .OrioksAuthInputPasswordCommandHandler import OrioksAuthInputPasswordCommandHandler +from .OrioksLogoutCommandHandler import OrioksLogoutCommandHandler + +__all__ = [ + 'OrioksAuthStartCommandHandler', 'OrioksAuthCancelCommandHandler', + 'OrioksAuthInputLoginCommandHandler', 'OrioksAuthInputPasswordCommandHandler', + 'OrioksLogoutCommandHandler' +] diff --git a/app/handlers/commands/settings/NotificationSettingsCommandHandler.py b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py new file mode 100644 index 0000000..66a5d5c --- /dev/null +++ b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py @@ -0,0 +1,129 @@ +from aiogram import types +from aiogram.utils import markdown + +import app +from app.handlers import AbstractCommandHandler +import db.notify_settings + + +class NotificationSettingsCommandHandler(AbstractCommandHandler): + + notify_settings_names_to_vars = { + 'marks': 'Оценки', + 'news': 'Новости', + 'discipline_sources': 'Ресурсы', + 'homeworks': 'Домашние задания', + 'requests': 'Заявки', + } + + @staticmethod + def _get_section_name_with_status(section_name: str, is_on_off: dict) -> str: + emoji = '🔔' if is_on_off[section_name] else '❌' + return f'{emoji} {NotificationSettingsCommandHandler.notify_settings_names_to_vars[section_name]}' + + @staticmethod + def init_notify_settings_inline_btns(is_on_off: dict) -> types.InlineKeyboardMarkup: + """ + is_on_off = { + 'Обучение': False, + 'Новости': False, + 'Ресурсы': False, + 'Домашние задания': False, + 'Заявки': False, + } + """ + inline_kb_full: types.InlineKeyboardMarkup = types.InlineKeyboardMarkup(row_width=1) + inline_kb_full.add( + types.InlineKeyboardButton( + NotificationSettingsCommandHandler._get_section_name_with_status('marks', is_on_off), + callback_data='notify_settings-marks' + ), + types.InlineKeyboardButton( + NotificationSettingsCommandHandler._get_section_name_with_status('news', is_on_off), + callback_data='notify_settings-news' + ), + types.InlineKeyboardButton( + NotificationSettingsCommandHandler._get_section_name_with_status('discipline_sources', is_on_off), + callback_data='notify_settings-discipline_sources' + ), + types.InlineKeyboardButton( + NotificationSettingsCommandHandler._get_section_name_with_status('homeworks', is_on_off), + callback_data='notify_settings-homeworks' + ), + types.InlineKeyboardButton( + NotificationSettingsCommandHandler._get_section_name_with_status('requests', is_on_off), + callback_data='notify_settings-requests' + ) + ) + return inline_kb_full + + @staticmethod + async def process(message: types.Message, *args, **kwargs): + await NotificationSettingsCommandHandler.send_user_settings(message.from_user.id, callback_query=None) + + @staticmethod + async def send_user_settings(user_id: int, callback_query: types.CallbackQuery = None) -> types.Message: + is_on_off_dict = db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_id) + text = markdown.text( + markdown.text( + markdown.text('📓'), + markdown.text( + markdown.hbold('“Обучение”'), + markdown.text('изменения баллов в накопительно-балльной системе (НБС)'), + sep=': ', + ), + sep=' ', + ), + markdown.text( + markdown.text('📰'), + markdown.text( + markdown.hbold('“Новости”'), + markdown.text('публикация общих новостей\n(новости по дисциплинам', markdown.hitalic('(coming soon))')), + sep=': ', + ), + sep=' ', + ), + markdown.text( + markdown.text('📁'), + markdown.text( + markdown.hbold('“Ресурсы”'), + markdown.text('изменения и загрузка файлов по дисциплине', markdown.hitalic('(coming soon)')), + sep=': ', + ), + sep=' ', + ), + markdown.text( + markdown.text('📝'), + markdown.text( + markdown.hbold('“Домашние задания”'), + markdown.text('изменения статусов отправленных работ'), + sep=': ', + ), + sep=' ', + ), + markdown.text( + markdown.text('📄'), + markdown.text( + markdown.hbold('“Заявки”'), + markdown.text('изменения статусов заявок на обходной лист, материальную помощь, ' + 'социальную стипендию, копии документов, справки'), + sep=': ', + ), + sep=' ', + ), + sep='\n\n', + ) + if not callback_query: + return await app.bot.send_message( + user_id, + text=text, + reply_markup=NotificationSettingsCommandHandler.init_notify_settings_inline_btns( + is_on_off=is_on_off_dict + ), + ) + return await callback_query.message.edit_text( + text=text, + reply_markup=NotificationSettingsCommandHandler.init_notify_settings_inline_btns( + is_on_off=is_on_off_dict + ), + ) diff --git a/app/handlers/commands/settings/__init__.py b/app/handlers/commands/settings/__init__.py new file mode 100644 index 0000000..1a5dec3 --- /dev/null +++ b/app/handlers/commands/settings/__init__.py @@ -0,0 +1,3 @@ +from .NotificationSettingsCommandHandler import NotificationSettingsCommandHandler + +__all__ = ['NotificationSettingsCommandHandler'] diff --git a/app/handlers/errors/BaseErrorHandler.py b/app/handlers/errors/BaseErrorHandler.py new file mode 100644 index 0000000..df2a92b --- /dev/null +++ b/app/handlers/errors/BaseErrorHandler.py @@ -0,0 +1,24 @@ +import logging + +from aiogram import types +from aiogram.utils.exceptions import MessageNotModified, CantParseEntities, TelegramAPIError + +from app.handlers.AbstractErrorHandler import AbstractErrorHandler +from utils.notify_to_user import SendToTelegram + + +class BaseErrorHandler(AbstractErrorHandler): + + @staticmethod + async def process(update: types.Update, exception): + if isinstance(exception, MessageNotModified): + pass + + if isinstance(exception, CantParseEntities): + pass + + if isinstance(exception, TelegramAPIError): + pass + + await SendToTelegram.message_to_admins(message=f'Update: {update} \n{exception}') + logging.exception(f'Update: {update} \n{exception}') diff --git a/app/handlers/errors/__init__.py b/app/handlers/errors/__init__.py new file mode 100644 index 0000000..bef9a7b --- /dev/null +++ b/app/handlers/errors/__init__.py @@ -0,0 +1,3 @@ +from .BaseErrorHandler import BaseErrorHandler + +__all__ = ['BaseErrorHandler'] \ No newline at end of file diff --git a/app/menus/AbstractMenu.py b/app/menus/AbstractMenu.py new file mode 100644 index 0000000..070a3af --- /dev/null +++ b/app/menus/AbstractMenu.py @@ -0,0 +1,9 @@ +from abc import abstractmethod + + +class AbstractMenu: + + @staticmethod + @abstractmethod + async def show(chat_id: int, telegram_user_id: int) -> None: + raise NotImplementedError diff --git a/app/menus/__init__.py b/app/menus/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/menus/orioks/OrioksAuthFailedMenu.py b/app/menus/orioks/OrioksAuthFailedMenu.py new file mode 100644 index 0000000..8a99c51 --- /dev/null +++ b/app/menus/orioks/OrioksAuthFailedMenu.py @@ -0,0 +1,23 @@ +from aiogram.utils import markdown + +import app +import keyboards +from app.menus.AbstractMenu import AbstractMenu + +import db.user_status + + +class OrioksAuthFailedMenu(AbstractMenu): + + @staticmethod + async def show(chat_id: int, telegram_user_id: int) -> None: + if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=telegram_user_id): + await app.bot.send_message( + chat_id, + markdown.text( + markdown.hbold('Ошибка авторизации!'), + markdown.text('Попробуйте ещё раз: /login'), + sep='\n', + ), + reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация') + ) diff --git a/app/menus/orioks/__init__.py b/app/menus/orioks/__init__.py new file mode 100644 index 0000000..b8ff8c6 --- /dev/null +++ b/app/menus/orioks/__init__.py @@ -0,0 +1,3 @@ +from .OrioksAuthFailedMenu import OrioksAuthFailedMenu + +__all__ = ['OrioksAuthFailedMenu'] diff --git a/app/menus/start/StartMenu.py b/app/menus/start/StartMenu.py new file mode 100644 index 0000000..5fc0b52 --- /dev/null +++ b/app/menus/start/StartMenu.py @@ -0,0 +1,41 @@ +from aiogram.utils import markdown + +import app +from app.menus.AbstractMenu import AbstractMenu + +import db.user_status +import keyboards +import db.user_first_add + + +class StartMenu(AbstractMenu): + + @staticmethod + async def show(chat_id: int, telegram_user_id: int) -> None: + db.user_first_add.user_first_add_to_db(user_telegram_id=telegram_user_id) + if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=telegram_user_id): + await app.bot.send_message( + chat_id, + markdown.text( + markdown.text('Привет!'), + markdown.text('Этот Бот поможет тебе узнавать об изменениях в твоём ОРИОКС в режиме реального времени.'), + markdown.text(), + markdown.text('Ознакомиться с Информацией о проекте: /faq'), + markdown.text('Ознакомиться с Инструкцией: /manual'), + markdown.text('Выполнить вход в аккаунт ОРИОКС: /login'), + sep='\n', + ), + reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + ) + else: + await app.bot.send_message( + chat_id, + markdown.text( + markdown.text('Настроить уведомления: /notifysettings'), + markdown.text(), + markdown.text('Ознакомиться с Инструкцией: /manual'), + markdown.text('Выполнить выход из аккаунта ОРИОКС: /logout'), + sep='\n', + ), + reply_markup=keyboards.main_menu_keyboard(first_btn_text='Настройка уведомлений') + ) diff --git a/app/menus/start/__init__.py b/app/menus/start/__init__.py new file mode 100644 index 0000000..ebee60d --- /dev/null +++ b/app/menus/start/__init__.py @@ -0,0 +1 @@ +from .StartMenu import StartMenu diff --git a/app/middlewares/AdminCommandsMiddleware.py b/app/middlewares/AdminCommandsMiddleware.py new file mode 100644 index 0000000..834777e --- /dev/null +++ b/app/middlewares/AdminCommandsMiddleware.py @@ -0,0 +1,15 @@ +from aiogram import types +from aiogram.dispatcher.handler import CancelHandler +from aiogram.dispatcher.middlewares import BaseMiddleware + +from config import Config + + +class AdminCommandsMiddleware(BaseMiddleware): + """ + Middleware, разрешающее использовать команды Админов только пользователям из `config.TELEGRAM_ADMIN_IDS_LIST` + """ + + async def on_process_message(self, message: types.Message, *args, **kwargs): + if message.get_command() in ('/stat',) and message.from_user.id not in Config.TELEGRAM_ADMIN_IDS_LIST: + raise CancelHandler() diff --git a/app/middlewares/UserAgreementMiddleware.py b/app/middlewares/UserAgreementMiddleware.py new file mode 100644 index 0000000..10f91b8 --- /dev/null +++ b/app/middlewares/UserAgreementMiddleware.py @@ -0,0 +1,34 @@ +from aiogram import types +from aiogram.dispatcher.handler import CancelHandler +from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram.utils import markdown + +import db.user_first_add +import db.user_status + + +class UserAgreementMiddleware(BaseMiddleware): + """ + Middleware, блокирующее дальнейшее использование Бота, + если не принято пользовательское соглашение + """ + + inline_btn_user_agreement_accept = types.InlineKeyboardButton( + 'Принять пользовательское соглашение', + callback_data='button_user_agreement_accept' + ) + inline_agreement_accept = types.InlineKeyboardMarkup().add(inline_btn_user_agreement_accept) + + async def on_process_message(self, message: types.Message, *args, **kwargs): + db.user_first_add.user_first_add_to_db(user_telegram_id=message.from_user.id) + if not db.user_status.get_user_agreement_status(user_telegram_id=message.from_user.id): + await message.reply( + markdown.text( + markdown.text('Для получения доступа к Боту, необходимо принять Пользовательское соглашение:'), + markdown.text('https://orioks-monitoring.github.io/bot/rules'), + sep='\n', + ), + reply_markup=self.inline_agreement_accept, + disable_web_page_preview=True, + ) + raise CancelHandler() diff --git a/app/middlewares/UserOrioksAttemptsMiddleware.py b/app/middlewares/UserOrioksAttemptsMiddleware.py new file mode 100644 index 0000000..b7754c0 --- /dev/null +++ b/app/middlewares/UserOrioksAttemptsMiddleware.py @@ -0,0 +1,33 @@ +from aiogram import types +from aiogram.dispatcher.handler import CancelHandler +from aiogram.dispatcher.middlewares import BaseMiddleware +from aiogram.utils import markdown + +from config import Config + +import db.user_first_add +import db.user_status + + +class UserOrioksAttemptsMiddleware(BaseMiddleware): + """ + Middleware, блокирующее дальнейшее использование Бота, + если превышено максимальное количество попыток входа в + аккаунт ОРИОКС + """ + + async def on_process_message(self, message: types.Message, *args, **kwargs): + if db.user_status.get_user_orioks_attempts( + user_telegram_id=message.from_user.id) > Config.ORIOKS_MAX_LOGIN_TRIES: + await message.reply( + markdown.text( + markdown.hbold('Ты совершил подозрительно много попыток входа в аккаунт ОРИОКС.'), + markdown.text('Возможно, ты нарушаешь Пользовательское соглашение, с которым согласился.'), + markdown.text(), + markdown.text('Связаться с поддержкой Бота: @orioks_monitoring_support'), + sep='\n', + ), + reply_markup=types.ReplyKeyboardRemove(), + disable_web_page_preview=True, + ) + raise CancelHandler() diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py new file mode 100644 index 0000000..b4afb7b --- /dev/null +++ b/app/middlewares/__init__.py @@ -0,0 +1,5 @@ +from .AdminCommandsMiddleware import AdminCommandsMiddleware +from .UserAgreementMiddleware import UserAgreementMiddleware +from .UserOrioksAttemptsMiddleware import UserOrioksAttemptsMiddleware + +__all__ = ['AdminCommandsMiddleware', 'UserAgreementMiddleware', 'UserOrioksAttemptsMiddleware'] diff --git a/app/models/BaseModel.py b/app/models/BaseModel.py new file mode 100644 index 0000000..4b443ad --- /dev/null +++ b/app/models/BaseModel.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, DateTime, func, Integer + +from app import db_session +from app.models import DeclarativeModelBase + + +class BaseModel(DeclarativeModelBase): + __abstract__ = True + + id = Column(Integer, primary_key=True) + created_at = Column(DateTime, default=func.now()) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now()) + + @classmethod + def find_one(cls, **query): + return cls.query.filter_by(**query).one_or_none() + + def save(self): + if self.id is None: + db_session.add(self) + db_session.commit() + + def delete(self): + db_session.delete(self) + db_session.commit() + + def as_dict(self): + return {column.key: getattr(self, attr) for attr, column in self.__mapper__.c.items()} diff --git a/app/models/__init__.py b/app/models/__init__.py new file mode 100644 index 0000000..83cf4e5 --- /dev/null +++ b/app/models/__init__.py @@ -0,0 +1,7 @@ +from sqlalchemy.orm import declarative_base + +DeclarativeModelBase = declarative_base() + +from .BaseModel import BaseModel + +__all__ = ['DeclarativeModelBase', 'BaseModel'] diff --git a/app/models/admins/AdminStatistics.py b/app/models/admins/AdminStatistics.py new file mode 100644 index 0000000..ec3c869 --- /dev/null +++ b/app/models/admins/AdminStatistics.py @@ -0,0 +1,10 @@ +from sqlalchemy import Column, Integer + +from app.models import BaseModel + + +class AdminStatistics(BaseModel): + + scheduled_requests = Column(Integer, nullable=False, default=0) + success_logins = Column(Integer, nullable=False, default=0) + failed_logins = Column(Integer, nullable=False, default=0) diff --git a/app/models/admins/__init__.py b/app/models/admins/__init__.py new file mode 100644 index 0000000..178fbeb --- /dev/null +++ b/app/models/admins/__init__.py @@ -0,0 +1,3 @@ +from .AdminStatistics import AdminStatistics + +__all__ = ['AdminStatistics'] diff --git a/app/models/users/UserNotifySettings.py b/app/models/users/UserNotifySettings.py new file mode 100644 index 0000000..55c5683 --- /dev/null +++ b/app/models/users/UserNotifySettings.py @@ -0,0 +1,21 @@ +from sqlalchemy import Column, Integer, Boolean + +from app.models import BaseModel + + +class UserNotifySettings(BaseModel): + + user_telegram_id = Column(Integer, nullable=False) + marks = Column(Boolean, nullable=False, default=True) + news = Column(Boolean, nullable=False, default=True) + discipline_sources = Column(Boolean, nullable=False, default=True) + homeworks = Column(Boolean, nullable=False, default=True) + requests = Column(Boolean, nullable=False, default=True) + + def fill(self, user_telegram_id: int) -> None: + self.user_telegram_id = user_telegram_id + self.marks = True + self.news = True + self.discipline_sources = True + self.homeworks = True + self.requests = True diff --git a/app/models/users/UserStatus.py b/app/models/users/UserStatus.py new file mode 100644 index 0000000..b60bf9b --- /dev/null +++ b/app/models/users/UserStatus.py @@ -0,0 +1,17 @@ +from sqlalchemy import Column, Integer, Boolean + +from app.models import BaseModel + + +class UserStatus(BaseModel): + + user_telegram_id = Column(Integer, nullable=False) + agreement_accepted = Column(Boolean, nullable=False, default=False) + authenticated = Column(Boolean, nullable=False, default=False) + login_attempt_count = Column(Integer, nullable=False, default=0) + + def fill(self, user_telegram_id: int): + self.user_telegram_id = user_telegram_id + self.agreement_accepted = False + self.authenticated = False + self.login_attempt_count = 0 diff --git a/app/models/users/__init__.py b/app/models/users/__init__.py new file mode 100644 index 0000000..fca36a0 --- /dev/null +++ b/app/models/users/__init__.py @@ -0,0 +1,4 @@ +from .UserStatus import UserStatus +from .UserNotifySettings import UserNotifySettings + +__all__ = ['UserStatus', 'UserNotifySettings'] diff --git a/checking/homeworks/get_orioks_homeworks.py b/checking/homeworks/get_orioks_homeworks.py index 538cd52..753d2b4 100644 --- a/checking/homeworks/get_orioks_homeworks.py +++ b/checking/homeworks/get_orioks_homeworks.py @@ -5,7 +5,7 @@ import aiohttp from bs4 import BeautifulSoup -import config +from config import Config from utils import exceptions from utils.delete_file import safe_delete from utils.json_files import JsonFile @@ -28,14 +28,14 @@ def _orioks_parse_homeworks(raw_html: str) -> dict: 'about': { 'discipline': tr.find_all('td')[3].text, 'task': tr.find_all('td')[4].text, - 'url': config.ORIOKS_PAGE_URLS['masks']['homeworks'].format(id=_thread_id), + 'url': Config.ORIOKS_PAGE_URLS['masks']['homeworks'].format(id=_thread_id), }, } return homeworks async def get_orioks_homeworks(session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) + raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) return _orioks_parse_homeworks(raw_html) @@ -117,8 +117,8 @@ def compare(old_dict: dict, new_dict: dict) -> list: async def user_homeworks_check(user_telegram_id: int, session: aiohttp.ClientSession) -> None: - student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'homeworks', student_json_file) + student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'homeworks', student_json_file) try: homeworks_dict = await get_orioks_homeworks(session=session) except exceptions.OrioksCantParseData: diff --git a/checking/marks/get_orioks_marks.py b/checking/marks/get_orioks_marks.py index 9fdf6b6..6f377ef 100644 --- a/checking/marks/get_orioks_marks.py +++ b/checking/marks/get_orioks_marks.py @@ -5,8 +5,8 @@ import aiohttp from bs4 import BeautifulSoup import logging -import config from checking.marks.compares import file_compares, get_discipline_objs_from_diff +from config import Config from utils import exceptions from utils.json_files import JsonFile from utils.notify_to_user import SendToTelegram @@ -98,13 +98,13 @@ def _get_orioks_forang(raw_html: str): async def get_orioks_marks(session: aiohttp.ClientSession): - raw_html = await get_request(url=config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) + raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) return _get_orioks_forang(raw_html) async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession) -> None: - student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'marks', student_json_file) + student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'marks', student_json_file) try: detailed_info = await get_orioks_marks(session=session) except FileNotFoundError: diff --git a/checking/news/get_orioks_news.py b/checking/news/get_orioks_news.py index 015779d..4f9597c 100644 --- a/checking/news/get_orioks_news.py +++ b/checking/news/get_orioks_news.py @@ -5,7 +5,7 @@ import aiohttp from bs4 import BeautifulSoup -import config +from config import Config from utils import exceptions from utils.json_files import JsonFile from utils.make_request import get_request @@ -35,7 +35,7 @@ def _orioks_parse_news(raw_html: str) -> dict: async def get_orioks_news(session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=config.ORIOKS_PAGE_URLS['notify']['news'], session=session) + raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['news'], session=session) return _orioks_parse_news(raw_html) @@ -45,7 +45,7 @@ def _find_in_str_with_beginning_and_ending(string_to_find: str, beginning: str, async def get_news_by_news_id(news_id: int, session: aiohttp.ClientSession) -> NewsObject: - raw_html = await get_request(url=config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) + raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) bs_content = BeautifulSoup(raw_html, "html.parser") well_raw = bs_content.find_all('div', {'class': 'well'})[0] return NewsObject( @@ -53,7 +53,7 @@ async def get_news_by_news_id(news_id: int, session: aiohttp.ClientSession) -> N string_to_find=well_raw.text, beginning='Заголовок:', ending='Тело новости:'), - url=config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), + url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), id=news_id ) @@ -76,8 +76,8 @@ def transform_news_to_msg(news_obj: NewsObject) -> str: async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) -> NewsObject: - student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) + student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) try: last_news_id = await get_orioks_news(session=session) except exceptions.OrioksCantParseData: @@ -89,8 +89,8 @@ async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) async def user_news_check_from_news_id(user_telegram_id: int, session: aiohttp.ClientSession, current_new: NewsObject) -> None: - student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) + student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) last_news_id = {'last_id': current_new.id} if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): await JsonFile.save(data=last_news_id, filename=path_users_to_file) diff --git a/checking/on_startup.py b/checking/on_startup.py index 51df372..f781328 100644 --- a/checking/on_startup.py +++ b/checking/on_startup.py @@ -8,7 +8,6 @@ import aiohttp import aioschedule -import config import db.notify_settings import db.user_status from checking.marks.get_orioks_marks import user_marks_check @@ -17,36 +16,37 @@ from checking.requests.get_orioks_requests import user_requests_check from http.cookies import SimpleCookie import utils +from config import Config from utils import exceptions from utils.notify_to_user import SendToTelegram import utils.delete_file def _get_user_orioks_cookies_from_telegram_id(user_telegram_id: int) -> SimpleCookie: - path_to_cookies = os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl') + path_to_cookies = os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl') return SimpleCookie(pickle.load(open(path_to_cookies, 'rb'))) def _delete_users_tracking_data_in_notify_settings_off(user_telegram_id: int, user_notify_settings: dict) -> None: if not user_notify_settings['marks']: utils.delete_file.safe_delete( - os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') + os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') ) if not user_notify_settings['news']: utils.delete_file.safe_delete( - os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') + os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') ) if not user_notify_settings['discipline_sources']: utils.delete_file.safe_delete(os.path.join( - config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') + Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') ) if not user_notify_settings['homeworks']: utils.delete_file.safe_delete(os.path.join( - config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') + Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') ) if not user_notify_settings['requests']: utils.delete_file.safe_delete(os.path.join( - config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') + Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') ) @@ -55,8 +55,8 @@ async def make_one_user_check(user_telegram_id: int) -> None: cookies = _get_user_orioks_cookies_from_telegram_id(user_telegram_id=user_telegram_id) async with aiohttp.ClientSession( cookies=cookies, - timeout=config.REQUESTS_TIMEOUT, - headers=config.ORIOKS_REQUESTS_HEADERS + timeout=Config.REQUESTS_TIMEOUT, + headers=Config.ORIOKS_REQUESTS_HEADERS ) as session: if user_notify_settings['marks']: await user_marks_check(user_telegram_id=user_telegram_id, session=session) @@ -84,8 +84,8 @@ async def make_all_users_news_check(tries_counter: int = 0) -> list: try: async with aiohttp.ClientSession( cookies=cookies, - timeout=config.REQUESTS_TIMEOUT, - headers=config.ORIOKS_REQUESTS_HEADERS + timeout=Config.REQUESTS_TIMEOUT, + headers=Config.ORIOKS_REQUESTS_HEADERS ) as session: current_new = await get_current_new(user_telegram_id=picked_user_to_check_news, session=session) except exceptions.OrioksCantParseData: @@ -96,7 +96,7 @@ async def make_all_users_news_check(tries_counter: int = 0) -> list: except FileNotFoundError: logging.error(f'(COOKIES) FileNotFoundError: {user_telegram_id}') continue - user_session = aiohttp.ClientSession(cookies=cookies, timeout=config.REQUESTS_TIMEOUT) + user_session = aiohttp.ClientSession(cookies=cookies, timeout=Config.REQUESTS_TIMEOUT) tasks.append(user_news_check_from_news_id( user_telegram_id=user_telegram_id, session=user_session, @@ -131,7 +131,7 @@ async def do_checks(): async def scheduler(): await SendToTelegram.message_to_admins(message='Бот запущен!') - aioschedule.every(config.ORIOKS_SECONDS_BETWEEN_WAVES).seconds.do(do_checks) + aioschedule.every(Config.ORIOKS_SECONDS_BETWEEN_WAVES).seconds.do(do_checks) while True: await aioschedule.run_pending() await asyncio.sleep(1) diff --git a/checking/requests/get_orioks_requests.py b/checking/requests/get_orioks_requests.py index 5de3851..46cdaa6 100644 --- a/checking/requests/get_orioks_requests.py +++ b/checking/requests/get_orioks_requests.py @@ -5,7 +5,7 @@ import aiohttp from bs4 import BeautifulSoup -import config +from config import Config from utils import exceptions from utils.delete_file import safe_delete from utils.json_files import JsonFile @@ -30,14 +30,14 @@ def _orioks_parse_requests(raw_html: str, section: str) -> dict: 'new_messages': int(tr.find_all('td')[new_messages_td_list_index].select_one('b').text), 'about': { 'name': tr.find_all('td')[3].text, - 'url': config.ORIOKS_PAGE_URLS['masks']['requests'][section].format(id=_thread_id), + 'url': Config.ORIOKS_PAGE_URLS['masks']['requests'][section].format(id=_thread_id), }, } return requests async def get_orioks_requests(section: str, session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) + raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) return _orioks_parse_requests(raw_html=raw_html, section=section) @@ -118,8 +118,8 @@ def compare(old_dict: dict, new_dict: dict) -> list: async def _user_requests_check_with_subsection(user_telegram_id: int, section: str, session: aiohttp.ClientSession) -> None: - student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', + student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'requests', section, student_json_file) try: requests_dict = await get_orioks_requests(section=section, session=session) diff --git a/config.py b/config.py index db1428a..8fbb845 100644 --- a/config.py +++ b/config.py @@ -3,62 +3,63 @@ import aiohttp -TELEGRAM_BOT_API_TOKEN = os.getenv('TELEGRAM_BOT_API_TOKEN') +class Config: + TELEGRAM_BOT_API_TOKEN = os.getenv('TELEGRAM_BOT_API_TOKEN') -BASEDIR = os.path.dirname(os.path.abspath(__file__)) -STUDENT_FILE_JSON_MASK = '{id}.json' -PATH_TO_STUDENTS_TRACKING_DATA = os.path.join(BASEDIR, 'users_data', 'tracking_data') + BASEDIR = os.path.dirname(os.path.abspath(__file__)) + STUDENT_FILE_JSON_MASK = '{id}.json' + PATH_TO_STUDENTS_TRACKING_DATA = os.path.join(BASEDIR, 'users_data', 'tracking_data') -PATH_TO_DB = os.path.join(BASEDIR, 'orioks-monitoring_bot.db') -PATH_TO_SQL_FOLDER = os.path.join(BASEDIR, 'db', 'sql') + PATH_TO_DB = os.path.join(BASEDIR, 'orioks-monitoring_bot.db') + PATH_TO_SQL_FOLDER = os.path.join(BASEDIR, 'db', 'sql') -notify_settings_btns = ( - 'notify_settings-marks', - 'notify_settings-news', - 'notify_settings-discipline_sources', - 'notify_settings-homeworks', - 'notify_settings-requests' -) + notify_settings_btns = ( + 'notify_settings-marks', + 'notify_settings-news', + 'notify_settings-discipline_sources', + 'notify_settings-homeworks', + 'notify_settings-requests' + ) -TELEGRAM_ADMIN_IDS_LIST = json.loads(os.environ['TELEGRAM_ADMIN_IDS_LIST']) + TELEGRAM_ADMIN_IDS_LIST = json.loads(os.environ['TELEGRAM_ADMIN_IDS_LIST']) -ORIOKS_MAX_LOGIN_TRIES = 10 + ORIOKS_MAX_LOGIN_TRIES = 10 -TELEGRAM_STICKER_LOADER = 'CAACAgIAAxkBAAEEIlpiLSwO28zurkSJGRj6J9SLBIAHYQACIwADKA9qFCdRJeeMIKQGIwQ' + TELEGRAM_STICKER_LOADER = 'CAACAgIAAxkBAAEEIlpiLSwO28zurkSJGRj6J9SLBIAHYQACIwADKA9qFCdRJeeMIKQGIwQ' -REQUESTS_TIMEOUT = aiohttp.ClientTimeout(total=30) + REQUESTS_TIMEOUT = aiohttp.ClientTimeout(total=30) -ORIOKS_SECONDS_BETWEEN_REQUESTS = 1.5 -ORIOKS_SECONDS_BETWEEN_WAVES = 5 -ORIOKS_REQUESTS_SEMAPHORE_VALUE = 1 -ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE = 1 + ORIOKS_SECONDS_BETWEEN_REQUESTS = 1.5 + ORIOKS_SECONDS_BETWEEN_WAVES = 5 + ORIOKS_REQUESTS_SEMAPHORE_VALUE = 1 + ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE = 1 -ORIOKS_REQUESTS_HEADERS = { - 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', - 'Accept-Encoding': 'gzip, deflate, br', - 'Accept-Language': 'ru-RU,ru;q=0.9', - 'User-Agent': 'orioks_monitoring/1.0 (Linux; aiohttp)' -} + ORIOKS_REQUESTS_HEADERS = { + 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9', + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept-Language': 'ru-RU,ru;q=0.9', + 'User-Agent': 'orioks_monitoring/1.0 (Linux; aiohttp)' + } -ORIOKS_PAGE_URLS = { - 'login': 'https://orioks.miet.ru/user/login', - 'masks': { - 'news': 'https://orioks.miet.ru/main/view-news?id={id}', - 'homeworks': 'https://orioks.miet.ru/student/homework/view?id_thread={id}', - 'requests': { - 'questionnaire': 'https://orioks.miet.ru/request/questionnaire/view?id_thread={id}', # not sure - 'doc': 'https://orioks.miet.ru/request/doc/view?id_thread={id}', # not sure - 'reference': 'https://orioks.miet.ru/request/reference/view?id_thread={id}', - } - }, - 'notify': { - 'marks': 'https://orioks.miet.ru/student/student', - 'news': 'https://orioks.miet.ru', - 'homeworks': 'https://orioks.miet.ru/student/homework/list', - 'requests': { - 'questionnaire': 'https://orioks.miet.ru/request/questionnaire/list?AnketaTreadForm[status]=1,2,4,6,3,5,7&AnketaTreadForm[accept]=-1', - 'doc': 'https://orioks.miet.ru/request/doc/list?DocThreadForm[status]=1,2,4,6,3,5,7&DocThreadForm[type]=0', - 'reference': 'https://orioks.miet.ru/request/reference/list?ReferenceThreadForm[status]=1,2,4,6,3,5,7', + ORIOKS_PAGE_URLS = { + 'login': 'https://orioks.miet.ru/user/login', + 'masks': { + 'news': 'https://orioks.miet.ru/main/view-news?id={id}', + 'homeworks': 'https://orioks.miet.ru/student/homework/view?id_thread={id}', + 'requests': { + 'questionnaire': 'https://orioks.miet.ru/request/questionnaire/view?id_thread={id}', # not sure + 'doc': 'https://orioks.miet.ru/request/doc/view?id_thread={id}', # not sure + 'reference': 'https://orioks.miet.ru/request/reference/view?id_thread={id}', + } + }, + 'notify': { + 'marks': 'https://orioks.miet.ru/student/student', + 'news': 'https://orioks.miet.ru', + 'homeworks': 'https://orioks.miet.ru/student/homework/list', + 'requests': { + 'questionnaire': 'https://orioks.miet.ru/request/questionnaire/list?AnketaTreadForm[status]=1,2,4,6,3,5,7&AnketaTreadForm[accept]=-1', + 'doc': 'https://orioks.miet.ru/request/doc/list?DocThreadForm[status]=1,2,4,6,3,5,7&DocThreadForm[type]=0', + 'reference': 'https://orioks.miet.ru/request/reference/list?ReferenceThreadForm[status]=1,2,4,6,3,5,7', + } } } -} diff --git a/db/admins_statistics.py b/db/admins_statistics.py index f283081..39ec1da 100644 --- a/db/admins_statistics.py +++ b/db/admins_statistics.py @@ -1,8 +1,9 @@ import sqlite3 -import config import os from dataclasses import dataclass +from config import Config + @dataclass class AdminsStatisticsRowNames: @@ -12,13 +13,13 @@ class AdminsStatisticsRowNames: def create_and_init_admins_statistics() -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'create_admins_statistics.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_admins_statistics.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script) - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'init_admins_statistics.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_admins_statistics.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'orioks_scheduled_requests': 0, @@ -30,10 +31,10 @@ def create_and_init_admins_statistics() -> None: def select_all_from_admins_statistics() -> dict: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_admins_statistics.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_from_admins_statistics.sql'), 'r') as sql_file: sql_script = sql_file.read() rows = sql.execute(sql_script).fetchone() db.close() @@ -50,9 +51,9 @@ def update_inc_admins_statistics_row_name(row_name: str) -> None: if row_name not in ('orioks_scheduled_requests', 'orioks_success_logins', 'orioks_failed_logins'): raise Exception('update_inc_admins_statistics_row_name() -> row_name must only be in (' 'orioks_scheduled_requests, orioks_success_logins, orioks_failed_logins)') - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_inc_admins_statistics_row_name.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_inc_admins_statistics_row_name.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script.format(row_name=row_name), { 'row_name': row_name @@ -62,9 +63,9 @@ def update_inc_admins_statistics_row_name(row_name: str) -> None: def select_count_user_status_statistics() -> dict: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_count_user_status_statistics.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_count_user_status_statistics.sql'), 'r') as sql_file: sql_script = sql_file.read() users_agreement_accepted = sql.execute( sql_script.format(row_name='is_user_agreement_accepted'), { @@ -103,9 +104,9 @@ def select_count_notify_settings_row_name(row_name: str) -> int: if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): raise Exception('select_count_notify_settings_row_name() -> row_name must only be in (' 'marks, news, discipline_sources, homeworks, requests)') - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_count_notify_settings_row_name.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_count_notify_settings_row_name.sql'), 'r') as sql_file: sql_script = sql_file.read() count_notify_settings_marks = sql.execute(sql_script.format(row_name=row_name)).fetchone() return int(count_notify_settings_marks[0]) diff --git a/db/notify_settings.py b/db/notify_settings.py index 71dfb54..aee4bb4 100644 --- a/db/notify_settings.py +++ b/db/notify_settings.py @@ -1,12 +1,13 @@ import sqlite3 -import config import os +from config import Config + def get_user_notify_settings_to_dict(user_telegram_id: int) -> dict: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_user_notify_settings.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_from_user_notify_settings.sql'), 'r') as sql_file: sql_script = sql_file.read() raw = sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone() db.close() @@ -26,9 +27,9 @@ def update_user_notify_settings(user_telegram_id: int, row_name: str, to_value: if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): raise Exception('update_user_notify_settings() -> row_name must only be in (' 'marks, news, discipline_sources, homeworks, requests)') - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_set_row_name.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_set_row_name.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script.format(row_name=row_name), { 'to_value': to_value, @@ -39,9 +40,9 @@ def update_user_notify_settings(user_telegram_id: int, row_name: str, to_value: def update_user_notify_settings_reset_to_default(user_telegram_id: int) -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_reset_to_default.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_reset_to_default.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'user_telegram_id': user_telegram_id, @@ -56,9 +57,9 @@ def update_user_notify_settings_reset_to_default(user_telegram_id: int) -> None: def select_all_news_enabled_users() -> set: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_news_enabled_users.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_news_enabled_users.sql'), 'r') as sql_file: sql_script = sql_file.read() result = set() for user in sql.execute(sql_script).fetchall(): diff --git a/db/user_first_add.py b/db/user_first_add.py index 19f9402..9c0ab7f 100644 --- a/db/user_first_add.py +++ b/db/user_first_add.py @@ -1,16 +1,17 @@ -import config import sqlite3 import os +from config import Config + def user_first_add_to_db(user_telegram_id: int) -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'create_user_status.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script) - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'init_user_status.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'user_telegram_id': user_telegram_id, @@ -19,11 +20,11 @@ def user_first_add_to_db(user_telegram_id: int) -> None: 'orioks_login_attempts': 0 }) - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'create_user_notify_settings.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_user_notify_settings.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script) - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'init_user_notify_settings.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_user_notify_settings.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'user_telegram_id': user_telegram_id, diff --git a/db/user_status.py b/db/user_status.py index 808954c..29831cd 100644 --- a/db/user_status.py +++ b/db/user_status.py @@ -1,14 +1,15 @@ import sqlite3 from typing import Set -import config import os +from config import Config + def get_user_agreement_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_agreement_accepted_from_user_status.sql'), + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_is_user_agreement_accepted_from_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() is_user_agreement_accepted = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) @@ -17,9 +18,9 @@ def get_user_agreement_status(user_telegram_id: int) -> bool: def get_user_orioks_authenticated_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_orioks_authenticated_from_user_status.sql'), + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_is_user_orioks_authenticated_from_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() is_user_orioks_authenticated = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) @@ -28,9 +29,9 @@ def get_user_orioks_authenticated_status(user_telegram_id: int) -> bool: def update_user_agreement_status(user_telegram_id: int, is_user_agreement_accepted: bool) -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_agreement_accepted.sql'), + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_agreement_accepted.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { @@ -42,9 +43,9 @@ def update_user_agreement_status(user_telegram_id: int, is_user_agreement_accept def update_user_orioks_authenticated_status(user_telegram_id: int, is_user_orioks_authenticated: bool) -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_orioks_authenticated.sql'), + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_orioks_authenticated.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { @@ -56,9 +57,9 @@ def update_user_orioks_authenticated_status(user_telegram_id: int, is_user_oriok def select_all_orioks_authenticated_users() -> Set[int]: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_orioks_authenticated_users.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_orioks_authenticated_users.sql'), 'r') as sql_file: sql_script = sql_file.read() result = set() for user in sql.execute(sql_script).fetchall(): @@ -68,9 +69,9 @@ def select_all_orioks_authenticated_users() -> Set[int]: def get_user_orioks_attempts(user_telegram_id: int) -> int: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_user_orioks_attempts_from_user_status.sql'), + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_user_orioks_attempts_from_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() attempts = int(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) @@ -79,9 +80,9 @@ def get_user_orioks_attempts(user_telegram_id: int) -> int: def update_inc_user_orioks_attempts(user_telegram_id: int) -> None: - db = sqlite3.connect(config.PATH_TO_DB) + db = sqlite3.connect(Config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_inc_user_orioks_attempts.sql'), 'r') as sql_file: + with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_inc_user_orioks_attempts.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'to_value': int(get_user_orioks_attempts(user_telegram_id=user_telegram_id)) + 1, diff --git a/handlers/admins.py b/handlers/admins.py deleted file mode 100644 index 6f1168e..0000000 --- a/handlers/admins.py +++ /dev/null @@ -1,51 +0,0 @@ -from aiogram import types -import aiogram.utils.markdown as md - -import config -import handlers.notify_settings -import db.admins_statistics -import db.notify_settings - - -async def admin_get_statistics(message: types.Message): - msg = '' - for key, value in db.admins_statistics.select_count_user_status_statistics().items(): - msg += md.text( - md.text(key), - md.text(value), - sep=': ', - ) + '\n' - msg += '\n' - - for category in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): - msg += md.text( - md.text(handlers.notify_settings.notify_settings_names_to_vars[category]), - md.text(db.admins_statistics.select_count_notify_settings_row_name(row_name=category)), - sep=': ', - ) + '\n' - msg += '\n' - - for key, value in db.admins_statistics.select_all_from_admins_statistics().items(): - msg += md.text( - md.text(key), - md.text(value), - sep=': ', - ) + '\n' - - requests_wave_time = ( - db.admins_statistics.select_count_notify_settings_row_name(row_name='marks') + - 2 + # marks category - db.admins_statistics.select_count_notify_settings_row_name(row_name='discipline_sources') + - db.admins_statistics.select_count_notify_settings_row_name(row_name='homeworks') + - db.admins_statistics.select_count_notify_settings_row_name(row_name='requests') * 3 - ) * config.ORIOKS_SECONDS_BETWEEN_REQUESTS / 60 - msg += md.text( - md.text('Примерное время выполнения одной волны запросов'), - md.text( - md.text(round(requests_wave_time, 2)), - md.text('минут'), - sep=' ', - ), - sep=': ', - ) + '\n' - await message.reply(msg) diff --git a/handlers/callback_queries.py b/handlers/callback_queries.py deleted file mode 100644 index 0e31810..0000000 --- a/handlers/callback_queries.py +++ /dev/null @@ -1,36 +0,0 @@ -from main import bot -from aiogram import types -import answers -import db - - -async def callback_query_handler_user_agreement(callback_query: types.CallbackQuery): - """@dp.callback_query_handler(lambda c: c.data == 'button_user_agreement_accept')""" - if db.user_status.get_user_agreement_status(user_telegram_id=callback_query.from_user.id): - return await bot.answer_callback_query( - callback_query.id, - text='Пользовательское соглашение уже принято.', show_alert=True) - db.user_status.update_user_agreement_status( - user_telegram_id=callback_query.from_user.id, - is_user_agreement_accepted=True - ) - await bot.answer_callback_query(callback_query.id) - answer_message = await bot.send_message(callback_query.from_user.id, 'Пользовательское соглашение принято!') - await answers.menu.menu_command(chat_id=answer_message.chat.id, user_id=callback_query.from_user.id) - - -async def callback_query_handler_notify_settings_btns(callback_query: types.CallbackQuery): - """@dp.callback_query_handler(lambda c: c.data in config.notify_settings_btns)""" - _row_name = callback_query.data.split('-')[1] - if callback_query.data in ['notify_settings-discipline_sources']: - return await bot.answer_callback_query( - callback_query.id, - text='Эта категория ещё недоступна.', show_alert=True - ) - db.notify_settings.update_user_notify_settings( - user_telegram_id=callback_query.from_user.id, - row_name=_row_name, - to_value=not db.notify_settings.get_user_notify_settings_to_dict( - user_telegram_id=callback_query.from_user.id)[_row_name], - ) - await answers.settings.send_user_settings(user_id=callback_query.from_user.id, callback_query=callback_query) diff --git a/handlers/commands.py b/handlers/commands.py deleted file mode 100644 index 2b39b7f..0000000 --- a/handlers/commands.py +++ /dev/null @@ -1,38 +0,0 @@ -import aiogram.utils.markdown as md -from aiogram import types - -from answers import menu - - -async def start_cmd_handler(message: types.Message): - """" - @dp.message_handler(text='Меню') - @dp.message_handler(commands='start') - """ - await menu.menu_command(chat_id=message.chat.id, user_id=message.from_user.id) - - -async def msg_manual(message: types.Message): - """ - @dp.message_handler(text='Руководство') - @dp.message_handler(commands='manual') - """ - await message.reply( - md.text( - md.text('https://orioks-monitoring.github.io/bot/documentation'), - ), - disable_web_page_preview=True, - ) - - -async def msg_faq(message: types.Message): - """ - @dp.message_handler(text='О проекте') - @dp.message_handler(commands='faq') - """ - await message.reply( - md.text( - md.text('https://orioks-monitoring.github.io/bot/faq'), - ), - disable_web_page_preview=True, - ) diff --git a/handlers/errors.py b/handlers/errors.py deleted file mode 100644 index 02e5fe5..0000000 --- a/handlers/errors.py +++ /dev/null @@ -1,20 +0,0 @@ -import logging -from aiogram import types -from aiogram.utils.exceptions import (TelegramAPIError, - MessageNotModified, - CantParseEntities) -from utils.notify_to_user import SendToTelegram - - -async def errors_handler(update: types.Update, exception): - if isinstance(exception, MessageNotModified): - pass - - if isinstance(exception, CantParseEntities): - pass - - if isinstance(exception, TelegramAPIError): - pass - - await SendToTelegram.message_to_admins(message=f'Update: {update} \n{exception}') - logging.exception(f'Update: {update} \n{exception}') diff --git a/handlers/notify_settings.py b/handlers/notify_settings.py deleted file mode 100644 index b87d2ef..0000000 --- a/handlers/notify_settings.py +++ /dev/null @@ -1,47 +0,0 @@ -from aiogram import types - -from answers import settings - - -notify_settings_names_to_vars = { - 'marks': 'Оценки', - 'news': 'Новости', - 'discipline_sources': 'Ресурсы', - 'homeworks': 'Домашние задания', - 'requests': 'Заявки', -} - - -def _get_section_name_with_status(section_name: str, is_on_off: dict) -> str: - emoji = '🔔' if is_on_off[section_name] else '❌' - return f'{emoji} {notify_settings_names_to_vars[section_name]}' - - -def init_notify_settings_inline_btns(is_on_off: dict) -> types.InlineKeyboardMarkup: - """ - is_on_off = { - 'Обучение': False, - 'Новости': False, - 'Ресурсы': False, - 'Домашние задания': False, - 'Заявки': False, - } - """ - inline_kb_full: types.InlineKeyboardMarkup = types.InlineKeyboardMarkup(row_width=1) - inline_kb_full.add( - types.InlineKeyboardButton(_get_section_name_with_status('marks', is_on_off), - callback_data='notify_settings-marks'), - types.InlineKeyboardButton(_get_section_name_with_status('news', is_on_off), - callback_data='notify_settings-news'), - types.InlineKeyboardButton(_get_section_name_with_status('discipline_sources', is_on_off), - callback_data='notify_settings-discipline_sources'), - types.InlineKeyboardButton(_get_section_name_with_status('homeworks', is_on_off), - callback_data='notify_settings-homeworks'), - types.InlineKeyboardButton(_get_section_name_with_status('requests', is_on_off), - callback_data='notify_settings-requests') - ) - return inline_kb_full - - -async def user_settings(message: types.Message): - await settings.send_user_settings(user_id=message.from_user.id, callback_query=None) diff --git a/handlers/orioks_auth.py b/handlers/orioks_auth.py deleted file mode 100644 index 19a203e..0000000 --- a/handlers/orioks_auth.py +++ /dev/null @@ -1,189 +0,0 @@ -import asyncio - -import aiogram.utils.markdown as md -from aiogram import types -from aiogram.dispatcher import FSMContext - -import config -import db.user_status -import db.admins_statistics -import keyboards -import utils.exceptions -import utils.orioks -import utils.handle_orioks_logout -from answers import menu -from forms import Form -from main import bot -from utils.notify_to_user import SendToTelegram - - -async def cmd_start(message: types.Message): - """ - @dp.message_handler(text='Авторизация') - @dp.message_handler(commands='login') - """ - if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): - return await message.reply( - md.text( - md.hbold('Ты уже выполнил вход в аккаунт ОРИОКС.'), - md.text(), - md.text('Выполнить выход из аккаунта ОРИОКС: /logout'), - sep='\n', - ) - ) - await Form.login.set() - await bot.send_message( - message.chat.id, - md.text( - md.text('Я беспокоюсь, мои данные могут быть перехвачены?'), - md.text(), - md.text('Отменить авторизацию и получить дополнительную информацию:', md.hbold('/cancel')), - ), - ) - await message.reply( - md.text( - md.hbold('🔒 Введи логин ОРИОКС'), - ), - reply_markup=types.ReplyKeyboardRemove() - ) - - -async def cancel_handler(message: types.Message, state: FSMContext): - """ - @dp.message_handler(state='*', commands='cancel') - """ - current_state = await state.get_state() - if current_state is None: - return - - await state.finish() - await message.reply( - md.text( - md.hbold('Авторизация отменена.'), - md.text('Если ты боишься вводить свои данные, ознакомься со следующей информацией'), - sep='\n', - ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), - disable_web_page_preview=True, - ) - - -async def process_login_invalid(message: types.Message): - """ - @dp.message_handler(lambda message: not message.text.isdigit(), state=Form.login) - """ - return await message.reply( - md.text( - md.text('Логин должен состоять только из цифр.'), - md.text('Введи логин (только цифры):'), - sep='\n' - ), - ) - - -async def process_login(message: types.Message, state: FSMContext): - """ - @dp.message_handler(state=Form.login) - """ - async with state.proxy() as data: - data['login'] = int(message.text) - - await Form.next() - await message.reply( - md.text( - md.hbold('Введи пароль ОРИОКС:'), - md.text(), - md.text( - md.hitalic('🔒 Пароль используется только для однократной авторизации'), - md.hitalic('Он не хранится на сервере и будет удалён из истории сообщений'), - md.text('Узнать подробнее можно здесь'), - sep='. ' - ), - sep='\n', - ), - disable_web_page_preview=True, - ) - - -async def process_password(message: types.Message, state: FSMContext): - """ - @dp.message_handler(state=Form.password) - """ - db.user_status.update_inc_user_orioks_attempts(user_telegram_id=message.from_user.id) - if db.user_status.get_user_orioks_attempts(user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: - return await message.reply( - md.text( - md.hbold('Ошибка! Ты истратил все попытки входа в аккаунт ОРИОКС.'), - md.text(), - md.text('Связаться с поддержкой Бота: @orioks_monitoring_support'), - sep='\n', - ) - ) - await Form.next() - await state.update_data(password=message.text) - if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): - await state.finish() - await bot.delete_message(message.chat.id, message.message_id) - return await bot.send_message( - chat_id=message.chat.id, - text=md.text('Авторизация уже выполнена') - ) - async with state.proxy() as data: - sticker_message = await bot.send_sticker( - message.chat.id, - config.TELEGRAM_STICKER_LOADER, - ) - try: - await utils.orioks.orioks_login_save_cookies(user_login=data['login'], - user_password=data['password'], - user_telegram_id=message.from_user.id) - db.user_status.update_user_orioks_authenticated_status( - user_telegram_id=message.from_user.id, - is_user_orioks_authenticated=True - ) - await menu.menu_command(chat_id=message.chat.id, user_id=message.from_user.id) - await bot.send_message( - message.chat.id, - md.text( - md.text('Вход в аккаунт ОРИОКС выполнен!') - ) - ) - db.admins_statistics.update_inc_admins_statistics_row_name( - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_success_logins - ) - except utils.exceptions.OrioksInvalidLoginCredsError: - db.admins_statistics.update_inc_admins_statistics_row_name( - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_failed_logins - ) - await menu.menu_if_failed_login(chat_id=message.chat.id, user_id=message.from_user.id) - except (asyncio.TimeoutError, TypeError) as e: - await bot.send_message( - chat_id=message.chat.id, - text=md.text( - md.hbold('🔧 Сервер ОРИОКС в данный момент недоступен!'), - md.text('Пожалуйста, попробуй ещё раз через 15 минут.'), - sep='\n', - ) - ) - await SendToTelegram.message_to_admins(message='Сервер ОРИОКС не отвечает') - await menu.menu_if_failed_login(chat_id=message.chat.id, user_id=message.from_user.id) - await bot.delete_message(message.chat.id, message.message_id) - await state.finish() - - await bot.delete_message(sticker_message.chat.id, sticker_message.message_id) - - -async def orioks_logout(message: types.Message): - await message.reply( - md.text( - md.hbold('Выход из аккаунта ОРИОКС выполнен.'), - md.text('Теперь ты НЕ будешь получать уведомления от Бота.'), - sep='\n', - ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), - ) - db.user_status.update_user_orioks_authenticated_status( - user_telegram_id=message.from_user.id, - is_user_orioks_authenticated=False - ) - utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=message.from_user.id) diff --git a/handles_register.py b/handles_register.py deleted file mode 100644 index 6dc7211..0000000 --- a/handles_register.py +++ /dev/null @@ -1,51 +0,0 @@ -import config -from forms import Form -from handlers import commands, orioks_auth, notify_settings, admins, callback_queries, errors - - -def handles_register(dp): - """commands""" - dp.register_message_handler(commands.start_cmd_handler, text=['Меню']) - dp.register_message_handler(commands.start_cmd_handler, commands=['start']) - - dp.register_message_handler(commands.msg_manual, text=['Руководство']) - dp.register_message_handler(commands.msg_manual, commands=['manual']) - - dp.register_message_handler(commands.msg_faq, text=['О проекте']) - dp.register_message_handler(commands.msg_faq, commands=['faq']) - - """orioks_auth""" - dp.register_message_handler(orioks_auth.cmd_start, text=['Авторизация']) - dp.register_message_handler(orioks_auth.cmd_start, commands=['login']) - - - dp.register_message_handler(orioks_auth.orioks_logout, commands=['logout']) - - dp.register_message_handler(orioks_auth.cancel_handler, commands=['cancel'], state='*') - - dp.register_message_handler(orioks_auth.process_login_invalid, lambda message: not message.text.isdigit(), - state=Form.login) - - dp.register_message_handler(orioks_auth.process_login, state=Form.login) - - dp.register_message_handler(orioks_auth.process_password, state=Form.password) - - """notify settings""" - dp.register_message_handler(notify_settings.user_settings, text=['Настройка уведомлений']) - dp.register_message_handler(notify_settings.user_settings, commands=['notifysettings']) - - """admins""" - dp.register_message_handler(admins.admin_get_statistics, commands=['stat']) - - """callback queries""" - dp.register_callback_query_handler( - callback_queries.callback_query_handler_user_agreement, - lambda c: c.data == 'button_user_agreement_accept' - ) - dp.register_callback_query_handler( - callback_queries.callback_query_handler_notify_settings_btns, - lambda c: c.data in config.notify_settings_btns - ) - - """errors""" - dp.register_errors_handler(errors.errors_handler, exception=Exception) diff --git a/images/imager.py b/images/imager.py index 0e30cdf..48384a1 100644 --- a/images/imager.py +++ b/images/imager.py @@ -4,9 +4,10 @@ from PIL import Image, ImageDraw, ImageFont from typing import NamedTuple import pathlib -import config import secrets +from config import Config + class PathToImages(NamedTuple): one: pathlib.Path @@ -19,7 +20,7 @@ class PathToImages(NamedTuple): class Imager: def __init__(self): - self._base_dir = os.path.join(config.BASEDIR, 'images', 'source') + self._base_dir = os.path.join(Config.BASEDIR, 'images', 'source') self._font_path = os.path.join(self._base_dir, 'PTSansCaption-Bold.ttf') self._font_upper_size = 64 @@ -184,7 +185,7 @@ def get_image_marks( self._get_image_by_grade(current_grade, max_grade) self._calculate_font_size_and_text_width(title_text, side_text, mark_change_text=mark_change_text) self._draw_text_marks(title_text, mark_change_text, side_text) - path_to_result_image = pathlib.Path(os.path.join(config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) + path_to_result_image = pathlib.Path(os.path.join(Config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) self.image.save(path_to_result_image) return path_to_result_image @@ -194,7 +195,7 @@ def get_image_news( side_text: str, url: str ) -> pathlib.Path: - path_to_result_image = pathlib.Path(os.path.join(config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) + path_to_result_image = pathlib.Path(os.path.join(Config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) self._get_news_image() if title_text == '': self.image.save(path_to_result_image) diff --git a/images/test.py b/images/test.py deleted file mode 100644 index 134d9b6..0000000 --- a/images/test.py +++ /dev/null @@ -1,19 +0,0 @@ -from imager import Imager - - -img = Imager().get_image_marks( - current_grade=15, - max_grade=20, - title_text='А/П.1 по «Метрология, стандартизация и сертификация в инфокоммуникациях: Методы и средства измерения в телекоммуникационных системах»', - mark_change_text='0 —> 15 (из 20) (+ 15)', - side_text='Изменён балл за контрольное мероприятие' -) - - -# img = Imager().get_image_news( -# title_text='Выбор элективных и факультативных дисциплин на 1 семестр 2022-2023 уч.г.', -# #title_text='Перевод в дистанционный режим обучения РТ-21, РТ-13, ЭН-22, ЭН-32, ПМ-21, П-21, Л-13, УТС-11М, ПИН -31, УТС-31, КТ-22, ЭН-34 в связи с возникшими случаями заболевания новой коронавирусной инфекцией (Covid-19)', -# side_text='Опубликована новость', -# url='https://orioks.miet.ru/main/view-news?id=474' -# ) - diff --git a/main.py b/main.py deleted file mode 100644 index 5c3ae17..0000000 --- a/main.py +++ /dev/null @@ -1,38 +0,0 @@ -import logging - -from aiogram import Bot, Dispatcher, types -from aiogram.contrib.fsm_storage.memory import MemoryStorage -from aiogram.utils import executor - -import config - -import db.admins_statistics - -import handles_register -import middlewares -from checking import on_startup -import utils.makedirs - - -bot = Bot(token=config.TELEGRAM_BOT_API_TOKEN, parse_mode=types.ParseMode.HTML) -storage = MemoryStorage() -dp = Dispatcher(bot, storage=storage) - - -def _settings_before_start() -> None: - handles_register.handles_register(dp) - db.admins_statistics.create_and_init_admins_statistics() - dp.middleware.setup(middlewares.UserAgreementMiddleware()) - dp.middleware.setup(middlewares.UserOrioksAttemptsMiddleware()) - dp.middleware.setup(middlewares.AdminCommandsMiddleware()) - utils.makedirs.make_dirs() - - -def main(): - logging.basicConfig(level=logging.INFO) - _settings_before_start() - executor.start_polling(dp, skip_updates=True, on_startup=on_startup.on_startup) - - -if __name__ == '__main__': - main() diff --git a/middlewares.py b/middlewares.py deleted file mode 100644 index a249601..0000000 --- a/middlewares.py +++ /dev/null @@ -1,61 +0,0 @@ -import aiogram.utils.markdown as md -from aiogram import types -from aiogram.dispatcher.handler import CancelHandler -from aiogram.dispatcher.middlewares import BaseMiddleware - -import config -import db.user_first_add -import db.user_status - -inline_btn_user_agreement_accept = types.InlineKeyboardButton( - 'Принять пользовательское соглашение', - callback_data='button_user_agreement_accept' -) -inline_agreement_accept = types.InlineKeyboardMarkup().add(inline_btn_user_agreement_accept) - - -class UserAgreementMiddleware(BaseMiddleware): - """Middleware, блокирующее дальнейшее использование Бота, если не принято пользовательское соглашение""" - - async def on_process_message(self, message: types.Message, *args, **kwargs): - db.user_first_add.user_first_add_to_db(user_telegram_id=message.from_user.id) - if not db.user_status.get_user_agreement_status(user_telegram_id=message.from_user.id): - await message.reply( - md.text( - md.text('Для получения доступа к Боту, необходимо принять Пользовательское соглашение:'), - md.text('https://orioks-monitoring.github.io/bot/rules'), - sep='\n', - ), - reply_markup=inline_agreement_accept, - disable_web_page_preview=True, - ) - raise CancelHandler() - - -class UserOrioksAttemptsMiddleware(BaseMiddleware): - """Middleware, блокирующее дальнейшее использование Бота, если превышено максимальное количество попыток входа в - аккаунт ОРИОКС""" - - async def on_process_message(self, message: types.Message, *args, **kwargs): - if db.user_status.get_user_orioks_attempts( - user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: - await message.reply( - md.text( - md.hbold('Ты совершил подозрительно много попыток входа в аккаунт ОРИОКС.'), - md.text('Возможно, ты нарушаешь Пользовательское соглашение, с которым согласился.'), - md.text(), - md.text('Связаться с поддержкой Бота: @orioks_monitoring_support'), - sep='\n', - ), - reply_markup=types.ReplyKeyboardRemove(), - disable_web_page_preview=True, - ) - raise CancelHandler() - - -class AdminCommandsMiddleware(BaseMiddleware): - """Middleware, разрешающее использовать команды Админов только пользователям из `config.TELEGRAM_ADMIN_IDS_LIST`""" - - async def on_process_message(self, message: types.Message, *args, **kwargs): - if message.get_command() in ('/stat',) and message.from_user.id not in config.TELEGRAM_ADMIN_IDS_LIST: - raise CancelHandler() diff --git a/requirements.txt b/requirements.txt index 6924d2f..be40799 100644 --- a/requirements.txt +++ b/requirements.txt @@ -44,4 +44,4 @@ types-aiofiles==0.8.8 typing_extensions==4.2.0 urllib3==1.26.8 wsproto==1.1.0 -yarl==1.7.2 +yarl==1.7.2 \ No newline at end of file diff --git a/run-app.py b/run-app.py new file mode 100644 index 0000000..1f5ba8e --- /dev/null +++ b/run-app.py @@ -0,0 +1,4 @@ +import app + +app.run() + diff --git a/utils/handle_orioks_logout.py b/utils/handle_orioks_logout.py index 6927eb3..8ea74c3 100644 --- a/utils/handle_orioks_logout.py +++ b/utils/handle_orioks_logout.py @@ -1,25 +1,25 @@ import os import db.user_status import db.notify_settings -import config +from config import Config from utils.delete_file import safe_delete def make_orioks_logout(user_telegram_id: int) -> None: - safe_delete(os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) + safe_delete(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) - safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) + safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', f'{user_telegram_id}.json')) db.user_status.update_user_orioks_authenticated_status( diff --git a/utils/make_request.py b/utils/make_request.py index 7985e91..1f4e34b 100644 --- a/utils/make_request.py +++ b/utils/make_request.py @@ -1,17 +1,17 @@ import asyncio import logging -import config import db.admins_statistics import aiohttp +from config import Config -_sem = asyncio.Semaphore(config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) +_sem = asyncio.Semaphore(Config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) async def get_request(url: str, session: aiohttp.ClientSession) -> str: async with _sem: # next coroutine(s) will stuck here until the previous is done - await asyncio.sleep(config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please + await asyncio.sleep(Config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please # TODO: is db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_telegram_id) # else safe delete all user's file # TODO: is db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) diff --git a/utils/makedirs.py b/utils/makedirs.py index 404a2f1..2445c9e 100644 --- a/utils/makedirs.py +++ b/utils/makedirs.py @@ -1,18 +1,19 @@ import os -import config + +from config import Config def make_dirs() -> None: - os.makedirs(os.path.join(config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) + os.makedirs(os.path.join(Config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) - os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) diff --git a/utils/notify_to_user.py b/utils/notify_to_user.py index 86e001e..5a00acc 100644 --- a/utils/notify_to_user.py +++ b/utils/notify_to_user.py @@ -1,32 +1,34 @@ import pathlib from aiogram.utils.exceptions import ChatNotFound, BotBlocked -import main -import config + +import app import utils.handle_orioks_logout import aiogram.utils.markdown as md from aiogram.types.input_file import InputFile +from config import Config + class SendToTelegram: @staticmethod async def text_message_to_user(user_telegram_id: int, message: str) -> None: try: - await main.bot.send_message(user_telegram_id, message) + await app.bot.send_message(user_telegram_id, message) except (BotBlocked, ChatNotFound) as e: utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=user_telegram_id) @staticmethod async def photo_message_to_user(user_telegram_id: int, photo_path: pathlib.Path, caption: str) -> None: try: - await main.bot.send_photo(user_telegram_id, InputFile(photo_path), caption) + await app.bot.send_photo(user_telegram_id, InputFile(photo_path), caption) except (BotBlocked, ChatNotFound) as e: utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=user_telegram_id) @staticmethod async def message_to_admins(message: str) -> None: - for admin_telegram_id in config.TELEGRAM_ADMIN_IDS_LIST: - await main.bot.send_message( + for admin_telegram_id in Config.TELEGRAM_ADMIN_IDS_LIST: + await app.bot.send_message( admin_telegram_id, md.text( md.hbold('[ADMIN]'), diff --git a/utils/orioks.py b/utils/orioks.py index ae31f10..cfa1090 100644 --- a/utils/orioks.py +++ b/utils/orioks.py @@ -6,14 +6,15 @@ import aiohttp from bs4 import BeautifulSoup -import config import utils.exceptions from datetime import datetime + +from config import Config from utils.notify_to_user import SendToTelegram import aiogram.utils.markdown as md -_sem = asyncio.Semaphore(config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) +_sem = asyncio.Semaphore(Config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) async def orioks_login_save_cookies(user_login: int, user_password: str, user_telegram_id: int) -> None: @@ -36,12 +37,12 @@ async def orioks_login_save_cookies(user_login: int, user_password: str, user_te ) async with _sem: # orioks dont die please async with aiohttp.ClientSession( - timeout=config.REQUESTS_TIMEOUT, - headers=config.ORIOKS_REQUESTS_HEADERS + timeout=Config.REQUESTS_TIMEOUT, + headers=Config.ORIOKS_REQUESTS_HEADERS ) as session: try: logging.info(f'request to login: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') - async with session.get(str(config.ORIOKS_PAGE_URLS['login'])) as resp: + async with session.get(str(Config.ORIOKS_PAGE_URLS['login'])) as resp: bs_content = BeautifulSoup(await resp.text(), "html.parser") _csrf_token = bs_content.find('input', {'name': '_csrf'})['value'] login_data = { @@ -53,12 +54,12 @@ async def orioks_login_save_cookies(user_login: int, user_password: str, user_te except asyncio.TimeoutError as e: raise e try: - async with session.post(str(config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: - if str(resp.url) == config.ORIOKS_PAGE_URLS['login']: + async with session.post(str(Config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: + if str(resp.url) == Config.ORIOKS_PAGE_URLS['login']: raise utils.exceptions.OrioksInvalidLoginCredsError except asyncio.TimeoutError as e: raise e cookies = session.cookie_jar.filter_cookies(resp.url) - pickle.dump(cookies, open(os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) + pickle.dump(cookies, open(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) await asyncio.sleep(1) From c0c4d691336fa7809edd96c84d1bb1aec05d04e3 Mon Sep 17 00:00:00 2001 From: Alexey Voronin Date: Mon, 13 Jun 2022 18:59:56 +0300 Subject: [PATCH 2/5] chore(refactoring): moved utils to app.helpers --- app/__init__.py | 6 +- app/exceptions/FileCompareException.py | 4 + .../OrioksInvalidLoginCredentialsException.py | 3 + app/exceptions/OrioksParseDataException.py | 4 + app/exceptions/__init__.py | 8 ++ .../OrioksAuthInputPasswordCommandHandler.py | 12 ++- .../orioks/OrioksLogoutCommandHandler.py | 4 +- app/handlers/errors/BaseErrorHandler.py | 6 +- app/helpers/CommonHelper.py | 34 ++++++++ .../helpers/JsonFileHelper.py | 3 +- app/helpers/OrioksHelper.py | 87 +++++++++++++++++++ app/helpers/RequestHelper.py | 27 ++++++ .../helpers/TelegramMessageHelper.py | 26 +++--- app/helpers/__init__.py | 10 +++ app/middlewares/AdminCommandsMiddleware.py | 1 + app/middlewares/UserAgreementMiddleware.py | 1 + .../UserOrioksAttemptsMiddleware.py | 1 + checking/homeworks/get_orioks_homeworks.py | 34 ++++---- checking/marks/compares.py | 18 ++-- checking/marks/get_orioks_marks.py | 51 +++++------ checking/news/get_orioks_news.py | 33 ++++--- checking/on_startup.py | 32 ++++--- checking/requests/get_orioks_requests.py | 33 ++++--- utils/delete_file.py | 10 --- utils/exceptions.py | 10 --- utils/handle_orioks_logout.py | 29 ------- utils/make_request.py | 25 ------ utils/makedirs.py | 19 ---- utils/my_isdigit.py | 6 -- utils/orioks.py | 65 -------------- 30 files changed, 301 insertions(+), 301 deletions(-) create mode 100644 app/exceptions/FileCompareException.py create mode 100644 app/exceptions/OrioksInvalidLoginCredentialsException.py create mode 100644 app/exceptions/OrioksParseDataException.py create mode 100644 app/exceptions/__init__.py create mode 100644 app/helpers/CommonHelper.py rename utils/json_files.py => app/helpers/JsonFileHelper.py (96%) create mode 100644 app/helpers/OrioksHelper.py create mode 100644 app/helpers/RequestHelper.py rename utils/notify_to_user.py => app/helpers/TelegramMessageHelper.py (55%) create mode 100644 app/helpers/__init__.py delete mode 100644 utils/delete_file.py delete mode 100644 utils/exceptions.py delete mode 100644 utils/handle_orioks_logout.py delete mode 100644 utils/make_request.py delete mode 100644 utils/makedirs.py delete mode 100644 utils/my_isdigit.py delete mode 100644 utils/orioks.py diff --git a/app/__init__.py b/app/__init__.py index 65539eb..1b2e619 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,9 +6,8 @@ from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker -import utils.makedirs - from app.handlers import register_handlers +from app.helpers import CommonHelper from app.middlewares import UserAgreementMiddleware, UserOrioksAttemptsMiddleware, AdminCommandsMiddleware from checking import on_startup @@ -30,8 +29,7 @@ def _settings_before_start() -> None: dispatcher.middleware.setup(UserAgreementMiddleware()) dispatcher.middleware.setup(UserOrioksAttemptsMiddleware()) dispatcher.middleware.setup(AdminCommandsMiddleware()) - utils.makedirs.make_dirs() - pass + CommonHelper.make_dirs() def run(): diff --git a/app/exceptions/FileCompareException.py b/app/exceptions/FileCompareException.py new file mode 100644 index 0000000..ed6b876 --- /dev/null +++ b/app/exceptions/FileCompareException.py @@ -0,0 +1,4 @@ + + +class FileCompareException(Exception): + pass diff --git a/app/exceptions/OrioksInvalidLoginCredentialsException.py b/app/exceptions/OrioksInvalidLoginCredentialsException.py new file mode 100644 index 0000000..516f1ee --- /dev/null +++ b/app/exceptions/OrioksInvalidLoginCredentialsException.py @@ -0,0 +1,3 @@ + +class OrioksInvalidLoginCredentialsException(Exception): + pass diff --git a/app/exceptions/OrioksParseDataException.py b/app/exceptions/OrioksParseDataException.py new file mode 100644 index 0000000..226ade6 --- /dev/null +++ b/app/exceptions/OrioksParseDataException.py @@ -0,0 +1,4 @@ + + +class OrioksParseDataException(Exception): + pass diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py new file mode 100644 index 0000000..d2c8f68 --- /dev/null +++ b/app/exceptions/__init__.py @@ -0,0 +1,8 @@ +from .OrioksInvalidLoginCredentialsException import OrioksInvalidLoginCredentialsException +from .OrioksParseDataException import OrioksParseDataException +from .FileCompareException import FileCompareException + +__all__ = [ + 'OrioksInvalidLoginCredentialsException', + 'OrioksParseDataException', 'FileCompareException' +] diff --git a/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py index 037c934..5b520c3 100644 --- a/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py @@ -4,17 +4,15 @@ from aiogram.utils import markdown import app +from app.exceptions import OrioksInvalidLoginCredentialsException from app.forms import OrioksAuthForm from app.handlers import AbstractCommandHandler import db.user_status import db.admins_statistics -import utils.exceptions -import utils.orioks -import utils.handle_orioks_logout +from app.helpers import OrioksHelper, TelegramMessageHelper from app.menus.orioks import OrioksAuthFailedMenu from app.menus.start import StartMenu from config import Config -from utils.notify_to_user import SendToTelegram class OrioksAuthInputPasswordCommandHandler(AbstractCommandHandler): @@ -48,7 +46,7 @@ async def process(message: types.Message, *args, **kwargs): Config.TELEGRAM_STICKER_LOADER, ) try: - await utils.orioks.orioks_login_save_cookies(user_login=data['login'], + await OrioksHelper.orioks_login_save_cookies(user_login=data['login'], user_password=data['password'], user_telegram_id=message.from_user.id) db.user_status.update_user_orioks_authenticated_status( @@ -65,7 +63,7 @@ async def process(message: types.Message, *args, **kwargs): db.admins_statistics.update_inc_admins_statistics_row_name( row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_success_logins ) - except utils.exceptions.OrioksInvalidLoginCredsError: + except OrioksInvalidLoginCredentialsException: db.admins_statistics.update_inc_admins_statistics_row_name( row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_failed_logins ) @@ -79,7 +77,7 @@ async def process(message: types.Message, *args, **kwargs): sep='\n', ) ) - await SendToTelegram.message_to_admins(message='Сервер ОРИОКС не отвечает') + await TelegramMessageHelper.message_to_admins(message='Сервер ОРИОКС не отвечает') await OrioksAuthFailedMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) await app.bot.delete_message(message.chat.id, message.message_id) await state.finish() diff --git a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py index 5a8c3be..dc6c5ef 100644 --- a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py @@ -5,7 +5,7 @@ from app.handlers import AbstractCommandHandler import db.user_status -import utils.handle_orioks_logout +from app.helpers import OrioksHelper class OrioksLogoutCommandHandler(AbstractCommandHandler): @@ -24,4 +24,4 @@ async def process(message: types.Message, *args, **kwargs): user_telegram_id=message.from_user.id, is_user_orioks_authenticated=False ) - utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=message.from_user.id) + OrioksHelper.make_orioks_logout(user_telegram_id=message.from_user.id) diff --git a/app/handlers/errors/BaseErrorHandler.py b/app/handlers/errors/BaseErrorHandler.py index df2a92b..70437ed 100644 --- a/app/handlers/errors/BaseErrorHandler.py +++ b/app/handlers/errors/BaseErrorHandler.py @@ -4,7 +4,7 @@ from aiogram.utils.exceptions import MessageNotModified, CantParseEntities, TelegramAPIError from app.handlers.AbstractErrorHandler import AbstractErrorHandler -from utils.notify_to_user import SendToTelegram +from app.helpers import TelegramMessageHelper class BaseErrorHandler(AbstractErrorHandler): @@ -20,5 +20,5 @@ async def process(update: types.Update, exception): if isinstance(exception, TelegramAPIError): pass - await SendToTelegram.message_to_admins(message=f'Update: {update} \n{exception}') - logging.exception(f'Update: {update} \n{exception}') + await TelegramMessageHelper.message_to_admins(message=f'Update: {update} \n{exception}') + logging.exception(f'Update: %s \n %s', (update, exception, )) diff --git a/app/helpers/CommonHelper.py b/app/helpers/CommonHelper.py new file mode 100644 index 0000000..066c722 --- /dev/null +++ b/app/helpers/CommonHelper.py @@ -0,0 +1,34 @@ +import os +import pathlib +from typing import Union + +from config import Config + + +class CommonHelper: + + @staticmethod + def make_dirs() -> None: + os.makedirs(os.path.join(Config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) + os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) + + @staticmethod + def is_correct_convert_to_float(x) -> bool: + try: + float(x) + return True + except ValueError: + return False + + @staticmethod + def safe_delete(path: Union[str, pathlib.Path]) -> None: + try: + os.remove(path) + except FileNotFoundError: + pass diff --git a/utils/json_files.py b/app/helpers/JsonFileHelper.py similarity index 96% rename from utils/json_files.py rename to app/helpers/JsonFileHelper.py index 9326bc0..847a117 100644 --- a/utils/json_files.py +++ b/app/helpers/JsonFileHelper.py @@ -3,7 +3,8 @@ import aiofiles -class JsonFile: +class JsonFileHelper: + @staticmethod async def save(data: Union[list, dict], filename: str) -> None: async with aiofiles.open(filename, mode='w', encoding='utf-8') as f: diff --git a/app/helpers/OrioksHelper.py b/app/helpers/OrioksHelper.py new file mode 100644 index 0000000..f12b40d --- /dev/null +++ b/app/helpers/OrioksHelper.py @@ -0,0 +1,87 @@ +import asyncio +import logging +import os +import pickle + +import aiohttp +from bs4 import BeautifulSoup + +from datetime import datetime + +from app import CommonHelper +from app.exceptions import OrioksInvalidLoginCredentialsException +from app.helpers import TelegramMessageHelper +from config import Config +import aiogram.utils.markdown as md + +import db.user_status +import db.notify_settings + +_sem = asyncio.Semaphore(Config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) + + +class OrioksHelper: + + @staticmethod + async def orioks_login_save_cookies(user_login: int, user_password: str, user_telegram_id: int) -> None: + # pylint: disable=protected-access + user_queue = len(_sem._waiters) + 2 + if user_queue - 2 > 0: + logging.info(f'login: {user_queue=}') + _cats_queue_emoji = f'{"🐈" * (user_queue - 1)}🐈‍⬛' + await TelegramMessageHelper.text_message_to_user( + user_telegram_id=user_telegram_id, + message=md.text( + md.text(_cats_queue_emoji), + md.text( + md.text(f'Твой номер в очереди на авторизацию: {user_queue}.'), + md.text('Ты получишь уведомление, когда она будет выполнена.'), + sep=' ', + ), + md.text('Это предотвращает слишком большую нагрузку на ОРИОКС'), + sep='\n', + ) + ) + async with _sem: # orioks dont die please + async with aiohttp.ClientSession( + timeout=Config.REQUESTS_TIMEOUT, + headers=Config.ORIOKS_REQUESTS_HEADERS + ) as session: + try: + logging.info(f'request to login: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') + async with session.get(str(Config.ORIOKS_PAGE_URLS['login'])) as resp: + bs_content = BeautifulSoup(await resp.text(), "html.parser") + _csrf_token = bs_content.find('input', {'name': '_csrf'})['value'] + login_data = { + 'LoginForm[login]': int(user_login), + 'LoginForm[password]': str(user_password), + 'LoginForm[rememberMe]': 1, + '_csrf': _csrf_token, + } + except asyncio.TimeoutError as e: + raise e + try: + async with session.post(str(Config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: + if str(resp.url) == Config.ORIOKS_PAGE_URLS['login']: + raise OrioksInvalidLoginCredentialsException + except asyncio.TimeoutError as e: + raise e + + cookies = session.cookie_jar.filter_cookies(resp.url) + pickle.dump(cookies, + open(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) + await asyncio.sleep(1) + + @staticmethod + def make_orioks_logout(user_telegram_id: int) -> None: + CommonHelper.safe_delete(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', f'{user_telegram_id}.json')) + + db.user_status.update_user_orioks_authenticated_status(user_telegram_id=user_telegram_id, is_user_orioks_authenticated=False) + db.notify_settings.update_user_notify_settings_reset_to_default(user_telegram_id=user_telegram_id) diff --git a/app/helpers/RequestHelper.py b/app/helpers/RequestHelper.py new file mode 100644 index 0000000..1495b40 --- /dev/null +++ b/app/helpers/RequestHelper.py @@ -0,0 +1,27 @@ +import asyncio +import logging + +import db.admins_statistics +import aiohttp + +from config import Config + + +class RequestHelper: + _sem = asyncio.Semaphore(Config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) + + @staticmethod + async def get_request(url: str, session: aiohttp.ClientSession) -> str: + async with RequestHelper._sem: # next coroutine(s) will stuck here until the previous is done + await asyncio.sleep(Config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please + # TODO: is db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_telegram_id) + # else safe delete all user's file + # TODO: is db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) + # else safe delete non-enabled categories + logging.debug('get request to: %s', (url, )) + async with session.get(str(url)) as resp: + raw_html = await resp.text() + db.admins_statistics.update_inc_admins_statistics_row_name( # TODO: sum of requests and inc for one use db + row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_scheduled_requests + ) + return raw_html diff --git a/utils/notify_to_user.py b/app/helpers/TelegramMessageHelper.py similarity index 55% rename from utils/notify_to_user.py rename to app/helpers/TelegramMessageHelper.py index 5a00acc..4294eff 100644 --- a/utils/notify_to_user.py +++ b/app/helpers/TelegramMessageHelper.py @@ -1,38 +1,38 @@ import pathlib -from aiogram.utils.exceptions import ChatNotFound, BotBlocked +from aiogram.types import InputFile +from aiogram.utils import markdown +from aiogram.utils.exceptions import BotBlocked, ChatNotFound import app -import utils.handle_orioks_logout -import aiogram.utils.markdown as md -from aiogram.types.input_file import InputFile - +from app.helpers import OrioksHelper from config import Config -class SendToTelegram: +class TelegramMessageHelper: + @staticmethod async def text_message_to_user(user_telegram_id: int, message: str) -> None: try: await app.bot.send_message(user_telegram_id, message) - except (BotBlocked, ChatNotFound) as e: - utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=user_telegram_id) + except (BotBlocked, ChatNotFound): + OrioksHelper.make_orioks_logout(user_telegram_id=user_telegram_id) @staticmethod async def photo_message_to_user(user_telegram_id: int, photo_path: pathlib.Path, caption: str) -> None: try: await app.bot.send_photo(user_telegram_id, InputFile(photo_path), caption) - except (BotBlocked, ChatNotFound) as e: - utils.handle_orioks_logout.make_orioks_logout(user_telegram_id=user_telegram_id) + except (BotBlocked, ChatNotFound): + OrioksHelper.make_orioks_logout(user_telegram_id=user_telegram_id) @staticmethod async def message_to_admins(message: str) -> None: for admin_telegram_id in Config.TELEGRAM_ADMIN_IDS_LIST: await app.bot.send_message( admin_telegram_id, - md.text( - md.hbold('[ADMIN]'), - md.text(message), + markdown.text( + markdown.hbold('[ADMIN]'), + markdown.text(message), sep=': ', ) ) diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py new file mode 100644 index 0000000..c22be8e --- /dev/null +++ b/app/helpers/__init__.py @@ -0,0 +1,10 @@ +from .CommonHelper import CommonHelper +from .JsonFileHelper import JsonFileHelper +from .OrioksHelper import OrioksHelper +from .RequestHelper import RequestHelper +from .TelegramMessageHelper import TelegramMessageHelper + +__all__ = [ + 'CommonHelper', 'JsonFileHelper', 'OrioksHelper', + 'RequestHelper', 'TelegramMessageHelper' +] diff --git a/app/middlewares/AdminCommandsMiddleware.py b/app/middlewares/AdminCommandsMiddleware.py index 834777e..5681c79 100644 --- a/app/middlewares/AdminCommandsMiddleware.py +++ b/app/middlewares/AdminCommandsMiddleware.py @@ -10,6 +10,7 @@ class AdminCommandsMiddleware(BaseMiddleware): Middleware, разрешающее использовать команды Админов только пользователям из `config.TELEGRAM_ADMIN_IDS_LIST` """ + # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): if message.get_command() in ('/stat',) and message.from_user.id not in Config.TELEGRAM_ADMIN_IDS_LIST: raise CancelHandler() diff --git a/app/middlewares/UserAgreementMiddleware.py b/app/middlewares/UserAgreementMiddleware.py index 10f91b8..e183e30 100644 --- a/app/middlewares/UserAgreementMiddleware.py +++ b/app/middlewares/UserAgreementMiddleware.py @@ -19,6 +19,7 @@ class UserAgreementMiddleware(BaseMiddleware): ) inline_agreement_accept = types.InlineKeyboardMarkup().add(inline_btn_user_agreement_accept) + # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): db.user_first_add.user_first_add_to_db(user_telegram_id=message.from_user.id) if not db.user_status.get_user_agreement_status(user_telegram_id=message.from_user.id): diff --git a/app/middlewares/UserOrioksAttemptsMiddleware.py b/app/middlewares/UserOrioksAttemptsMiddleware.py index b7754c0..9bb4c78 100644 --- a/app/middlewares/UserOrioksAttemptsMiddleware.py +++ b/app/middlewares/UserOrioksAttemptsMiddleware.py @@ -16,6 +16,7 @@ class UserOrioksAttemptsMiddleware(BaseMiddleware): аккаунт ОРИОКС """ + # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): if db.user_status.get_user_orioks_attempts( user_telegram_id=message.from_user.id) > Config.ORIOKS_MAX_LOGIN_TRIES: diff --git a/checking/homeworks/get_orioks_homeworks.py b/checking/homeworks/get_orioks_homeworks.py index 753d2b4..73d97ef 100644 --- a/checking/homeworks/get_orioks_homeworks.py +++ b/checking/homeworks/get_orioks_homeworks.py @@ -5,19 +5,16 @@ import aiohttp from bs4 import BeautifulSoup +from app.exceptions import OrioksParseDataException, FileCompareException +from app.helpers import JsonFileHelper, TelegramMessageHelper, CommonHelper, RequestHelper from config import Config -from utils import exceptions -from utils.delete_file import safe_delete -from utils.json_files import JsonFile -from utils.make_request import get_request -from utils.notify_to_user import SendToTelegram import aiogram.utils.markdown as md def _orioks_parse_homeworks(raw_html: str) -> dict: bs_content = BeautifulSoup(raw_html, "html.parser") if bs_content.select_one('.table.table-condensed.table-thread') is None: - raise exceptions.OrioksCantParseData + raise OrioksParseDataException table_raw = bs_content.select('.table.table-condensed.table-thread tr:not(:first-child)') homeworks = dict() for tr in table_raw: @@ -35,7 +32,7 @@ def _orioks_parse_homeworks(raw_html: str) -> dict: async def get_orioks_homeworks(session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) + raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) return _orioks_parse_homeworks(raw_html) @@ -99,8 +96,9 @@ def compare(old_dict: dict, new_dict: dict) -> list: for thread_id_old in old_dict: try: _ = new_dict[thread_id_old] - except KeyError: - raise exceptions.FileCompareError + except KeyError as exception: + raise FileCompareException from exception + if old_dict[thread_id_old]['status'] != new_dict[thread_id_old]['status']: diffs.append({ 'type': 'new_status', # or `new_message` @@ -121,23 +119,23 @@ async def user_homeworks_check(user_telegram_id: int, session: aiohttp.ClientSes path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'homeworks', student_json_file) try: homeworks_dict = await get_orioks_homeworks(session=session) - except exceptions.OrioksCantParseData: + except OrioksParseDataException: logging.info('(HOMEWORKS) exception: utils.exceptions.OrioksCantParseData') - safe_delete(path=path_users_to_file) + CommonHelper.safe_delete(path=path_users_to_file) return None if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): - await JsonFile.save(data=homeworks_dict, filename=path_users_to_file) + await JsonFileHelper.save(data=homeworks_dict, filename=path_users_to_file) return None - _old_json = await JsonFile.open(filename=path_users_to_file) - old_dict = JsonFile.convert_dict_keys_to_int(_old_json) + _old_json = await JsonFileHelper.open(filename=path_users_to_file) + old_dict = JsonFileHelper.convert_dict_keys_to_int(_old_json) try: diffs = compare(old_dict=old_dict, new_dict=homeworks_dict) - except exceptions.FileCompareError: - await JsonFile.save(data=homeworks_dict, filename=path_users_to_file) + except FileCompareException: + await JsonFileHelper.save(data=homeworks_dict, filename=path_users_to_file) return None if len(diffs) > 0: msg_to_send = await get_homeworks_to_msg(diffs=diffs) - await SendToTelegram.text_message_to_user(user_telegram_id=user_telegram_id, message=msg_to_send) - await JsonFile.save(data=homeworks_dict, filename=path_users_to_file) + await TelegramMessageHelper.text_message_to_user(user_telegram_id=user_telegram_id, message=msg_to_send) + await JsonFileHelper.save(data=homeworks_dict, filename=path_users_to_file) diff --git a/checking/marks/compares.py b/checking/marks/compares.py index cfda34d..44ee937 100644 --- a/checking/marks/compares.py +++ b/checking/marks/compares.py @@ -1,6 +1,6 @@ -from utils import exceptions +from app.exceptions import FileCompareException +from app.helpers import CommonHelper import aiogram.utils.markdown as md -from utils.my_isdigit import my_isdigit from typing import NamedTuple @@ -14,20 +14,20 @@ class DisciplineObject(NamedTuple): def file_compares(old_file: list, new_file: list) -> list: if len(old_file) != len(new_file): - raise exceptions.FileCompareError + raise FileCompareException diffs = [] for old, new in zip(old_file, new_file): if old['subject'] != new['subject']: - raise exceptions.FileCompareError + raise FileCompareException if len(old['tasks']) != len(new['tasks']): - raise exceptions.FileCompareError + raise FileCompareException diffs_one_subject = [] for old_task, new_task in zip(old['tasks'], new['tasks']): if old_task['max_grade'] != new_task['max_grade']: - raise exceptions.FileCompareError + raise FileCompareException if old_task['alias'] != new_task['alias']: - raise exceptions.FileCompareError + raise FileCompareException old_grade = old_task['current_grade'] new_grade = new_task['current_grade'] @@ -35,8 +35,8 @@ def file_compares(old_file: list, new_file: list) -> list: old_grade = 0 if old_grade == '-' else old_grade new_grade = 0 if new_grade == '-' else new_grade if new_grade == 'н' or old_grade == 'н': - new_grade_to_digit = new_grade if my_isdigit(new_grade) else 0 - old_grade_to_digit = old_grade if my_isdigit(old_grade) else 0 + new_grade_to_digit = new_grade if CommonHelper.is_correct_convert_to_float(new_grade) else 0 + old_grade_to_digit = old_grade if CommonHelper.is_correct_convert_to_float(old_grade) else 0 diffs_one_subject.append({ 'type': 'missing_grade', 'task': new_task['alias'], diff --git a/checking/marks/get_orioks_marks.py b/checking/marks/get_orioks_marks.py index 6f377ef..87ffaf4 100644 --- a/checking/marks/get_orioks_marks.py +++ b/checking/marks/get_orioks_marks.py @@ -5,15 +5,12 @@ import aiohttp from bs4 import BeautifulSoup import logging + +from app.exceptions import OrioksParseDataException, FileCompareException +from app.helpers import CommonHelper, RequestHelper, TelegramMessageHelper, JsonFileHelper from checking.marks.compares import file_compares, get_discipline_objs_from_diff from config import Config -from utils import exceptions -from utils.json_files import JsonFile -from utils.notify_to_user import SendToTelegram -from utils.make_request import get_request -from utils.my_isdigit import my_isdigit from images.imager import Imager -from utils.delete_file import safe_delete @dataclass @@ -36,8 +33,8 @@ def _iterate_forang_version_with_list(forang: dict) -> list: max_grade = mark['max_ball'] one_discipline.append({'alias': alias, 'current_grade': current_grade, 'max_grade': max_grade}) - discipline_ball.current += current_grade if my_isdigit(current_grade) else 0 - discipline_ball.might_be += max_grade if my_isdigit(max_grade) and current_grade != '-' else 0 + discipline_ball.current += current_grade if CommonHelper.is_correct_convert_to_float(current_grade) else 0 + discipline_ball.might_be += max_grade if CommonHelper.is_correct_convert_to_float(max_grade) and current_grade != '-' else 0 json_to_save.append({ 'subject': discipline['name'], 'tasks': one_discipline, @@ -64,8 +61,8 @@ def _iterate_forang_version_with_keys(forang: dict) -> list: max_grade = mark['max_ball'] one_discipline.append({'alias': alias, 'current_grade': current_grade, 'max_grade': max_grade}) - discipline_ball.current += current_grade if my_isdigit(current_grade) else 0 - discipline_ball.might_be += max_grade if my_isdigit(max_grade) and current_grade != '-' else 0 + discipline_ball.current += current_grade if CommonHelper.is_correct_convert_to_float(current_grade) else 0 + discipline_ball.might_be += max_grade if CommonHelper.is_correct_convert_to_float(max_grade) and current_grade != '-' else 0 json_to_save.append({ 'subject': forang['dises'][discipline_index]['name'], 'tasks': one_discipline, @@ -82,12 +79,12 @@ def _get_orioks_forang(raw_html: str): bs_content = BeautifulSoup(raw_html, "html.parser") try: forang_raw = bs_content.find(id='forang').text - except AttributeError: - raise exceptions.OrioksCantParseData + except AttributeError as exception: + raise OrioksParseDataException from exception forang = json.loads(forang_raw) if len(forang) == 0: - raise exceptions.OrioksCantParseData + raise OrioksParseDataException try: json_to_save = _iterate_forang_version_with_list(forang=forang) @@ -98,7 +95,7 @@ def _get_orioks_forang(raw_html: str): async def get_orioks_marks(session: aiohttp.ClientSession): - raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) + raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) return _get_orioks_forang(raw_html) @@ -107,25 +104,25 @@ async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'marks', student_json_file) try: detailed_info = await get_orioks_marks(session=session) - except FileNotFoundError: - await SendToTelegram.message_to_admins(message=f'FileNotFoundError - {user_telegram_id}') - raise Exception(f'FileNotFoundError - {user_telegram_id}') - except exceptions.OrioksCantParseData: + except FileNotFoundError as exception: + await TelegramMessageHelper.message_to_admins(message=f'FileNotFoundError - {user_telegram_id}') + raise Exception(f'FileNotFoundError - {user_telegram_id}') from exception + except OrioksParseDataException: logging.info('(MARKS) exception: utils.exceptions.OrioksCantParseData') - safe_delete(path=path_users_to_file) + CommonHelper.safe_delete(path=path_users_to_file) return None if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): - await JsonFile.save(data=detailed_info, filename=path_users_to_file) + await JsonFileHelper.save(data=detailed_info, filename=path_users_to_file) return None - old_json = await JsonFile.open(filename=path_users_to_file) + old_json = await JsonFileHelper.open(filename=path_users_to_file) try: diffs = file_compares(old_file=old_json, new_file=detailed_info) - except exceptions.FileCompareError: - await JsonFile.save(data=detailed_info, filename=path_users_to_file) + except FileCompareException: + await JsonFileHelper.save(data=detailed_info, filename=path_users_to_file) if old_json[0]['subject'] != detailed_info[0]['subject'] and \ old_json[-1]['subject'] != detailed_info[-1]['subject']: - await SendToTelegram.text_message_to_user( + await TelegramMessageHelper.text_message_to_user( user_telegram_id=user_telegram_id, message='🎉 Поздравляем с началом нового семестра и желаем успехов в учёбе!\n' 'Новости Бота в канале @orioks_monitoring' @@ -142,10 +139,10 @@ async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession mark_change_text=discipline_obj.mark_change_text, side_text='Изменён балл за контрольное мероприятие' ) - await SendToTelegram.photo_message_to_user( + await TelegramMessageHelper.photo_message_to_user( user_telegram_id=user_telegram_id, photo_path=photo_path, caption=discipline_obj.caption ) - safe_delete(path=photo_path) - await JsonFile.save(data=detailed_info, filename=path_users_to_file) + CommonHelper.safe_delete(path=photo_path) + await JsonFileHelper.save(data=detailed_info, filename=path_users_to_file) diff --git a/checking/news/get_orioks_news.py b/checking/news/get_orioks_news.py index 4f9597c..b6a8b56 100644 --- a/checking/news/get_orioks_news.py +++ b/checking/news/get_orioks_news.py @@ -5,14 +5,11 @@ import aiohttp from bs4 import BeautifulSoup +from app.exceptions import OrioksParseDataException +from app.helpers import RequestHelper, CommonHelper, JsonFileHelper, TelegramMessageHelper from config import Config -from utils import exceptions -from utils.json_files import JsonFile -from utils.make_request import get_request -from utils.notify_to_user import SendToTelegram import aiogram.utils.markdown as md from images.imager import Imager -from utils.delete_file import safe_delete from typing import NamedTuple @@ -26,7 +23,7 @@ def _orioks_parse_news(raw_html: str) -> dict: bs_content = BeautifulSoup(raw_html, "html.parser") news_raw = bs_content.find(id='news') if news_raw is None: - raise exceptions.OrioksCantParseData + raise OrioksParseDataException last_news_line = news_raw.select_one('#news tr:nth-child(2) a')['href'] last_news_id = int(re.findall(r'\d+$', last_news_line)[0]) return { @@ -35,7 +32,7 @@ def _orioks_parse_news(raw_html: str) -> dict: async def get_orioks_news(session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['news'], session=session) + raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['news'], session=session) return _orioks_parse_news(raw_html) @@ -45,7 +42,7 @@ def _find_in_str_with_beginning_and_ending(string_to_find: str, beginning: str, async def get_news_by_news_id(news_id: int, session: aiohttp.ClientSession) -> NewsObject: - raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) + raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) bs_content = BeautifulSoup(raw_html, "html.parser") well_raw = bs_content.find_all('div', {'class': 'well'})[0] return NewsObject( @@ -80,10 +77,10 @@ async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) try: last_news_id = await get_orioks_news(session=session) - except exceptions.OrioksCantParseData: + except OrioksParseDataException as exception: logging.info('(NEWS) exception: utils.exceptions.OrioksCantParseData') - safe_delete(path=path_users_to_file) - raise exceptions.OrioksCantParseData + CommonHelper.safe_delete(path=path_users_to_file) + raise OrioksParseDataException from exception return await get_news_by_news_id(news_id=last_news_id['last_id'], session=session) @@ -93,15 +90,15 @@ async def user_news_check_from_news_id(user_telegram_id: int, session: aiohttp.C path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) last_news_id = {'last_id': current_new.id} if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): - await JsonFile.save(data=last_news_id, filename=path_users_to_file) + await JsonFileHelper.save(data=last_news_id, filename=path_users_to_file) await session.close() return None - old_json = await JsonFile.open(filename=path_users_to_file) + old_json = await JsonFileHelper.open(filename=path_users_to_file) if last_news_id['last_id'] == old_json['last_id']: await session.close() return None if old_json['last_id'] > last_news_id['last_id']: - await SendToTelegram.message_to_admins( + await TelegramMessageHelper.message_to_admins( message=f'[{user_telegram_id}] - old_json["last_id"] > last_news_id["last_id"]' ) await session.close() @@ -120,12 +117,12 @@ async def user_news_check_from_news_id(user_telegram_id: int, session: aiohttp.C side_text='Опубликована новость', url=news_obj.url ) - await SendToTelegram.photo_message_to_user( + await TelegramMessageHelper.photo_message_to_user( user_telegram_id=user_telegram_id, photo_path=path_to_img, caption=transform_news_to_msg(news_obj=news_obj) ) - await JsonFile.save(data={"last_id": news_id}, filename=path_users_to_file) - safe_delete(path=path_to_img) + await JsonFileHelper.save(data={"last_id": news_id}, filename=path_users_to_file) + CommonHelper.safe_delete(path=path_to_img) await session.close() - await JsonFile.save(data=last_news_id, filename=path_users_to_file) + await JsonFileHelper.save(data=last_news_id, filename=path_users_to_file) diff --git a/checking/on_startup.py b/checking/on_startup.py index f781328..5fe7a50 100644 --- a/checking/on_startup.py +++ b/checking/on_startup.py @@ -10,16 +10,14 @@ import db.notify_settings import db.user_status +from app.exceptions import OrioksParseDataException +from app.helpers import CommonHelper, TelegramMessageHelper from checking.marks.get_orioks_marks import user_marks_check from checking.news.get_orioks_news import get_current_new, user_news_check_from_news_id from checking.homeworks.get_orioks_homeworks import user_homeworks_check from checking.requests.get_orioks_requests import user_requests_check from http.cookies import SimpleCookie -import utils from config import Config -from utils import exceptions -from utils.notify_to_user import SendToTelegram -import utils.delete_file def _get_user_orioks_cookies_from_telegram_id(user_telegram_id: int) -> SimpleCookie: @@ -29,23 +27,23 @@ def _get_user_orioks_cookies_from_telegram_id(user_telegram_id: int) -> SimpleCo def _delete_users_tracking_data_in_notify_settings_off(user_telegram_id: int, user_notify_settings: dict) -> None: if not user_notify_settings['marks']: - utils.delete_file.safe_delete( + CommonHelper.safe_delete( os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') ) if not user_notify_settings['news']: - utils.delete_file.safe_delete( + CommonHelper.safe_delete( os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') ) if not user_notify_settings['discipline_sources']: - utils.delete_file.safe_delete(os.path.join( + CommonHelper.safe_delete(os.path.join( Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') ) if not user_notify_settings['homeworks']: - utils.delete_file.safe_delete(os.path.join( + CommonHelper.safe_delete(os.path.join( Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') ) if not user_notify_settings['requests']: - utils.delete_file.safe_delete(os.path.join( + CommonHelper.safe_delete(os.path.join( Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') ) @@ -88,13 +86,13 @@ async def make_all_users_news_check(tries_counter: int = 0) -> list: headers=Config.ORIOKS_REQUESTS_HEADERS ) as session: current_new = await get_current_new(user_telegram_id=picked_user_to_check_news, session=session) - except exceptions.OrioksCantParseData: + except OrioksParseDataException: return await make_all_users_news_check(tries_counter=tries_counter + 1) for user_telegram_id in users_to_check_news: try: cookies = _get_user_orioks_cookies_from_telegram_id(user_telegram_id=user_telegram_id) except FileNotFoundError: - logging.error(f'(COOKIES) FileNotFoundError: {user_telegram_id}') + logging.error('(COOKIES) FileNotFoundError: %s' % (user_telegram_id, )) continue user_session = aiohttp.ClientSession(cookies=cookies, timeout=Config.REQUESTS_TIMEOUT) tasks.append(user_news_check_from_news_id( @@ -109,15 +107,15 @@ async def run_requests(tasks: list) -> None: try: await asyncio.gather(*tasks) except asyncio.TimeoutError: - await SendToTelegram.message_to_admins(message='Сервер ОРИОКС не отвечает') + await TelegramMessageHelper.message_to_admins(message='Сервер ОРИОКС не отвечает') return except Exception as e: - logging.error(f'Ошибка в запросах ОРИОКС!\n{e}') - await SendToTelegram.message_to_admins(message=f'Ошибка в запросах ОРИОКС!\n{e}') + logging.error('Ошибка в запросах ОРИОКС!\n %s' % (e, )) + await TelegramMessageHelper.message_to_admins(message=f'Ошибка в запросах ОРИОКС!\n{e}') async def do_checks(): - logging.info(f'started: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') + logging.info('started: %s' % (datetime.now().strftime("%H:%M:%S %d.%m.%Y"),)) users_to_check = db.user_status.select_all_orioks_authenticated_users() tasks = [] + await make_all_users_news_check() @@ -126,11 +124,11 @@ async def do_checks(): user_telegram_id=user_telegram_id )) await run_requests(tasks=tasks) - logging.info(f'ended: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') + logging.info('ended: %s' % (datetime.now().strftime("%H:%M:%S %d.%m.%Y"),)) async def scheduler(): - await SendToTelegram.message_to_admins(message='Бот запущен!') + await TelegramMessageHelper.message_to_admins(message='Бот запущен!') aioschedule.every(Config.ORIOKS_SECONDS_BETWEEN_WAVES).seconds.do(do_checks) while True: await aioschedule.run_pending() diff --git a/checking/requests/get_orioks_requests.py b/checking/requests/get_orioks_requests.py index 46cdaa6..6dd6454 100644 --- a/checking/requests/get_orioks_requests.py +++ b/checking/requests/get_orioks_requests.py @@ -5,12 +5,9 @@ import aiohttp from bs4 import BeautifulSoup +from app.exceptions import OrioksParseDataException, FileCompareException +from app.helpers import CommonHelper, RequestHelper, JsonFileHelper, TelegramMessageHelper from config import Config -from utils import exceptions -from utils.delete_file import safe_delete -from utils.json_files import JsonFile -from utils.notify_to_user import SendToTelegram -from utils.make_request import get_request import aiogram.utils.markdown as md @@ -20,7 +17,7 @@ def _orioks_parse_requests(raw_html: str, section: str) -> dict: new_messages_td_list_index = 6 bs_content = BeautifulSoup(raw_html, "html.parser") if bs_content.select_one('.table.table-condensed.table-thread') is None: - raise exceptions.OrioksCantParseData + raise OrioksParseDataException table_raw = bs_content.select('.table.table-condensed.table-thread tr:not(:first-child)') requests = dict() for tr in table_raw: @@ -37,7 +34,7 @@ def _orioks_parse_requests(raw_html: str, section: str) -> dict: async def get_orioks_requests(section: str, session: aiohttp.ClientSession) -> dict: - raw_html = await get_request(url=Config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) + raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) return _orioks_parse_requests(raw_html=raw_html, section=section) @@ -99,8 +96,8 @@ def compare(old_dict: dict, new_dict: dict) -> list: for thread_id_old in old_dict: try: _ = new_dict[thread_id_old] - except KeyError: - raise exceptions.FileCompareError + except KeyError as exception: + raise FileCompareException from exception if old_dict[thread_id_old]['status'] != new_dict[thread_id_old]['status']: diffs.append({ 'type': 'new_status', # or `new_message` @@ -123,26 +120,26 @@ async def _user_requests_check_with_subsection(user_telegram_id: int, section: s 'requests', section, student_json_file) try: requests_dict = await get_orioks_requests(section=section, session=session) - except exceptions.OrioksCantParseData: + except OrioksParseDataException: logging.info('(REQUESTS) exception: utils.exceptions.OrioksCantParseData') - safe_delete(path=path_users_to_file) + CommonHelper.safe_delete(path=path_users_to_file) return None if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): - await JsonFile.save(data=requests_dict, filename=path_users_to_file) + await JsonFileHelper.save(data=requests_dict, filename=path_users_to_file) return None - _old_json = await JsonFile.open(filename=path_users_to_file) - old_dict = JsonFile.convert_dict_keys_to_int(_old_json) + _old_json = await JsonFileHelper.open(filename=path_users_to_file) + old_dict = JsonFileHelper.convert_dict_keys_to_int(_old_json) try: diffs = compare(old_dict=old_dict, new_dict=requests_dict) - except exceptions.FileCompareError: - await JsonFile.save(data=requests_dict, filename=path_users_to_file) + except FileCompareException: + await JsonFileHelper.save(data=requests_dict, filename=path_users_to_file) return None if len(diffs) > 0: msg_to_send = await get_requests_to_msg(diffs=diffs) - await SendToTelegram.text_message_to_user(user_telegram_id=user_telegram_id, message=msg_to_send) - await JsonFile.save(data=requests_dict, filename=path_users_to_file) + await TelegramMessageHelper.text_message_to_user(user_telegram_id=user_telegram_id, message=msg_to_send) + await JsonFileHelper.save(data=requests_dict, filename=path_users_to_file) return None diff --git a/utils/delete_file.py b/utils/delete_file.py deleted file mode 100644 index 0769cd1..0000000 --- a/utils/delete_file.py +++ /dev/null @@ -1,10 +0,0 @@ -import os -from typing import Union -import pathlib - - -def safe_delete(path: Union[str, pathlib.Path]) -> None: - try: - os.remove(path) - except FileNotFoundError: - pass diff --git a/utils/exceptions.py b/utils/exceptions.py deleted file mode 100644 index dde873d..0000000 --- a/utils/exceptions.py +++ /dev/null @@ -1,10 +0,0 @@ -class OrioksInvalidLoginCredsError(Exception): - pass - - -class OrioksCantParseData(Exception): - pass - - -class FileCompareError(Exception): - pass diff --git a/utils/handle_orioks_logout.py b/utils/handle_orioks_logout.py deleted file mode 100644 index 8ea74c3..0000000 --- a/utils/handle_orioks_logout.py +++ /dev/null @@ -1,29 +0,0 @@ -import os -import db.user_status -import db.notify_settings -from config import Config -from utils.delete_file import safe_delete - - -def make_orioks_logout(user_telegram_id: int) -> None: - safe_delete(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) - - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) - - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) - - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) - - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) - - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', - f'{user_telegram_id}.json')) - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) - safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', - f'{user_telegram_id}.json')) - - db.user_status.update_user_orioks_authenticated_status( - user_telegram_id=user_telegram_id, - is_user_orioks_authenticated=False - ) - db.notify_settings.update_user_notify_settings_reset_to_default(user_telegram_id=user_telegram_id) diff --git a/utils/make_request.py b/utils/make_request.py deleted file mode 100644 index 1f4e34b..0000000 --- a/utils/make_request.py +++ /dev/null @@ -1,25 +0,0 @@ -import asyncio -import logging - -import db.admins_statistics -import aiohttp - -from config import Config - -_sem = asyncio.Semaphore(Config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) - - -async def get_request(url: str, session: aiohttp.ClientSession) -> str: - async with _sem: # next coroutine(s) will stuck here until the previous is done - await asyncio.sleep(Config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please - # TODO: is db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_telegram_id) - # else safe delete all user's file - # TODO: is db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) - # else safe delete non-enabled categories - logging.debug(f'get request to: {url}') - async with session.get(str(url)) as resp: - raw_html = await resp.text() - db.admins_statistics.update_inc_admins_statistics_row_name( # TODO: sum of requests and inc for one use db - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_scheduled_requests - ) - return raw_html diff --git a/utils/makedirs.py b/utils/makedirs.py deleted file mode 100644 index 2445c9e..0000000 --- a/utils/makedirs.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - -from config import Config - - -def make_dirs() -> None: - os.makedirs(os.path.join(Config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) - - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) - - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) - - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) - - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) - - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) diff --git a/utils/my_isdigit.py b/utils/my_isdigit.py deleted file mode 100644 index 98ae99e..0000000 --- a/utils/my_isdigit.py +++ /dev/null @@ -1,6 +0,0 @@ -def my_isdigit(x) -> bool: - try: - float(x) - return True - except ValueError: - return False diff --git a/utils/orioks.py b/utils/orioks.py deleted file mode 100644 index cfa1090..0000000 --- a/utils/orioks.py +++ /dev/null @@ -1,65 +0,0 @@ -import asyncio -import logging -import os -import pickle - -import aiohttp -from bs4 import BeautifulSoup - -import utils.exceptions -from datetime import datetime - -from config import Config -from utils.notify_to_user import SendToTelegram -import aiogram.utils.markdown as md - - -_sem = asyncio.Semaphore(Config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) - - -async def orioks_login_save_cookies(user_login: int, user_password: str, user_telegram_id: int) -> None: - user_queue = len(_sem._waiters) + 2 - if user_queue - 2 > 0: - logging.info(f'login: {user_queue=}') - _cats_queue_emoji = f'{"🐈" * (user_queue - 1)}🐈‍⬛' - await SendToTelegram.text_message_to_user( - user_telegram_id=user_telegram_id, - message=md.text( - md.text(_cats_queue_emoji), - md.text( - md.text(f'Твой номер в очереди на авторизацию: {user_queue}.'), - md.text('Ты получишь уведомление, когда она будет выполнена.'), - sep=' ', - ), - md.text('Это предотвращает слишком большую нагрузку на ОРИОКС'), - sep='\n', - ) - ) - async with _sem: # orioks dont die please - async with aiohttp.ClientSession( - timeout=Config.REQUESTS_TIMEOUT, - headers=Config.ORIOKS_REQUESTS_HEADERS - ) as session: - try: - logging.info(f'request to login: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') - async with session.get(str(Config.ORIOKS_PAGE_URLS['login'])) as resp: - bs_content = BeautifulSoup(await resp.text(), "html.parser") - _csrf_token = bs_content.find('input', {'name': '_csrf'})['value'] - login_data = { - 'LoginForm[login]': int(user_login), - 'LoginForm[password]': str(user_password), - 'LoginForm[rememberMe]': 1, - '_csrf': _csrf_token, - } - except asyncio.TimeoutError as e: - raise e - try: - async with session.post(str(Config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: - if str(resp.url) == Config.ORIOKS_PAGE_URLS['login']: - raise utils.exceptions.OrioksInvalidLoginCredsError - except asyncio.TimeoutError as e: - raise e - - cookies = session.cookie_jar.filter_cookies(resp.url) - pickle.dump(cookies, open(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) - await asyncio.sleep(1) From d9a76a102e098843eff309bf973f839bf26fca5a Mon Sep 17 00:00:00 2001 From: Alexey Voronin Date: Tue, 14 Jun 2022 04:49:35 +0300 Subject: [PATCH 3/5] feature(sqlalchemy+alembic): added db connection + first migration --- .env.example | 3 + .env.template | 3 + .gitignore | 4 + alembic.ini | 105 ++++++++++++++++++ app/__init__.py | 35 ++++-- .../assets/fonts}/PTSansCaption-Bold.ttf | Bin .../source => app/assets/images}/green.png | Bin {images/source => app/assets/images}/news.png | Bin .../source => app/assets/images}/orange.png | Bin {images/source => app/assets/images}/red.png | Bin {images/source => app/assets/images}/salt.png | Bin .../source => app/assets/images}/yellow.png | Bin app/fixtures/AbstractFixture.py | 37 ++++++ app/fixtures/AdminStatisticFixture.py | 14 +++ app/fixtures/__init__.py | 11 ++ app/handlers/__init__.py | 4 +- app/helpers/AssetsHelper.py | 16 +++ app/helpers/CommonHelper.py | 18 +-- .../helpers/MarksPictureHelper.py | 38 ++----- app/helpers/OrioksHelper.py | 35 +++--- app/helpers/RequestHelper.py | 6 +- app/helpers/StorageHelper.py | 4 + app/helpers/TelegramMessageHelper.py | 4 +- app/helpers/__init__.py | 6 +- app/middlewares/AdminCommandsMiddleware.py | 4 +- .../UserOrioksAttemptsMiddleware.py | 4 +- app/migrations/README | 1 + app/migrations/env.py | 85 ++++++++++++++ app/migrations/script.py.mako | 24 ++++ .../e09d97658b84_initial_migration.py | 60 ++++++++++ app/models/__init__.py | 7 +- app/models/admins/AdminStatistics.py | 7 ++ app/models/users/UserNotifySettings.py | 2 + app/models/users/UserStatus.py | 2 + checking/homeworks/get_orioks_homeworks.py | 4 +- checking/marks/get_orioks_marks.py | 13 +-- checking/news/get_orioks_news.py | 21 ++-- checking/on_startup.py | 26 ++--- checking/requests/get_orioks_requests.py | 4 +- config.py | 15 ++- db/admins_statistics.py | 28 +---- db/notify_settings.py | 18 +-- db/sql/create_admins_statistics.sql | 5 - db/sql/init_admins_statistics.sql | 2 - db/user_first_add.py | 2 +- db/user_status.py | 22 ++-- requirements.txt | 22 +++- 47 files changed, 550 insertions(+), 171 deletions(-) create mode 100644 .env.example create mode 100644 .env.template create mode 100644 alembic.ini rename {images/source => app/assets/fonts}/PTSansCaption-Bold.ttf (100%) rename {images/source => app/assets/images}/green.png (100%) rename {images/source => app/assets/images}/news.png (100%) rename {images/source => app/assets/images}/orange.png (100%) rename {images/source => app/assets/images}/red.png (100%) rename {images/source => app/assets/images}/salt.png (100%) rename {images/source => app/assets/images}/yellow.png (100%) create mode 100644 app/fixtures/AbstractFixture.py create mode 100644 app/fixtures/AdminStatisticFixture.py create mode 100644 app/fixtures/__init__.py create mode 100644 app/helpers/AssetsHelper.py rename images/imager.py => app/helpers/MarksPictureHelper.py (84%) create mode 100644 app/helpers/StorageHelper.py create mode 100644 app/migrations/README create mode 100644 app/migrations/env.py create mode 100644 app/migrations/script.py.mako create mode 100644 app/migrations/versions/e09d97658b84_initial_migration.py delete mode 100644 db/sql/create_admins_statistics.sql delete mode 100644 db/sql/init_admins_statistics.sql diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..09c1675 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +TELEGRAM_BOT_API_TOKEN=0123456789:AAEf-L4BLFSfUxHYgtY-HvZgE-0123456789 +TELEGRAM_ADMIN_IDS_LIST=["1234567890"] +DATABASE_URL=sqlite:///database.sqlite3 \ No newline at end of file diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..f13ae88 --- /dev/null +++ b/.env.template @@ -0,0 +1,3 @@ +TELEGRAM_BOT_API_TOKEN=$TELEGRAM_BOT_API_TOKEN +TELEGRAM_ADMIN_IDS_LIST=$TELEGRAM_ADMIN_IDS_LIST +DATABASE_URL=$DATABASE_URL \ No newline at end of file diff --git a/.gitignore b/.gitignore index 6f44445..9091372 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,7 @@ setenv.sh users_data/**/*.json users_data/**/*.pkl *.db + +.env + +database.sqlite3 diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..ca52819 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,105 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = app/migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to app/migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:app/migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = driver://user:pass@localhost/dbname + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/__init__.py b/app/__init__.py index 1b2e619..aff0dc5 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -1,37 +1,50 @@ import logging +import os from aiogram import Bot, Dispatcher, types from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.utils import executor -from sqlalchemy import create_engine -from sqlalchemy.orm import scoped_session, sessionmaker -from app.handlers import register_handlers from app.helpers import CommonHelper from app.middlewares import UserAgreementMiddleware, UserOrioksAttemptsMiddleware, AdminCommandsMiddleware + from checking import on_startup -from config import Config +from config import config -import db.admins_statistics -bot = Bot(token=Config.TELEGRAM_BOT_API_TOKEN, parse_mode=types.ParseMode.HTML) -storage = MemoryStorage() -dispatcher = Dispatcher(bot, storage=storage) +def initialize_database(): + from sqlalchemy import create_engine + from sqlalchemy.orm import scoped_session, sessionmaker + + engine = create_engine(config.DATABASE_URL, convert_unicode=True) + return scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) + -engine = create_engine('sqlite:///database.sqlite3', convert_unicode=True) -db_session = scoped_session(sessionmaker(autocommit=False, autoflush=False, bind=engine)) +def initialize_assets(): + from app.helpers.AssetsHelper import assetsHelper + current_folder_path = os.path.dirname(os.path.abspath(__file__)) + assetsHelper.initialize(f'{current_folder_path}/storage') def _settings_before_start() -> None: + from app.handlers import register_handlers + from app.fixtures import initialize_default_values + register_handlers(dispatcher=dispatcher) - db.admins_statistics.create_and_init_admins_statistics() + initialize_default_values() dispatcher.middleware.setup(UserAgreementMiddleware()) dispatcher.middleware.setup(UserOrioksAttemptsMiddleware()) dispatcher.middleware.setup(AdminCommandsMiddleware()) CommonHelper.make_dirs() +bot = Bot(token=config.TELEGRAM_BOT_API_TOKEN, parse_mode=types.ParseMode.HTML) +storage = MemoryStorage() +dispatcher = Dispatcher(bot, storage=storage) + +db_session = initialize_database() + def run(): logging.basicConfig(level=logging.INFO) _settings_before_start() diff --git a/images/source/PTSansCaption-Bold.ttf b/app/assets/fonts/PTSansCaption-Bold.ttf similarity index 100% rename from images/source/PTSansCaption-Bold.ttf rename to app/assets/fonts/PTSansCaption-Bold.ttf diff --git a/images/source/green.png b/app/assets/images/green.png similarity index 100% rename from images/source/green.png rename to app/assets/images/green.png diff --git a/images/source/news.png b/app/assets/images/news.png similarity index 100% rename from images/source/news.png rename to app/assets/images/news.png diff --git a/images/source/orange.png b/app/assets/images/orange.png similarity index 100% rename from images/source/orange.png rename to app/assets/images/orange.png diff --git a/images/source/red.png b/app/assets/images/red.png similarity index 100% rename from images/source/red.png rename to app/assets/images/red.png diff --git a/images/source/salt.png b/app/assets/images/salt.png similarity index 100% rename from images/source/salt.png rename to app/assets/images/salt.png diff --git a/images/source/yellow.png b/app/assets/images/yellow.png similarity index 100% rename from images/source/yellow.png rename to app/assets/images/yellow.png diff --git a/app/fixtures/AbstractFixture.py b/app/fixtures/AbstractFixture.py new file mode 100644 index 0000000..38a1745 --- /dev/null +++ b/app/fixtures/AbstractFixture.py @@ -0,0 +1,37 @@ +from abc import abstractmethod +from typing import Type, List, Dict + +from app.models import BaseModel + + +class AbstractFixture: + + @property + @abstractmethod + def model(self) -> Type[BaseModel]: + raise NotImplementedError + + @property + def fill_method_name(self) -> str: + return 'fill' + + @abstractmethod + def values(self) -> List[Dict]: + raise NotImplementedError + + def need_to_add_values(self) -> bool: + items_count = self.model.query.count() + if items_count > 0: + return False + return True + + def insert_data(self) -> bool: + if not self.need_to_add_values(): + return False + + values = self.values() if callable(self.values) else self.values + + for value in values: + model = self.model() + getattr(model, self.fill_method_name)(**value) + model.save() diff --git a/app/fixtures/AdminStatisticFixture.py b/app/fixtures/AdminStatisticFixture.py new file mode 100644 index 0000000..3b4361f --- /dev/null +++ b/app/fixtures/AdminStatisticFixture.py @@ -0,0 +1,14 @@ +from app.fixtures import AbstractFixture +from app.models.admins import AdminStatistics + + +class AdminStatisticFixture(AbstractFixture): + + model = AdminStatistics + values = [ + { + 'scheduled_requests': 0, + 'success_logins': 0, + 'failed_logins': 0 + } + ] diff --git a/app/fixtures/__init__.py b/app/fixtures/__init__.py new file mode 100644 index 0000000..93474a9 --- /dev/null +++ b/app/fixtures/__init__.py @@ -0,0 +1,11 @@ +from .AbstractFixture import AbstractFixture +from .AdminStatisticFixture import AdminStatisticFixture + +__all__ = ['AbstractFixture', 'AdminStatisticFixture'] + + +def initialize_default_values(): + fixture_classes = [AdminStatisticFixture] + for fixture_class in fixture_classes: + fixture_class = fixture_class() + fixture_class.insert_data() diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py index 3b80cd5..7f329c2 100644 --- a/app/handlers/__init__.py +++ b/app/handlers/__init__.py @@ -2,7 +2,7 @@ from aiogram import Dispatcher -from config import Config +from config import config from .AbstractCommandHandler import AbstractCommandHandler from .AbstractCallbackHandler import AbstractCallbackHandler @@ -44,7 +44,7 @@ def register_handlers(dispatcher: Dispatcher) -> None: UserAgreementCallbackHandler.process, lambda c: c.data == 'button_user_agreement_accept' ) dispatcher.register_callback_query_handler( - SettingsCallbackHandler.process, lambda c: c.data in Config.notify_settings_btns + SettingsCallbackHandler.process, lambda c: c.data in config.notify_settings_btns ) # Errors diff --git a/app/helpers/AssetsHelper.py b/app/helpers/AssetsHelper.py new file mode 100644 index 0000000..35f5af6 --- /dev/null +++ b/app/helpers/AssetsHelper.py @@ -0,0 +1,16 @@ +class AssetsHelper: + + def __init__(self): + self.__abs_assets_path = None + + def initialize(self, path: str): + self.__abs_assets_path = path + + def get_assets_path(self): + return self.__abs_assets_path + + def make_full_path(self, relative_path): + return f'{self.get_assets_path()}/{relative_path}' + + +assetsHelper = AssetsHelper() diff --git a/app/helpers/CommonHelper.py b/app/helpers/CommonHelper.py index 066c722..fedb325 100644 --- a/app/helpers/CommonHelper.py +++ b/app/helpers/CommonHelper.py @@ -2,21 +2,21 @@ import pathlib from typing import Union -from config import Config +from config import config class CommonHelper: @staticmethod def make_dirs() -> None: - os.makedirs(os.path.join(Config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) - os.makedirs(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) + os.makedirs(os.path.join(config.BASEDIR, 'users_data', 'cookies'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc'), exist_ok=True) + os.makedirs(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference'), exist_ok=True) @staticmethod def is_correct_convert_to_float(x) -> bool: diff --git a/images/imager.py b/app/helpers/MarksPictureHelper.py similarity index 84% rename from images/imager.py rename to app/helpers/MarksPictureHelper.py index 48384a1..c09f56a 100644 --- a/images/imager.py +++ b/app/helpers/MarksPictureHelper.py @@ -2,26 +2,17 @@ import os import qrcode as qrcode from PIL import Image, ImageDraw, ImageFont -from typing import NamedTuple import pathlib import secrets +from app.helpers.AssetsHelper import assetsHelper from config import Config -class PathToImages(NamedTuple): - one: pathlib.Path - two: pathlib.Path - three: pathlib.Path - four: pathlib.Path - five: pathlib.Path - news: pathlib.Path +class MarksPictureHelper: - -class Imager: def __init__(self): - self._base_dir = os.path.join(Config.BASEDIR, 'images', 'source') - self._font_path = os.path.join(self._base_dir, 'PTSansCaption-Bold.ttf') + self._font_path = assetsHelper.make_full_path('fonts/PTSansCaption-Bold.ttf') self._font_upper_size = 64 self._font_downer_size = 62 @@ -34,31 +25,22 @@ def __init__(self): self._width_line = 27 - self._background_paths = PathToImages( - one=pathlib.Path(os.path.join(self._base_dir, 'red.png')), - two=pathlib.Path(os.path.join(self._base_dir, 'orange.png')), - three=pathlib.Path(os.path.join(self._base_dir, 'yellow.png')), - four=pathlib.Path(os.path.join(self._base_dir, 'salt.png')), - five=pathlib.Path(os.path.join(self._base_dir, 'green.png')), - news=pathlib.Path(os.path.join(self._base_dir, 'news.png')), - ) - def _get_image_by_grade(self, current_grade, max_grade): if current_grade == 0: - self.image = Image.open(self._background_paths.one) + self.image = Image.open(assetsHelper.make_full_path('images/red.png')) elif current_grade / max_grade < 0.5: - self.image = Image.open(self._background_paths.two) + self.image = Image.open(assetsHelper.make_full_path('images/orange.png')) elif current_grade / max_grade < 0.7: - self.image = Image.open(self._background_paths.three) + self.image = Image.open(assetsHelper.make_full_path('images/yellow.png')) elif current_grade / max_grade < 0.85: - self.image = Image.open(self._background_paths.four) + self.image = Image.open(assetsHelper.make_full_path('images/salt.png')) elif current_grade / max_grade >= 0.85: - self.image = Image.open(self._background_paths.five) + self.image = Image.open(assetsHelper.make_full_path('images/green.png')) self.draw_text = ImageDraw.Draw(self.image) self.image_weight, self.image_height = self.image.size def _get_news_image(self): - self.image = Image.open(self._background_paths.news) + self.image = Image.open(assetsHelper.make_full_path('images/news.png')) self.draw_text = ImageDraw.Draw(self.image) self.image_weight, self.image_height = self.image.size @@ -203,4 +185,4 @@ def get_image_news( self._calculate_font_size_and_text_width(title_text, side_text, need_qr=True) self._draw_text_news(title_text, side_text, url) self.image.save(path_to_result_image) - return path_to_result_image + return path_to_result_image \ No newline at end of file diff --git a/app/helpers/OrioksHelper.py b/app/helpers/OrioksHelper.py index f12b40d..2a7a8fe 100644 --- a/app/helpers/OrioksHelper.py +++ b/app/helpers/OrioksHelper.py @@ -8,16 +8,15 @@ from datetime import datetime -from app import CommonHelper from app.exceptions import OrioksInvalidLoginCredentialsException -from app.helpers import TelegramMessageHelper -from config import Config +from app.helpers import TelegramMessageHelper, CommonHelper import aiogram.utils.markdown as md import db.user_status import db.notify_settings +from config import config -_sem = asyncio.Semaphore(Config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) +_sem = asyncio.Semaphore(config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) class OrioksHelper: @@ -44,12 +43,12 @@ async def orioks_login_save_cookies(user_login: int, user_password: str, user_te ) async with _sem: # orioks dont die please async with aiohttp.ClientSession( - timeout=Config.REQUESTS_TIMEOUT, - headers=Config.ORIOKS_REQUESTS_HEADERS + timeout=config.REQUESTS_TIMEOUT, + headers=config.ORIOKS_REQUESTS_HEADERS ) as session: try: logging.info(f'request to login: {datetime.now().strftime("%H:%M:%S %d.%m.%Y")}') - async with session.get(str(Config.ORIOKS_PAGE_URLS['login'])) as resp: + async with session.get(str(config.ORIOKS_PAGE_URLS['login'])) as resp: bs_content = BeautifulSoup(await resp.text(), "html.parser") _csrf_token = bs_content.find('input', {'name': '_csrf'})['value'] login_data = { @@ -61,27 +60,27 @@ async def orioks_login_save_cookies(user_login: int, user_password: str, user_te except asyncio.TimeoutError as e: raise e try: - async with session.post(str(Config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: - if str(resp.url) == Config.ORIOKS_PAGE_URLS['login']: + async with session.post(str(config.ORIOKS_PAGE_URLS['login']), data=login_data) as resp: + if str(resp.url) == config.ORIOKS_PAGE_URLS['login']: raise OrioksInvalidLoginCredentialsException except asyncio.TimeoutError as e: raise e cookies = session.cookie_jar.filter_cookies(resp.url) pickle.dump(cookies, - open(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) + open(os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl'), 'wb')) await asyncio.sleep(1) @staticmethod def make_orioks_logout(user_telegram_id: int) -> None: - CommonHelper.safe_delete(os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) - CommonHelper.safe_delete(os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'questionnaire', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) + CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', f'{user_telegram_id}.json')) db.user_status.update_user_orioks_authenticated_status(user_telegram_id=user_telegram_id, is_user_orioks_authenticated=False) db.notify_settings.update_user_notify_settings_reset_to_default(user_telegram_id=user_telegram_id) diff --git a/app/helpers/RequestHelper.py b/app/helpers/RequestHelper.py index 1495b40..a0d8a47 100644 --- a/app/helpers/RequestHelper.py +++ b/app/helpers/RequestHelper.py @@ -4,16 +4,16 @@ import db.admins_statistics import aiohttp -from config import Config +from config import config class RequestHelper: - _sem = asyncio.Semaphore(Config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) + _sem = asyncio.Semaphore(config.ORIOKS_REQUESTS_SEMAPHORE_VALUE) @staticmethod async def get_request(url: str, session: aiohttp.ClientSession) -> str: async with RequestHelper._sem: # next coroutine(s) will stuck here until the previous is done - await asyncio.sleep(Config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please + await asyncio.sleep(config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please # TODO: is db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_telegram_id) # else safe delete all user's file # TODO: is db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) diff --git a/app/helpers/StorageHelper.py b/app/helpers/StorageHelper.py new file mode 100644 index 0000000..08ea544 --- /dev/null +++ b/app/helpers/StorageHelper.py @@ -0,0 +1,4 @@ + + +class StorageHelper: + pass diff --git a/app/helpers/TelegramMessageHelper.py b/app/helpers/TelegramMessageHelper.py index 4294eff..cb8c718 100644 --- a/app/helpers/TelegramMessageHelper.py +++ b/app/helpers/TelegramMessageHelper.py @@ -6,7 +6,7 @@ import app from app.helpers import OrioksHelper -from config import Config +from config import config class TelegramMessageHelper: @@ -27,7 +27,7 @@ async def photo_message_to_user(user_telegram_id: int, photo_path: pathlib.Path, @staticmethod async def message_to_admins(message: str) -> None: - for admin_telegram_id in Config.TELEGRAM_ADMIN_IDS_LIST: + for admin_telegram_id in config.TELEGRAM_ADMIN_IDS_LIST: await app.bot.send_message( admin_telegram_id, markdown.text( diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py index c22be8e..e375614 100644 --- a/app/helpers/__init__.py +++ b/app/helpers/__init__.py @@ -1,10 +1,12 @@ from .CommonHelper import CommonHelper +from .AssetsHelper import AssetsHelper from .JsonFileHelper import JsonFileHelper from .OrioksHelper import OrioksHelper from .RequestHelper import RequestHelper from .TelegramMessageHelper import TelegramMessageHelper +from .MarksPictureHelper import MarksPictureHelper __all__ = [ - 'CommonHelper', 'JsonFileHelper', 'OrioksHelper', - 'RequestHelper', 'TelegramMessageHelper' + 'CommonHelper', 'AssetsHelper', 'JsonFileHelper', 'OrioksHelper', + 'RequestHelper', 'TelegramMessageHelper', 'MarksPictureHelper' ] diff --git a/app/middlewares/AdminCommandsMiddleware.py b/app/middlewares/AdminCommandsMiddleware.py index 5681c79..089141a 100644 --- a/app/middlewares/AdminCommandsMiddleware.py +++ b/app/middlewares/AdminCommandsMiddleware.py @@ -2,7 +2,7 @@ from aiogram.dispatcher.handler import CancelHandler from aiogram.dispatcher.middlewares import BaseMiddleware -from config import Config +from config import config class AdminCommandsMiddleware(BaseMiddleware): @@ -12,5 +12,5 @@ class AdminCommandsMiddleware(BaseMiddleware): # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): - if message.get_command() in ('/stat',) and message.from_user.id not in Config.TELEGRAM_ADMIN_IDS_LIST: + if message.get_command() in ('/stat',) and message.from_user.id not in config.TELEGRAM_ADMIN_IDS_LIST: raise CancelHandler() diff --git a/app/middlewares/UserOrioksAttemptsMiddleware.py b/app/middlewares/UserOrioksAttemptsMiddleware.py index 9bb4c78..0ec4ab3 100644 --- a/app/middlewares/UserOrioksAttemptsMiddleware.py +++ b/app/middlewares/UserOrioksAttemptsMiddleware.py @@ -3,7 +3,7 @@ from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils import markdown -from config import Config +from config import config import db.user_first_add import db.user_status @@ -19,7 +19,7 @@ class UserOrioksAttemptsMiddleware(BaseMiddleware): # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): if db.user_status.get_user_orioks_attempts( - user_telegram_id=message.from_user.id) > Config.ORIOKS_MAX_LOGIN_TRIES: + user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: await message.reply( markdown.text( markdown.hbold('Ты совершил подозрительно много попыток входа в аккаунт ОРИОКС.'), diff --git a/app/migrations/README b/app/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/app/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/app/migrations/env.py b/app/migrations/env.py new file mode 100644 index 0000000..4c8cf0f --- /dev/null +++ b/app/migrations/env.py @@ -0,0 +1,85 @@ +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +from app.models import DeclarativeModelBase +from config import config as application_config + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +config.set_main_option('sqlalchemy.url', str(application_config.DATABASE_URL).replace('%', '%%')) + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +target_metadata = DeclarativeModelBase.metadata + + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def run_migrations_offline() -> None: + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online() -> None: + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + connectable = engine_from_config( + config.get_section(config.config_ini_section), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata, compare_type=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/app/migrations/script.py.mako b/app/migrations/script.py.mako new file mode 100644 index 0000000..55df286 --- /dev/null +++ b/app/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/app/migrations/versions/e09d97658b84_initial_migration.py b/app/migrations/versions/e09d97658b84_initial_migration.py new file mode 100644 index 0000000..87d8c07 --- /dev/null +++ b/app/migrations/versions/e09d97658b84_initial_migration.py @@ -0,0 +1,60 @@ +"""Initial migration + +Revision ID: e09d97658b84 +Revises: +Create Date: 2022-06-14 04:37:58.451042 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'e09d97658b84' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('admin_statistics', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('scheduled_requests', sa.Integer(), nullable=False), + sa.Column('success_logins', sa.Integer(), nullable=False), + sa.Column('failed_logins', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_notify_settings', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('user_telegram_id', sa.Integer(), nullable=False), + sa.Column('marks', sa.Boolean(), nullable=False), + sa.Column('news', sa.Boolean(), nullable=False), + sa.Column('discipline_sources', sa.Boolean(), nullable=False), + sa.Column('homeworks', sa.Boolean(), nullable=False), + sa.Column('requests', sa.Boolean(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('user_status', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('user_telegram_id', sa.Integer(), nullable=False), + sa.Column('agreement_accepted', sa.Boolean(), nullable=False), + sa.Column('authenticated', sa.Boolean(), nullable=False), + sa.Column('login_attempt_count', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('user_status') + op.drop_table('user_notify_settings') + op.drop_table('admin_statistics') + # ### end Alembic commands ### diff --git a/app/models/__init__.py b/app/models/__init__.py index 83cf4e5..ce3bac6 100644 --- a/app/models/__init__.py +++ b/app/models/__init__.py @@ -1,7 +1,10 @@ -from sqlalchemy.orm import declarative_base +from sqlalchemy.orm import declarative_base, scoped_session -DeclarativeModelBase = declarative_base() +from .. import db_session +session = scoped_session(db_session) +DeclarativeModelBase = declarative_base() +DeclarativeModelBase.query = session.query_property() from .BaseModel import BaseModel __all__ = ['DeclarativeModelBase', 'BaseModel'] diff --git a/app/models/admins/AdminStatistics.py b/app/models/admins/AdminStatistics.py index ec3c869..7041c45 100644 --- a/app/models/admins/AdminStatistics.py +++ b/app/models/admins/AdminStatistics.py @@ -5,6 +5,13 @@ class AdminStatistics(BaseModel): + __tablename__ = 'admin_statistics' + scheduled_requests = Column(Integer, nullable=False, default=0) success_logins = Column(Integer, nullable=False, default=0) failed_logins = Column(Integer, nullable=False, default=0) + + def fill(self, scheduled_requests: int, success_logins: int, failed_logins: int): + self.scheduled_requests = scheduled_requests + self.success_logins = success_logins + self.failed_logins = failed_logins diff --git a/app/models/users/UserNotifySettings.py b/app/models/users/UserNotifySettings.py index 55c5683..18eed27 100644 --- a/app/models/users/UserNotifySettings.py +++ b/app/models/users/UserNotifySettings.py @@ -5,6 +5,8 @@ class UserNotifySettings(BaseModel): + __tablename__ = 'user_notify_settings' + user_telegram_id = Column(Integer, nullable=False) marks = Column(Boolean, nullable=False, default=True) news = Column(Boolean, nullable=False, default=True) diff --git a/app/models/users/UserStatus.py b/app/models/users/UserStatus.py index b60bf9b..cec0c36 100644 --- a/app/models/users/UserStatus.py +++ b/app/models/users/UserStatus.py @@ -5,6 +5,8 @@ class UserStatus(BaseModel): + __tablename__ = 'user_status' + user_telegram_id = Column(Integer, nullable=False) agreement_accepted = Column(Boolean, nullable=False, default=False) authenticated = Column(Boolean, nullable=False, default=False) diff --git a/checking/homeworks/get_orioks_homeworks.py b/checking/homeworks/get_orioks_homeworks.py index 73d97ef..6e833b1 100644 --- a/checking/homeworks/get_orioks_homeworks.py +++ b/checking/homeworks/get_orioks_homeworks.py @@ -25,14 +25,14 @@ def _orioks_parse_homeworks(raw_html: str) -> dict: 'about': { 'discipline': tr.find_all('td')[3].text, 'task': tr.find_all('td')[4].text, - 'url': Config.ORIOKS_PAGE_URLS['masks']['homeworks'].format(id=_thread_id), + 'url': config.ORIOKS_PAGE_URLS['masks']['homeworks'].format(id=_thread_id), }, } return homeworks async def get_orioks_homeworks(session: aiohttp.ClientSession) -> dict: - raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) + raw_html = await RequestHelper.get_request(url=config.ORIOKS_PAGE_URLS['notify']['homeworks'], session=session) return _orioks_parse_homeworks(raw_html) diff --git a/checking/marks/get_orioks_marks.py b/checking/marks/get_orioks_marks.py index 87ffaf4..ec85bb6 100644 --- a/checking/marks/get_orioks_marks.py +++ b/checking/marks/get_orioks_marks.py @@ -7,10 +7,9 @@ import logging from app.exceptions import OrioksParseDataException, FileCompareException -from app.helpers import CommonHelper, RequestHelper, TelegramMessageHelper, JsonFileHelper +from app.helpers import CommonHelper, RequestHelper, TelegramMessageHelper, JsonFileHelper, MarksPictureHelper from checking.marks.compares import file_compares, get_discipline_objs_from_diff -from config import Config -from images.imager import Imager +from config import config @dataclass @@ -95,13 +94,13 @@ def _get_orioks_forang(raw_html: str): async def get_orioks_marks(session: aiohttp.ClientSession): - raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) + raw_html = await RequestHelper.get_request(url=config.ORIOKS_PAGE_URLS['notify']['marks'], session=session) return _get_orioks_forang(raw_html) async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession) -> None: - student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'marks', student_json_file) + student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'marks', student_json_file) try: detailed_info = await get_orioks_marks(session=session) except FileNotFoundError as exception: @@ -132,7 +131,7 @@ async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession if len(diffs) > 0: for discipline_obj in get_discipline_objs_from_diff(diffs=diffs): - photo_path = Imager().get_image_marks( + photo_path = MarksPictureHelper().get_image_marks( current_grade=discipline_obj.current_grade, max_grade=discipline_obj.max_grade, title_text=discipline_obj.title_text, diff --git a/checking/news/get_orioks_news.py b/checking/news/get_orioks_news.py index b6a8b56..50b9bd5 100644 --- a/checking/news/get_orioks_news.py +++ b/checking/news/get_orioks_news.py @@ -6,10 +6,9 @@ from bs4 import BeautifulSoup from app.exceptions import OrioksParseDataException -from app.helpers import RequestHelper, CommonHelper, JsonFileHelper, TelegramMessageHelper -from config import Config +from app.helpers import RequestHelper, CommonHelper, JsonFileHelper, TelegramMessageHelper, MarksPictureHelper +from config import config import aiogram.utils.markdown as md -from images.imager import Imager from typing import NamedTuple @@ -32,7 +31,7 @@ def _orioks_parse_news(raw_html: str) -> dict: async def get_orioks_news(session: aiohttp.ClientSession) -> dict: - raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['news'], session=session) + raw_html = await RequestHelper.get_request(url=config.ORIOKS_PAGE_URLS['notify']['news'], session=session) return _orioks_parse_news(raw_html) @@ -42,7 +41,7 @@ def _find_in_str_with_beginning_and_ending(string_to_find: str, beginning: str, async def get_news_by_news_id(news_id: int, session: aiohttp.ClientSession) -> NewsObject: - raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) + raw_html = await RequestHelper.get_request(url=config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), session=session) bs_content = BeautifulSoup(raw_html, "html.parser") well_raw = bs_content.find_all('div', {'class': 'well'})[0] return NewsObject( @@ -50,7 +49,7 @@ async def get_news_by_news_id(news_id: int, session: aiohttp.ClientSession) -> N string_to_find=well_raw.text, beginning='Заголовок:', ending='Тело новости:'), - url=Config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), + url=config.ORIOKS_PAGE_URLS['masks']['news'].format(id=news_id), id=news_id ) @@ -73,8 +72,8 @@ def transform_news_to_msg(news_obj: NewsObject) -> str: async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) -> NewsObject: - student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) + student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) try: last_news_id = await get_orioks_news(session=session) except OrioksParseDataException as exception: @@ -86,8 +85,8 @@ async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) async def user_news_check_from_news_id(user_telegram_id: int, session: aiohttp.ClientSession, current_new: NewsObject) -> None: - student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) + student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'news', student_json_file) last_news_id = {'last_id': current_new.id} if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): await JsonFileHelper.save(data=last_news_id, filename=path_users_to_file) @@ -112,7 +111,7 @@ async def user_news_check_from_news_id(user_telegram_id: int, session: aiohttp.C news_obj = await get_news_by_news_id(news_id=news_id, session=session) except IndexError: continue # id новостей могут идти не по порядку, поэтому надо игнорировать IndexError - path_to_img = Imager().get_image_news( + path_to_img = MarksPictureHelper().get_image_news( title_text=news_obj.headline_news, side_text='Опубликована новость', url=news_obj.url diff --git a/checking/on_startup.py b/checking/on_startup.py index 5fe7a50..80ec07e 100644 --- a/checking/on_startup.py +++ b/checking/on_startup.py @@ -17,34 +17,34 @@ from checking.homeworks.get_orioks_homeworks import user_homeworks_check from checking.requests.get_orioks_requests import user_requests_check from http.cookies import SimpleCookie -from config import Config +from config import config def _get_user_orioks_cookies_from_telegram_id(user_telegram_id: int) -> SimpleCookie: - path_to_cookies = os.path.join(Config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl') + path_to_cookies = os.path.join(config.BASEDIR, 'users_data', 'cookies', f'{user_telegram_id}.pkl') return SimpleCookie(pickle.load(open(path_to_cookies, 'rb'))) def _delete_users_tracking_data_in_notify_settings_off(user_telegram_id: int, user_notify_settings: dict) -> None: if not user_notify_settings['marks']: CommonHelper.safe_delete( - os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') + os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') ) if not user_notify_settings['news']: CommonHelper.safe_delete( - os.path.join(Config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') + os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') ) if not user_notify_settings['discipline_sources']: CommonHelper.safe_delete(os.path.join( - Config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') + config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') ) if not user_notify_settings['homeworks']: CommonHelper.safe_delete(os.path.join( - Config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') + config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') ) if not user_notify_settings['requests']: CommonHelper.safe_delete(os.path.join( - Config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') + config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') ) @@ -53,8 +53,8 @@ async def make_one_user_check(user_telegram_id: int) -> None: cookies = _get_user_orioks_cookies_from_telegram_id(user_telegram_id=user_telegram_id) async with aiohttp.ClientSession( cookies=cookies, - timeout=Config.REQUESTS_TIMEOUT, - headers=Config.ORIOKS_REQUESTS_HEADERS + timeout=config.REQUESTS_TIMEOUT, + headers=config.ORIOKS_REQUESTS_HEADERS ) as session: if user_notify_settings['marks']: await user_marks_check(user_telegram_id=user_telegram_id, session=session) @@ -82,8 +82,8 @@ async def make_all_users_news_check(tries_counter: int = 0) -> list: try: async with aiohttp.ClientSession( cookies=cookies, - timeout=Config.REQUESTS_TIMEOUT, - headers=Config.ORIOKS_REQUESTS_HEADERS + timeout=config.REQUESTS_TIMEOUT, + headers=config.ORIOKS_REQUESTS_HEADERS ) as session: current_new = await get_current_new(user_telegram_id=picked_user_to_check_news, session=session) except OrioksParseDataException: @@ -94,7 +94,7 @@ async def make_all_users_news_check(tries_counter: int = 0) -> list: except FileNotFoundError: logging.error('(COOKIES) FileNotFoundError: %s' % (user_telegram_id, )) continue - user_session = aiohttp.ClientSession(cookies=cookies, timeout=Config.REQUESTS_TIMEOUT) + user_session = aiohttp.ClientSession(cookies=cookies, timeout=config.REQUESTS_TIMEOUT) tasks.append(user_news_check_from_news_id( user_telegram_id=user_telegram_id, session=user_session, @@ -129,7 +129,7 @@ async def do_checks(): async def scheduler(): await TelegramMessageHelper.message_to_admins(message='Бот запущен!') - aioschedule.every(Config.ORIOKS_SECONDS_BETWEEN_WAVES).seconds.do(do_checks) + aioschedule.every(config.ORIOKS_SECONDS_BETWEEN_WAVES).seconds.do(do_checks) while True: await aioschedule.run_pending() await asyncio.sleep(1) diff --git a/checking/requests/get_orioks_requests.py b/checking/requests/get_orioks_requests.py index 6dd6454..e535b72 100644 --- a/checking/requests/get_orioks_requests.py +++ b/checking/requests/get_orioks_requests.py @@ -27,14 +27,14 @@ def _orioks_parse_requests(raw_html: str, section: str) -> dict: 'new_messages': int(tr.find_all('td')[new_messages_td_list_index].select_one('b').text), 'about': { 'name': tr.find_all('td')[3].text, - 'url': Config.ORIOKS_PAGE_URLS['masks']['requests'][section].format(id=_thread_id), + 'url': config.ORIOKS_PAGE_URLS['masks']['requests'][section].format(id=_thread_id), }, } return requests async def get_orioks_requests(section: str, session: aiohttp.ClientSession) -> dict: - raw_html = await RequestHelper.get_request(url=Config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) + raw_html = await RequestHelper.get_request(url=config.ORIOKS_PAGE_URLS['notify']['requests'][section], session=session) return _orioks_parse_requests(raw_html=raw_html, section=section) diff --git a/config.py b/config.py index 8fbb845..b93b84f 100644 --- a/config.py +++ b/config.py @@ -1,10 +1,10 @@ import os import json import aiohttp +from dotenv import load_dotenv class Config: - TELEGRAM_BOT_API_TOKEN = os.getenv('TELEGRAM_BOT_API_TOKEN') BASEDIR = os.path.dirname(os.path.abspath(__file__)) STUDENT_FILE_JSON_MASK = '{id}.json' @@ -21,8 +21,6 @@ class Config: 'notify_settings-requests' ) - TELEGRAM_ADMIN_IDS_LIST = json.loads(os.environ['TELEGRAM_ADMIN_IDS_LIST']) - ORIOKS_MAX_LOGIN_TRIES = 10 TELEGRAM_STICKER_LOADER = 'CAACAgIAAxkBAAEEIlpiLSwO28zurkSJGRj6J9SLBIAHYQACIwADKA9qFCdRJeeMIKQGIwQ' @@ -63,3 +61,14 @@ class Config: } } } + + def __init__(self): + # Load environment variables from .env file + load_dotenv() + + self.TELEGRAM_BOT_API_TOKEN = os.getenv('TELEGRAM_BOT_API_TOKEN') + self.TELEGRAM_ADMIN_IDS_LIST = json.loads(os.environ['TELEGRAM_ADMIN_IDS_LIST']) + self.DATABASE_URL = os.getenv('DATABASE_URL') + + +config = Config() diff --git a/db/admins_statistics.py b/db/admins_statistics.py index 39ec1da..4ebd3be 100644 --- a/db/admins_statistics.py +++ b/db/admins_statistics.py @@ -2,7 +2,7 @@ import os from dataclasses import dataclass -from config import Config +from config import config @dataclass @@ -12,29 +12,11 @@ class AdminsStatisticsRowNames: orioks_failed_logins: str = 'orioks_failed_logins' -def create_and_init_admins_statistics() -> None: - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_admins_statistics.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script) - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_admins_statistics.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'orioks_scheduled_requests': 0, - 'orioks_success_logins': 0, - 'orioks_failed_logins': 0, - }) - db.commit() - db.close() - - def select_all_from_admins_statistics() -> dict: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_from_admins_statistics.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_admins_statistics.sql'), 'r') as sql_file: sql_script = sql_file.read() rows = sql.execute(sql_script).fetchone() db.close() @@ -51,9 +33,9 @@ def update_inc_admins_statistics_row_name(row_name: str) -> None: if row_name not in ('orioks_scheduled_requests', 'orioks_success_logins', 'orioks_failed_logins'): raise Exception('update_inc_admins_statistics_row_name() -> row_name must only be in (' 'orioks_scheduled_requests, orioks_success_logins, orioks_failed_logins)') - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_inc_admins_statistics_row_name.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_inc_admins_statistics_row_name.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script.format(row_name=row_name), { 'row_name': row_name diff --git a/db/notify_settings.py b/db/notify_settings.py index aee4bb4..896fab1 100644 --- a/db/notify_settings.py +++ b/db/notify_settings.py @@ -1,13 +1,13 @@ import sqlite3 import os -from config import Config +from config import config def get_user_notify_settings_to_dict(user_telegram_id: int) -> dict: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_from_user_notify_settings.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_user_notify_settings.sql'), 'r') as sql_file: sql_script = sql_file.read() raw = sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone() db.close() @@ -27,9 +27,9 @@ def update_user_notify_settings(user_telegram_id: int, row_name: str, to_value: if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): raise Exception('update_user_notify_settings() -> row_name must only be in (' 'marks, news, discipline_sources, homeworks, requests)') - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_set_row_name.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_set_row_name.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script.format(row_name=row_name), { 'to_value': to_value, @@ -40,9 +40,9 @@ def update_user_notify_settings(user_telegram_id: int, row_name: str, to_value: def update_user_notify_settings_reset_to_default(user_telegram_id: int) -> None: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_reset_to_default.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_reset_to_default.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { 'user_telegram_id': user_telegram_id, @@ -57,9 +57,9 @@ def update_user_notify_settings_reset_to_default(user_telegram_id: int) -> None: def select_all_news_enabled_users() -> set: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_news_enabled_users.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_news_enabled_users.sql'), 'r') as sql_file: sql_script = sql_file.read() result = set() for user in sql.execute(sql_script).fetchall(): diff --git a/db/sql/create_admins_statistics.sql b/db/sql/create_admins_statistics.sql deleted file mode 100644 index 9a01444..0000000 --- a/db/sql/create_admins_statistics.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE TABLE IF NOT EXISTS admins_statistics ( - orioks_scheduled_requests BIGINT DEFAULT 0, - orioks_success_logins BIGINT DEFAULT 0, - orioks_failed_logins BIGINT DEFAULT 0 -); \ No newline at end of file diff --git a/db/sql/init_admins_statistics.sql b/db/sql/init_admins_statistics.sql deleted file mode 100644 index d18d3ed..0000000 --- a/db/sql/init_admins_statistics.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT OR IGNORE INTO admins_statistics -VALUES (:orioks_scheduled_requests, :orioks_success_logins, :orioks_failed_logins); \ No newline at end of file diff --git a/db/user_first_add.py b/db/user_first_add.py index 9c0ab7f..00da705 100644 --- a/db/user_first_add.py +++ b/db/user_first_add.py @@ -1,7 +1,7 @@ import sqlite3 import os -from config import Config +from config import config def user_first_add_to_db(user_telegram_id: int) -> None: diff --git a/db/user_status.py b/db/user_status.py index 29831cd..f153c7e 100644 --- a/db/user_status.py +++ b/db/user_status.py @@ -2,14 +2,14 @@ from typing import Set import os -from config import Config +from config import config def get_user_agreement_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_is_user_agreement_accepted_from_user_status.sql'), + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_agreement_accepted_from_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() is_user_agreement_accepted = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) @@ -18,9 +18,9 @@ def get_user_agreement_status(user_telegram_id: int) -> bool: def get_user_orioks_authenticated_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_is_user_orioks_authenticated_from_user_status.sql'), + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_orioks_authenticated_from_user_status.sql'), 'r') as sql_file: sql_script = sql_file.read() is_user_orioks_authenticated = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) @@ -29,9 +29,9 @@ def get_user_orioks_authenticated_status(user_telegram_id: int) -> bool: def update_user_agreement_status(user_telegram_id: int, is_user_agreement_accepted: bool) -> None: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_agreement_accepted.sql'), + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_agreement_accepted.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { @@ -43,9 +43,9 @@ def update_user_agreement_status(user_telegram_id: int, is_user_agreement_accept def update_user_orioks_authenticated_status(user_telegram_id: int, is_user_orioks_authenticated: bool) -> None: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_orioks_authenticated.sql'), + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_orioks_authenticated.sql'), 'r') as sql_file: sql_script = sql_file.read() sql.execute(sql_script, { @@ -57,9 +57,9 @@ def update_user_orioks_authenticated_status(user_telegram_id: int, is_user_oriok def select_all_orioks_authenticated_users() -> Set[int]: - db = sqlite3.connect(Config.PATH_TO_DB) + db = sqlite3.connect(config.PATH_TO_DB) sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_all_orioks_authenticated_users.sql'), 'r') as sql_file: + with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_orioks_authenticated_users.sql'), 'r') as sql_file: sql_script = sql_file.read() result = set() for user in sql.execute(sql_script).fetchall(): diff --git a/requirements.txt b/requirements.txt index be40799..ceed49e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,9 @@ aiogram==2.19 aiohttp==3.8.1 aioschedule==0.5.2 aiosignal==1.2.0 +alembic==1.8.0 anyio==3.5.0 +astroid==2.11.5 async-generator==1.10 async-timeout==4.0.2 attrs==21.4.0 @@ -12,23 +14,37 @@ beautifulsoup4==4.10.0 certifi==2021.10.8 cffi==1.15.0 charset-normalizer==2.0.12 +colorama==0.4.4 cryptography==36.0.1 +dill==0.3.5.1 frozenlist==1.3.0 +greenlet==1.1.2 h11==0.12.0 html2image==2.0.1 httpcore==0.14.7 httpx==0.22.0 idna==3.3 imgkit==1.2.2 +importlib-metadata==4.11.4 +importlib-resources==5.7.1 install==1.3.5 +isort==5.10.1 +lazy-object-proxy==1.7.1 +Mako==1.2.0 +MarkupSafe==2.1.1 +mccabe==0.7.0 multidict==6.0.2 mypy==0.960 mypy-extensions==0.4.3 outcome==1.1.0 Pillow==9.1.0 +platformdirs==2.5.2 pycparser==2.21 +pylint==2.14.1 +pylint-runner==0.6.0 pyOpenSSL==22.0.0 PySocks==1.7.1 +python-dotenv==0.20.0 pytz==2021.3 qrcode==7.3.1 requests==2.27.1 @@ -37,11 +53,15 @@ six==1.16.0 sniffio==1.2.0 sortedcontainers==2.4.0 soupsieve==2.3.1 +SQLAlchemy==1.4.37 tomli==2.0.1 +tomlkit==0.11.0 trio==0.20.0 trio-websocket==0.9.2 types-aiofiles==0.8.8 typing_extensions==4.2.0 urllib3==1.26.8 +wrapt==1.14.1 wsproto==1.1.0 -yarl==1.7.2 \ No newline at end of file +yarl==1.7.2 +zipp==3.8.0 From f66977b510aa34f3a46e1794b80aef3eb4212ce3 Mon Sep 17 00:00:00 2001 From: Alexey Voronin <8183700@edu.miet.ru> Date: Sat, 23 Jul 2022 23:08:14 +0300 Subject: [PATCH 4/5] feature(refactoring): raw SQL replaced by SqlAlchemy ORM --- .env.example | 2 +- README.md | 2 + app/__init__.py | 12 ++- app/exceptions/DatabaseException.py | 3 + app/exceptions/__init__.py | 4 +- .../callbacks/SettingsCallbackHandler.py | 9 +- .../callbacks/UserAgreementCallbackHandler.py | 17 ++-- .../admins/AdminStatisticsCommandHandler.py | 16 ++-- .../OrioksAuthInputPasswordCommandHandler.py | 27 ++---- .../orioks/OrioksAuthStartCommandHandler.py | 5 +- .../orioks/OrioksLogoutCommandHandler.py | 11 +-- .../NotificationSettingsCommandHandler.py | 11 ++- app/helpers/AdminHelper.py | 75 +++++++++++++++ app/helpers/MarksPictureHelper.py | 7 +- app/helpers/OrioksHelper.py | 8 +- app/helpers/RequestHelper.py | 8 +- app/helpers/UserHelper.py | 88 +++++++++++++++++ app/helpers/__init__.py | 5 +- app/menus/orioks/OrioksAuthFailedMenu.py | 5 +- app/menus/start/StartMenu.py | 7 +- app/middlewares/UserAgreementMiddleware.py | 8 +- .../UserOrioksAttemptsMiddleware.py | 7 +- app/models/BaseModel.py | 17 +++- app/models/users/UserNotifySettings.py | 8 +- checking/homeworks/get_orioks_homeworks.py | 6 +- checking/on_startup.py | 36 +++---- checking/requests/get_orioks_requests.py | 6 +- db/admins_statistics.py | 94 ------------------- db/notify_settings.py | 68 -------------- db/sql/create_user_notify_settings.sql | 8 -- db/sql/create_user_status.sql | 6 -- db/sql/init_user_notify_settings.sql | 2 - db/sql/init_user_status.sql | 2 - db/sql/select_all_from_admins_statistics.sql | 1 - .../select_all_from_user_notify_settings.sql | 1 - db/sql/select_all_news_enabled_users.sql | 8 -- .../select_all_orioks_authenticated_users.sql | 1 - .../select_count_notify_settings_row_name.sql | 8 -- ...elect_count_notify_settings_statistics.sql | 1 - .../select_count_user_status_statistics.sql | 1 - ...er_agreement_accepted_from_user_status.sql | 1 - ..._orioks_authenticated_from_user_status.sql | 1 - ..._user_orioks_attempts_from_user_status.sql | 1 - .../update_inc_admins_statistics_row_name.sql | 2 - db/sql/update_inc_user_orioks_attempts.sql | 1 - ..._user_notify_settings_reset_to_default.sql | 9 -- ...date_user_notify_settings_set_row_name.sql | 1 - ..._status_set_is_user_agreement_accepted.sql | 1 - ...tatus_set_is_user_orioks_authenticated.sql | 1 - db/user_first_add.py | 38 -------- db/user_status.py | 92 ------------------ 51 files changed, 287 insertions(+), 472 deletions(-) create mode 100644 app/exceptions/DatabaseException.py create mode 100644 app/helpers/AdminHelper.py create mode 100644 app/helpers/UserHelper.py delete mode 100644 db/admins_statistics.py delete mode 100644 db/notify_settings.py delete mode 100644 db/sql/create_user_notify_settings.sql delete mode 100644 db/sql/create_user_status.sql delete mode 100644 db/sql/init_user_notify_settings.sql delete mode 100644 db/sql/init_user_status.sql delete mode 100644 db/sql/select_all_from_admins_statistics.sql delete mode 100644 db/sql/select_all_from_user_notify_settings.sql delete mode 100644 db/sql/select_all_news_enabled_users.sql delete mode 100644 db/sql/select_all_orioks_authenticated_users.sql delete mode 100644 db/sql/select_count_notify_settings_row_name.sql delete mode 100644 db/sql/select_count_notify_settings_statistics.sql delete mode 100644 db/sql/select_count_user_status_statistics.sql delete mode 100644 db/sql/select_is_user_agreement_accepted_from_user_status.sql delete mode 100644 db/sql/select_is_user_orioks_authenticated_from_user_status.sql delete mode 100644 db/sql/select_user_orioks_attempts_from_user_status.sql delete mode 100644 db/sql/update_inc_admins_statistics_row_name.sql delete mode 100644 db/sql/update_inc_user_orioks_attempts.sql delete mode 100644 db/sql/update_user_notify_settings_reset_to_default.sql delete mode 100644 db/sql/update_user_notify_settings_set_row_name.sql delete mode 100644 db/sql/update_user_status_set_is_user_agreement_accepted.sql delete mode 100644 db/sql/update_user_status_set_is_user_orioks_authenticated.sql delete mode 100644 db/user_first_add.py delete mode 100644 db/user_status.py diff --git a/.env.example b/.env.example index 09c1675..9322eb8 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,3 @@ TELEGRAM_BOT_API_TOKEN=0123456789:AAEf-L4BLFSfUxHYgtY-HvZgE-0123456789 -TELEGRAM_ADMIN_IDS_LIST=["1234567890"] +TELEGRAM_ADMIN_IDS_LIST=[1234567890] DATABASE_URL=sqlite:///database.sqlite3 \ No newline at end of file diff --git a/README.md b/README.md index 6eb7c01..9716c27 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ cp setenv-example.sh setenv.sh && vim setenv.sh source setenv.sh ``` + + 7. Запуск Бота ```bash python main.py diff --git a/app/__init__.py b/app/__init__.py index aff0dc5..6199bad 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -5,10 +5,7 @@ from aiogram.contrib.fsm_storage.memory import MemoryStorage from aiogram.utils import executor -from app.helpers import CommonHelper -from app.middlewares import UserAgreementMiddleware, UserOrioksAttemptsMiddleware, AdminCommandsMiddleware -from checking import on_startup from config import config @@ -24,14 +21,17 @@ def initialize_database(): def initialize_assets(): from app.helpers.AssetsHelper import assetsHelper current_folder_path = os.path.dirname(os.path.abspath(__file__)) - assetsHelper.initialize(f'{current_folder_path}/storage') + assetsHelper.initialize(f'{current_folder_path}/assets') def _settings_before_start() -> None: from app.handlers import register_handlers from app.fixtures import initialize_default_values + from app.middlewares import UserAgreementMiddleware, UserOrioksAttemptsMiddleware, AdminCommandsMiddleware + from app.helpers import CommonHelper register_handlers(dispatcher=dispatcher) + initialize_assets() initialize_default_values() dispatcher.middleware.setup(UserAgreementMiddleware()) dispatcher.middleware.setup(UserOrioksAttemptsMiddleware()) @@ -46,6 +46,8 @@ def _settings_before_start() -> None: db_session = initialize_database() def run(): - logging.basicConfig(level=logging.INFO) + from checking import on_startup + + logging.basicConfig(level=logging.DEBUG) _settings_before_start() executor.start_polling(dispatcher, skip_updates=True, on_startup=on_startup.on_startup) diff --git a/app/exceptions/DatabaseException.py b/app/exceptions/DatabaseException.py new file mode 100644 index 0000000..c5ad7ee --- /dev/null +++ b/app/exceptions/DatabaseException.py @@ -0,0 +1,3 @@ + +class DatabaseException(Exception): + pass diff --git a/app/exceptions/__init__.py b/app/exceptions/__init__.py index d2c8f68..bbd2e7e 100644 --- a/app/exceptions/__init__.py +++ b/app/exceptions/__init__.py @@ -1,8 +1,10 @@ from .OrioksInvalidLoginCredentialsException import OrioksInvalidLoginCredentialsException from .OrioksParseDataException import OrioksParseDataException from .FileCompareException import FileCompareException +from .DatabaseException import DatabaseException __all__ = [ 'OrioksInvalidLoginCredentialsException', - 'OrioksParseDataException', 'FileCompareException' + 'OrioksParseDataException', 'FileCompareException', + 'DatabaseException' ] diff --git a/app/handlers/callbacks/SettingsCallbackHandler.py b/app/handlers/callbacks/SettingsCallbackHandler.py index 5a77e81..cbfffc6 100644 --- a/app/handlers/callbacks/SettingsCallbackHandler.py +++ b/app/handlers/callbacks/SettingsCallbackHandler.py @@ -1,9 +1,9 @@ from aiogram import types import app -import db.notify_settings from app.handlers import AbstractCallbackHandler from app.handlers.commands.settings import NotificationSettingsCommandHandler +from app.helpers import UserHelper class SettingsCallbackHandler(AbstractCallbackHandler): @@ -16,12 +16,7 @@ async def process(callback_query: types.CallbackQuery, *args, **kwargs): callback_query.id, text='Эта категория ещё недоступна.', show_alert=True ) - db.notify_settings.update_user_notify_settings( - user_telegram_id=callback_query.from_user.id, - row_name=_row_name, - to_value=not db.notify_settings.get_user_notify_settings_to_dict( - user_telegram_id=callback_query.from_user.id)[_row_name], - ) + UserHelper.update_notification_settings(user_telegram_id=callback_query.from_user.id, setting_name=_row_name) await NotificationSettingsCommandHandler.send_user_settings( user_id=callback_query.from_user.id, callback_query=callback_query ) diff --git a/app/handlers/callbacks/UserAgreementCallbackHandler.py b/app/handlers/callbacks/UserAgreementCallbackHandler.py index d5a8f9d..566b7ac 100644 --- a/app/handlers/callbacks/UserAgreementCallbackHandler.py +++ b/app/handlers/callbacks/UserAgreementCallbackHandler.py @@ -1,7 +1,7 @@ from aiogram import types -import db.user_status import app from app.handlers import AbstractCallbackHandler +from app.helpers import UserHelper from app.menus.start import StartMenu @@ -9,14 +9,15 @@ class UserAgreementCallbackHandler(AbstractCallbackHandler): @staticmethod async def process(callback_query: types.CallbackQuery, *args, **kwargs): - if db.user_status.get_user_agreement_status(user_telegram_id=callback_query.from_user.id): + user_telegram_id = callback_query.from_user.id + + if UserHelper.is_user_agreement_accepted(user_telegram_id=user_telegram_id): return await app.bot.answer_callback_query( callback_query.id, text='Пользовательское соглашение уже принято.', show_alert=True) - db.user_status.update_user_agreement_status( - user_telegram_id=callback_query.from_user.id, - is_user_agreement_accepted=True - ) + + UserHelper.accept_user_agreement(user_telegram_id=user_telegram_id) + await app.bot.answer_callback_query(callback_query.id) - answer_message = await app.bot.send_message(callback_query.from_user.id, 'Пользовательское соглашение принято!') - await StartMenu.show(chat_id=answer_message.chat.id, telegram_user_id=callback_query.from_user.id) + answer_message = await app.bot.send_message(user_telegram_id, 'Пользовательское соглашение принято!') + await StartMenu.show(chat_id=answer_message.chat.id, telegram_user_id=user_telegram_id) diff --git a/app/handlers/commands/admins/AdminStatisticsCommandHandler.py b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py index eb8820b..9e7e5c8 100644 --- a/app/handlers/commands/admins/AdminStatisticsCommandHandler.py +++ b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py @@ -3,8 +3,8 @@ from app.handlers import AbstractCommandHandler -import db.admins_statistics from app.handlers.commands.settings import NotificationSettingsCommandHandler +from app.helpers import AdminHelper from config import Config @@ -13,7 +13,7 @@ class AdminStatisticsCommandHandler(AbstractCommandHandler): @staticmethod async def process(message: types.Message, *args, **kwargs): msg = '' - for key, value in db.admins_statistics.select_count_user_status_statistics().items(): + for key, value in AdminHelper.get_count_users_statistics().items(): msg += markdown.text( markdown.text(key), markdown.text(value), @@ -24,12 +24,12 @@ async def process(message: types.Message, *args, **kwargs): for category in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): msg += markdown.text( markdown.text(NotificationSettingsCommandHandler.notify_settings_names_to_vars[category]), - markdown.text(db.admins_statistics.select_count_notify_settings_row_name(row_name=category)), + markdown.text(AdminHelper.get_count_notify_settings_by_row_name(row_name=category)), sep=': ', ) + '\n' msg += '\n' - for key, value in db.admins_statistics.select_all_from_admins_statistics().items(): + for key, value in AdminHelper.get_general_statistics().items(): msg += markdown.text( markdown.text(key), markdown.text(value), @@ -37,12 +37,12 @@ async def process(message: types.Message, *args, **kwargs): ) + '\n' requests_wave_time = ( - db.admins_statistics.select_count_notify_settings_row_name(row_name='marks') + + AdminHelper.get_count_notify_settings_by_row_name(row_name='marks') + 2 + # marks category - db.admins_statistics.select_count_notify_settings_row_name( + AdminHelper.get_count_notify_settings_by_row_name( row_name='discipline_sources') + - db.admins_statistics.select_count_notify_settings_row_name(row_name='homeworks') + - db.admins_statistics.select_count_notify_settings_row_name(row_name='requests') * 3 + AdminHelper.get_count_notify_settings_by_row_name(row_name='homeworks') + + AdminHelper.get_count_notify_settings_by_row_name(row_name='requests') * 3 ) * Config.ORIOKS_SECONDS_BETWEEN_REQUESTS / 60 msg += markdown.text( markdown.text('Примерное время выполнения одной волны запросов'), diff --git a/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py index 5b520c3..d60998d 100644 --- a/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksAuthInputPasswordCommandHandler.py @@ -7,12 +7,10 @@ from app.exceptions import OrioksInvalidLoginCredentialsException from app.forms import OrioksAuthForm from app.handlers import AbstractCommandHandler -import db.user_status -import db.admins_statistics -from app.helpers import OrioksHelper, TelegramMessageHelper +from app.helpers import OrioksHelper, TelegramMessageHelper, UserHelper, AdminHelper from app.menus.orioks import OrioksAuthFailedMenu from app.menus.start import StartMenu -from config import Config +from config import config class OrioksAuthInputPasswordCommandHandler(AbstractCommandHandler): @@ -20,9 +18,8 @@ class OrioksAuthInputPasswordCommandHandler(AbstractCommandHandler): @staticmethod async def process(message: types.Message, *args, **kwargs): state = kwargs.get('state', None) - db.user_status.update_inc_user_orioks_attempts(user_telegram_id=message.from_user.id) - if db.user_status.get_user_orioks_attempts( - user_telegram_id=message.from_user.id) > Config.ORIOKS_MAX_LOGIN_TRIES: + UserHelper.increment_login_attempt_count(user_telegram_id=message.from_user.id) + if UserHelper.get_login_attempt_count(user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: return await message.reply( markdown.text( markdown.hbold('Ошибка! Ты истратил все попытки входа в аккаунт ОРИОКС.'), @@ -33,7 +30,7 @@ async def process(message: types.Message, *args, **kwargs): ) await OrioksAuthForm.next() await state.update_data(password=message.text) - if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): + if UserHelper.is_user_orioks_authenticated(user_telegram_id=message.from_user.id): await state.finish() await app.bot.delete_message(message.chat.id, message.message_id) return await app.bot.send_message( @@ -43,15 +40,15 @@ async def process(message: types.Message, *args, **kwargs): async with state.proxy() as data: sticker_message = await app.bot.send_sticker( message.chat.id, - Config.TELEGRAM_STICKER_LOADER, + config.TELEGRAM_STICKER_LOADER, ) try: await OrioksHelper.orioks_login_save_cookies(user_login=data['login'], user_password=data['password'], user_telegram_id=message.from_user.id) - db.user_status.update_user_orioks_authenticated_status( + UserHelper.update_authorization_status( user_telegram_id=message.from_user.id, - is_user_orioks_authenticated=True + is_authenticated=True ) await StartMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) await app.bot.send_message( @@ -60,13 +57,9 @@ async def process(message: types.Message, *args, **kwargs): markdown.text('Вход в аккаунт ОРИОКС выполнен!') ) ) - db.admins_statistics.update_inc_admins_statistics_row_name( - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_success_logins - ) + AdminHelper.increase_success_logins() except OrioksInvalidLoginCredentialsException: - db.admins_statistics.update_inc_admins_statistics_row_name( - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_failed_logins - ) + AdminHelper.increase_failed_logins() await OrioksAuthFailedMenu.show(chat_id=message.chat.id, telegram_user_id=message.from_user.id) except (asyncio.TimeoutError, TypeError): await app.bot.send_message( diff --git a/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py index 5b305ff..045cb84 100644 --- a/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksAuthStartCommandHandler.py @@ -5,15 +5,14 @@ from app.forms import OrioksAuthForm from app.handlers import AbstractCommandHandler -import db.user_status -import db.user_first_add +from app.helpers import UserHelper class OrioksAuthStartCommandHandler(AbstractCommandHandler): @staticmethod async def process(message: types.Message, *args, **kwargs): - if db.user_status.get_user_orioks_authenticated_status(user_telegram_id=message.from_user.id): + if UserHelper.is_user_orioks_authenticated(user_telegram_id=message.from_user.id): return await message.reply( markdown.text( markdown.hbold('Ты уже выполнил вход в аккаунт ОРИОКС.'), diff --git a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py index dc6c5ef..c189afa 100644 --- a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py @@ -4,7 +4,6 @@ import keyboards from app.handlers import AbstractCommandHandler -import db.user_status from app.helpers import OrioksHelper @@ -12,6 +11,8 @@ class OrioksLogoutCommandHandler(AbstractCommandHandler): @staticmethod async def process(message: types.Message, *args, **kwargs): + user_telegram_id = message.from_user.id + await message.reply( markdown.text( markdown.hbold('Выход из аккаунта ОРИОКС выполнен.'), @@ -20,8 +21,6 @@ async def process(message: types.Message, *args, **kwargs): ), reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), ) - db.user_status.update_user_orioks_authenticated_status( - user_telegram_id=message.from_user.id, - is_user_orioks_authenticated=False - ) - OrioksHelper.make_orioks_logout(user_telegram_id=message.from_user.id) + + # UserHelper.update_authorization_status(user_telegram_id=user_telegram_id, is_authenticated=False) + OrioksHelper.make_orioks_logout(user_telegram_id=user_telegram_id) diff --git a/app/handlers/commands/settings/NotificationSettingsCommandHandler.py b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py index 66a5d5c..6ce81e9 100644 --- a/app/handlers/commands/settings/NotificationSettingsCommandHandler.py +++ b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py @@ -3,7 +3,8 @@ import app from app.handlers import AbstractCommandHandler -import db.notify_settings +from app.helpers import UserHelper +from app.models.users import UserNotifySettings class NotificationSettingsCommandHandler(AbstractCommandHandler): @@ -17,9 +18,9 @@ class NotificationSettingsCommandHandler(AbstractCommandHandler): } @staticmethod - def _get_section_name_with_status(section_name: str, is_on_off: dict) -> str: - emoji = '🔔' if is_on_off[section_name] else '❌' - return f'{emoji} {NotificationSettingsCommandHandler.notify_settings_names_to_vars[section_name]}' + def _get_section_name_with_status(attribute_name: str, is_on_off: UserNotifySettings) -> str: + emoji = '🔔' if getattr(is_on_off, attribute_name) else '❌' + return f'{emoji} {NotificationSettingsCommandHandler.notify_settings_names_to_vars[attribute_name]}' @staticmethod def init_notify_settings_inline_btns(is_on_off: dict) -> types.InlineKeyboardMarkup: @@ -63,7 +64,7 @@ async def process(message: types.Message, *args, **kwargs): @staticmethod async def send_user_settings(user_id: int, callback_query: types.CallbackQuery = None) -> types.Message: - is_on_off_dict = db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_id) + is_on_off_dict = UserHelper.get_user_settings_by_telegram_id(user_telegram_id=user_id) text = markdown.text( markdown.text( markdown.text('📓'), diff --git a/app/helpers/AdminHelper.py b/app/helpers/AdminHelper.py new file mode 100644 index 0000000..d4dadce --- /dev/null +++ b/app/helpers/AdminHelper.py @@ -0,0 +1,75 @@ +from typing import Dict + +from app.models import session +from app.models.admins import AdminStatistics +from app.models.users import UserStatus, UserNotifySettings + + +class AdminHelper: + + @staticmethod + def get_statistics_object() -> AdminStatistics: + return AdminStatistics.find_one(id=1) + + @staticmethod + def increase_success_logins(): + statistics = AdminHelper.get_statistics_object() + statistics.success_logins += 1 + statistics.save() + + @staticmethod + def increase_failed_logins(): + statistics = AdminHelper.get_statistics_object() + statistics.failed_logins += 1 + statistics.save() + + @staticmethod + def increase_scheduled_requests(): + statistics = AdminHelper.get_statistics_object() + statistics.scheduled_requests += 1 + statistics.save() + + @staticmethod + def get_user_status_statistics(**kwargs): + users_with_accepted_agreement = UserStatus.query.filter_by(**kwargs) + return users_with_accepted_agreement.count() + + @staticmethod + def get_count_users_statistics() -> Dict: + users_agreement_accepted = AdminHelper.get_user_status_statistics(agreement_accepted=True) + users_agreement_discarded = AdminHelper.get_user_status_statistics(agreement_accepted=False) + + users_orioks_authentication = AdminHelper.get_user_status_statistics(authenticated=True) + users_orioks_no_authentication = AdminHelper.get_user_status_statistics(authenticated=False) + + all_users = users_agreement_discarded + users_agreement_accepted + all_orioks_users = users_orioks_no_authentication + users_orioks_authentication + return { + 'Приняли пользовательское соглашение': f'{users_agreement_accepted} / {all_users}', + 'Выполнили вход в ОРИОКС': f'{users_orioks_authentication} / {all_orioks_users}', + } + + @staticmethod + def get_count_notify_settings_by_row_name(row_name: str) -> int: + if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): + raise Exception('select_count_notify_settings_row_name() -> row_name must only be in (' + 'marks, news, discipline_sources, homeworks, requests)') + + users_count = session.\ + query(UserStatus).\ + join(UserNotifySettings, UserStatus.user_telegram_id == UserNotifySettings.user_telegram_id). \ + filter( + UserStatus.authenticated == True, + getattr(UserNotifySettings, row_name) == True + ).count() + + return int(users_count) + + @staticmethod + def get_general_statistics(): + statistics_object = AdminHelper.get_statistics_object() + successful_login_percent = f'{statistics_object.success_logins} / {statistics_object.success_logins + statistics_object.failed_logins}' + return { + 'Запланированные успешные запросы на сервера ОРИОКС': statistics_object.scheduled_requests, + 'Успешные попытки авторизации ОРИОКС': successful_login_percent, + } diff --git a/app/helpers/MarksPictureHelper.py b/app/helpers/MarksPictureHelper.py index c09f56a..925b84a 100644 --- a/app/helpers/MarksPictureHelper.py +++ b/app/helpers/MarksPictureHelper.py @@ -6,14 +6,13 @@ import secrets from app.helpers.AssetsHelper import assetsHelper -from config import Config +from config import config class MarksPictureHelper: def __init__(self): self._font_path = assetsHelper.make_full_path('fonts/PTSansCaption-Bold.ttf') - self._font_upper_size = 64 self._font_downer_size = 62 @@ -167,7 +166,7 @@ def get_image_marks( self._get_image_by_grade(current_grade, max_grade) self._calculate_font_size_and_text_width(title_text, side_text, mark_change_text=mark_change_text) self._draw_text_marks(title_text, mark_change_text, side_text) - path_to_result_image = pathlib.Path(os.path.join(Config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) + path_to_result_image = pathlib.Path(os.path.join(config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) self.image.save(path_to_result_image) return path_to_result_image @@ -177,7 +176,7 @@ def get_image_news( side_text: str, url: str ) -> pathlib.Path: - path_to_result_image = pathlib.Path(os.path.join(Config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) + path_to_result_image = pathlib.Path(os.path.join(config.BASEDIR, f'temp_{secrets.token_hex(15)}.png')) self._get_news_image() if title_text == '': self.image.save(path_to_result_image) diff --git a/app/helpers/OrioksHelper.py b/app/helpers/OrioksHelper.py index 2a7a8fe..a1ab148 100644 --- a/app/helpers/OrioksHelper.py +++ b/app/helpers/OrioksHelper.py @@ -9,11 +9,9 @@ from datetime import datetime from app.exceptions import OrioksInvalidLoginCredentialsException -from app.helpers import TelegramMessageHelper, CommonHelper +from app.helpers import TelegramMessageHelper, CommonHelper, UserHelper import aiogram.utils.markdown as md -import db.user_status -import db.notify_settings from config import config _sem = asyncio.Semaphore(config.ORIOKS_LOGIN_QUEUE_SEMAPHORE_VALUE) @@ -82,5 +80,5 @@ def make_orioks_logout(user_telegram_id: int) -> None: CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'doc', f'{user_telegram_id}.json')) CommonHelper.safe_delete(os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', 'reference', f'{user_telegram_id}.json')) - db.user_status.update_user_orioks_authenticated_status(user_telegram_id=user_telegram_id, is_user_orioks_authenticated=False) - db.notify_settings.update_user_notify_settings_reset_to_default(user_telegram_id=user_telegram_id) + UserHelper.update_authorization_status(user_telegram_id=user_telegram_id, is_authenticated=False) + UserHelper.reset_notification_settings(user_telegram_id=user_telegram_id) diff --git a/app/helpers/RequestHelper.py b/app/helpers/RequestHelper.py index a0d8a47..8beeb22 100644 --- a/app/helpers/RequestHelper.py +++ b/app/helpers/RequestHelper.py @@ -1,9 +1,9 @@ import asyncio import logging -import db.admins_statistics import aiohttp +from app.helpers import AdminHelper from config import config @@ -16,12 +16,12 @@ async def get_request(url: str, session: aiohttp.ClientSession) -> str: await asyncio.sleep(config.ORIOKS_SECONDS_BETWEEN_REQUESTS) # orioks dont die please # TODO: is db.user_status.get_user_orioks_authenticated_status(user_telegram_id=user_telegram_id) # else safe delete all user's file + # Обработать случай, когда пользователь к моменту достижения своей очереди разлогинился # TODO: is db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) # else safe delete non-enabled categories logging.debug('get request to: %s', (url, )) async with session.get(str(url)) as resp: raw_html = await resp.text() - db.admins_statistics.update_inc_admins_statistics_row_name( # TODO: sum of requests and inc for one use db - row_name=db.admins_statistics.AdminsStatisticsRowNames.orioks_scheduled_requests - ) + # TODO: sum of requests and inc for one use db + AdminHelper.increase_scheduled_requests() return raw_html diff --git a/app/helpers/UserHelper.py b/app/helpers/UserHelper.py new file mode 100644 index 0000000..5a4f0b0 --- /dev/null +++ b/app/helpers/UserHelper.py @@ -0,0 +1,88 @@ +from app.exceptions import DatabaseException +from app.models.users import UserStatus, UserNotifySettings + + +class UserHelper: + + @staticmethod + def __get_user_by_telegram_id(user_telegram_id: int) -> UserStatus: + user = UserStatus.find_one(user_telegram_id=user_telegram_id) + if user is None: + raise DatabaseException(f'User with telegram id {user_telegram_id} not found in database') + + return user + + @staticmethod + def get_user_settings_by_telegram_id(user_telegram_id: int) -> UserNotifySettings: + user_notify_settings = UserNotifySettings.find_one(user_telegram_id=user_telegram_id) + if user_notify_settings is None: + raise DatabaseException(f'Settings of user with telegram id {user_telegram_id} not found in database') + + return user_notify_settings + + @staticmethod + def create_user_if_not_exist(user_telegram_id: int) -> None: + existed_user = UserStatus.find_one(user_telegram_id=user_telegram_id) + if existed_user is None: + user = UserStatus() + user.fill(user_telegram_id=user_telegram_id) + user.save() + + existed_user_settings = UserNotifySettings.find_one(user_telegram_id=user_telegram_id) + if existed_user_settings is None: + user_settings = UserNotifySettings() + user_settings.fill(user_telegram_id=user_telegram_id) + user_settings.save() + + @staticmethod + def is_user_agreement_accepted(user_telegram_id: int) -> bool: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + return user.agreement_accepted + + @staticmethod + def accept_user_agreement(user_telegram_id: int) -> None: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + user.agreement_accepted = True + user.save() + + @staticmethod + def is_user_orioks_authenticated(user_telegram_id: int) -> bool: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + return user.authenticated + + @staticmethod + def get_login_attempt_count(user_telegram_id: int) -> int: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + return user.login_attempt_count + + @staticmethod + def increment_login_attempt_count(user_telegram_id: int) -> None: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + user.login_attempt_count += 1 + user.save() + + @staticmethod + def update_authorization_status(user_telegram_id: int, is_authenticated: bool) -> None: + user = UserHelper.__get_user_by_telegram_id(user_telegram_id=user_telegram_id) + user.authenticated = is_authenticated + user.save() + + @staticmethod + def reset_notification_settings(user_telegram_id: int) -> None: + user_settings = UserHelper.get_user_settings_by_telegram_id(user_telegram_id=user_telegram_id) + user_settings.fill(user_telegram_id=user_telegram_id) + user_settings.save() + + @staticmethod + def update_notification_settings(user_telegram_id: int, setting_name: str) -> None: + user_settings = UserHelper.get_user_settings_by_telegram_id(user_telegram_id=user_telegram_id) + if getattr(user_settings, setting_name) is None: + raise DatabaseException(f'Setting with name {setting_name} for user with id {user_telegram_id} not found') + + setattr(user_settings, setting_name, not bool(getattr(user_settings, setting_name))) + user_settings.save() + + @staticmethod + def get_users_with_enabled_news_subscription(): + users = UserNotifySettings.query.filter_by(news=True) + return users diff --git a/app/helpers/__init__.py b/app/helpers/__init__.py index e375614..8a3e1da 100644 --- a/app/helpers/__init__.py +++ b/app/helpers/__init__.py @@ -1,3 +1,5 @@ +from .AdminHelper import AdminHelper +from .UserHelper import UserHelper from .CommonHelper import CommonHelper from .AssetsHelper import AssetsHelper from .JsonFileHelper import JsonFileHelper @@ -8,5 +10,6 @@ __all__ = [ 'CommonHelper', 'AssetsHelper', 'JsonFileHelper', 'OrioksHelper', - 'RequestHelper', 'TelegramMessageHelper', 'MarksPictureHelper' + 'RequestHelper', 'TelegramMessageHelper', 'MarksPictureHelper', + 'UserHelper', 'AdminHelper' ] diff --git a/app/menus/orioks/OrioksAuthFailedMenu.py b/app/menus/orioks/OrioksAuthFailedMenu.py index 8a99c51..028fbea 100644 --- a/app/menus/orioks/OrioksAuthFailedMenu.py +++ b/app/menus/orioks/OrioksAuthFailedMenu.py @@ -2,16 +2,15 @@ import app import keyboards +from app.helpers import UserHelper from app.menus.AbstractMenu import AbstractMenu -import db.user_status - class OrioksAuthFailedMenu(AbstractMenu): @staticmethod async def show(chat_id: int, telegram_user_id: int) -> None: - if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=telegram_user_id): + if not UserHelper.is_user_orioks_authenticated(user_telegram_id=telegram_user_id): await app.bot.send_message( chat_id, markdown.text( diff --git a/app/menus/start/StartMenu.py b/app/menus/start/StartMenu.py index 5fc0b52..6d92052 100644 --- a/app/menus/start/StartMenu.py +++ b/app/menus/start/StartMenu.py @@ -1,19 +1,18 @@ from aiogram.utils import markdown import app +from app.helpers import UserHelper from app.menus.AbstractMenu import AbstractMenu -import db.user_status import keyboards -import db.user_first_add class StartMenu(AbstractMenu): @staticmethod async def show(chat_id: int, telegram_user_id: int) -> None: - db.user_first_add.user_first_add_to_db(user_telegram_id=telegram_user_id) - if not db.user_status.get_user_orioks_authenticated_status(user_telegram_id=telegram_user_id): + UserHelper.create_user_if_not_exist(user_telegram_id=telegram_user_id) + if not UserHelper.is_user_orioks_authenticated(user_telegram_id=telegram_user_id): await app.bot.send_message( chat_id, markdown.text( diff --git a/app/middlewares/UserAgreementMiddleware.py b/app/middlewares/UserAgreementMiddleware.py index e183e30..df092bd 100644 --- a/app/middlewares/UserAgreementMiddleware.py +++ b/app/middlewares/UserAgreementMiddleware.py @@ -3,8 +3,7 @@ from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils import markdown -import db.user_first_add -import db.user_status +from app.helpers import UserHelper class UserAgreementMiddleware(BaseMiddleware): @@ -21,8 +20,9 @@ class UserAgreementMiddleware(BaseMiddleware): # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): - db.user_first_add.user_first_add_to_db(user_telegram_id=message.from_user.id) - if not db.user_status.get_user_agreement_status(user_telegram_id=message.from_user.id): + user_telegram_id = message.from_user.id + UserHelper.create_user_if_not_exist(user_telegram_id=user_telegram_id) + if not UserHelper.is_user_agreement_accepted(user_telegram_id=user_telegram_id): await message.reply( markdown.text( markdown.text('Для получения доступа к Боту, необходимо принять Пользовательское соглашение:'), diff --git a/app/middlewares/UserOrioksAttemptsMiddleware.py b/app/middlewares/UserOrioksAttemptsMiddleware.py index 0ec4ab3..b66b6e5 100644 --- a/app/middlewares/UserOrioksAttemptsMiddleware.py +++ b/app/middlewares/UserOrioksAttemptsMiddleware.py @@ -3,11 +3,9 @@ from aiogram.dispatcher.middlewares import BaseMiddleware from aiogram.utils import markdown +from app.helpers import UserHelper from config import config -import db.user_first_add -import db.user_status - class UserOrioksAttemptsMiddleware(BaseMiddleware): """ @@ -18,8 +16,7 @@ class UserOrioksAttemptsMiddleware(BaseMiddleware): # pylint: disable=unused-argument async def on_process_message(self, message: types.Message, *args, **kwargs): - if db.user_status.get_user_orioks_attempts( - user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: + if UserHelper.get_login_attempt_count(user_telegram_id=message.from_user.id) > config.ORIOKS_MAX_LOGIN_TRIES: await message.reply( markdown.text( markdown.hbold('Ты совершил подозрительно много попыток входа в аккаунт ОРИОКС.'), diff --git a/app/models/BaseModel.py b/app/models/BaseModel.py index 4b443ad..3795c63 100644 --- a/app/models/BaseModel.py +++ b/app/models/BaseModel.py @@ -1,4 +1,5 @@ from sqlalchemy import Column, DateTime, func, Integer +from sqlalchemy.exc import SQLAlchemyError from app import db_session from app.models import DeclarativeModelBase @@ -16,13 +17,19 @@ def find_one(cls, **query): return cls.query.filter_by(**query).one_or_none() def save(self): - if self.id is None: - db_session.add(self) - db_session.commit() + try: + if self.id is None: + db_session.add(self) + db_session.commit() + except SQLAlchemyError: + db_session.rollback() def delete(self): - db_session.delete(self) - db_session.commit() + try: + db_session.delete(self) + db_session.commit() + except SQLAlchemyError: + db_session.rollback() def as_dict(self): return {column.key: getattr(self, attr) for attr, column in self.__mapper__.c.items()} diff --git a/app/models/users/UserNotifySettings.py b/app/models/users/UserNotifySettings.py index 18eed27..b45ca73 100644 --- a/app/models/users/UserNotifySettings.py +++ b/app/models/users/UserNotifySettings.py @@ -17,7 +17,7 @@ class UserNotifySettings(BaseModel): def fill(self, user_telegram_id: int) -> None: self.user_telegram_id = user_telegram_id self.marks = True - self.news = True - self.discipline_sources = True - self.homeworks = True - self.requests = True + self.news = False + self.discipline_sources = False + self.homeworks = False + self.requests = False diff --git a/checking/homeworks/get_orioks_homeworks.py b/checking/homeworks/get_orioks_homeworks.py index 6e833b1..4cf4d35 100644 --- a/checking/homeworks/get_orioks_homeworks.py +++ b/checking/homeworks/get_orioks_homeworks.py @@ -7,7 +7,7 @@ from app.exceptions import OrioksParseDataException, FileCompareException from app.helpers import JsonFileHelper, TelegramMessageHelper, CommonHelper, RequestHelper -from config import Config +from config import config import aiogram.utils.markdown as md @@ -115,8 +115,8 @@ def compare(old_dict: dict, new_dict: dict) -> list: async def user_homeworks_check(user_telegram_id: int, session: aiohttp.ClientSession) -> None: - student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', 'homeworks', student_json_file) + student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'homeworks', student_json_file) try: homeworks_dict = await get_orioks_homeworks(session=session) except OrioksParseDataException: diff --git a/checking/on_startup.py b/checking/on_startup.py index 80ec07e..469d164 100644 --- a/checking/on_startup.py +++ b/checking/on_startup.py @@ -8,10 +8,9 @@ import aiohttp import aioschedule -import db.notify_settings -import db.user_status from app.exceptions import OrioksParseDataException -from app.helpers import CommonHelper, TelegramMessageHelper +from app.helpers import CommonHelper, TelegramMessageHelper, UserHelper +from app.models.users import UserStatus, UserNotifySettings from checking.marks.get_orioks_marks import user_marks_check from checking.news.get_orioks_news import get_current_new, user_news_check_from_news_id from checking.homeworks.get_orioks_homeworks import user_homeworks_check @@ -25,44 +24,44 @@ def _get_user_orioks_cookies_from_telegram_id(user_telegram_id: int) -> SimpleCo return SimpleCookie(pickle.load(open(path_to_cookies, 'rb'))) -def _delete_users_tracking_data_in_notify_settings_off(user_telegram_id: int, user_notify_settings: dict) -> None: - if not user_notify_settings['marks']: +def _delete_users_tracking_data_in_notify_settings_off(user_telegram_id: int, user_notify_settings: UserNotifySettings) -> None: + if not user_notify_settings.marks: CommonHelper.safe_delete( os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'marks', f'{user_telegram_id}.json') ) - if not user_notify_settings['news']: + if not user_notify_settings.news: CommonHelper.safe_delete( os.path.join(config.PATH_TO_STUDENTS_TRACKING_DATA, 'news', f'{user_telegram_id}.json') ) - if not user_notify_settings['discipline_sources']: + if not user_notify_settings.discipline_sources: CommonHelper.safe_delete(os.path.join( config.PATH_TO_STUDENTS_TRACKING_DATA, 'discipline_sources', f'{user_telegram_id}.json') ) - if not user_notify_settings['homeworks']: + if not user_notify_settings.homeworks: CommonHelper.safe_delete(os.path.join( config.PATH_TO_STUDENTS_TRACKING_DATA, 'homeworks', f'{user_telegram_id}.json') ) - if not user_notify_settings['requests']: + if not user_notify_settings.requests: CommonHelper.safe_delete(os.path.join( config.PATH_TO_STUDENTS_TRACKING_DATA, 'requests', f'{user_telegram_id}.json') ) async def make_one_user_check(user_telegram_id: int) -> None: - user_notify_settings = db.notify_settings.get_user_notify_settings_to_dict(user_telegram_id=user_telegram_id) + user_notify_settings = UserHelper.get_user_settings_by_telegram_id(user_telegram_id=user_telegram_id) cookies = _get_user_orioks_cookies_from_telegram_id(user_telegram_id=user_telegram_id) async with aiohttp.ClientSession( cookies=cookies, timeout=config.REQUESTS_TIMEOUT, headers=config.ORIOKS_REQUESTS_HEADERS ) as session: - if user_notify_settings['marks']: + if user_notify_settings.marks: await user_marks_check(user_telegram_id=user_telegram_id, session=session) - if user_notify_settings['discipline_sources']: + if user_notify_settings.discipline_sources: pass # TODO: user_discipline_sources_check(user_telegram_id=user_telegram_id, session=session) - if user_notify_settings['homeworks']: + if user_notify_settings.homeworks: await user_homeworks_check(user_telegram_id=user_telegram_id, session=session) - if user_notify_settings['requests']: + if user_notify_settings.requests: await user_requests_check(user_telegram_id=user_telegram_id, session=session) _delete_users_tracking_data_in_notify_settings_off( user_telegram_id=user_telegram_id, @@ -72,7 +71,8 @@ async def make_one_user_check(user_telegram_id: int) -> None: async def make_all_users_news_check(tries_counter: int = 0) -> list: tasks = [] - users_to_check_news = db.notify_settings.select_all_news_enabled_users() + users_to_check_news = UserHelper.get_users_with_enabled_news_subscription() + users_to_check_news = [user.user_telegram_id for user in users_to_check_news] if len(users_to_check_news) == 0: return [] picked_user_to_check_news = random.choice(list(users_to_check_news)) @@ -116,10 +116,12 @@ async def run_requests(tasks: list) -> None: async def do_checks(): logging.info('started: %s' % (datetime.now().strftime("%H:%M:%S %d.%m.%Y"),)) - users_to_check = db.user_status.select_all_orioks_authenticated_users() + + authenticated_users = UserStatus.query.filter_by(authenticated=True) + users_telegram_ids = set(user.user_telegram_id for user in authenticated_users) tasks = [] + await make_all_users_news_check() - for user_telegram_id in users_to_check: + for user_telegram_id in users_telegram_ids: tasks.append(make_one_user_check( user_telegram_id=user_telegram_id )) diff --git a/checking/requests/get_orioks_requests.py b/checking/requests/get_orioks_requests.py index e535b72..1bc5d36 100644 --- a/checking/requests/get_orioks_requests.py +++ b/checking/requests/get_orioks_requests.py @@ -7,7 +7,7 @@ from app.exceptions import OrioksParseDataException, FileCompareException from app.helpers import CommonHelper, RequestHelper, JsonFileHelper, TelegramMessageHelper -from config import Config +from config import config import aiogram.utils.markdown as md @@ -115,8 +115,8 @@ def compare(old_dict: dict, new_dict: dict) -> list: async def _user_requests_check_with_subsection(user_telegram_id: int, section: str, session: aiohttp.ClientSession) -> None: - student_json_file = Config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) - path_users_to_file = os.path.join(Config.BASEDIR, 'users_data', 'tracking_data', + student_json_file = config.STUDENT_FILE_JSON_MASK.format(id=user_telegram_id) + path_users_to_file = os.path.join(config.BASEDIR, 'users_data', 'tracking_data', 'requests', section, student_json_file) try: requests_dict = await get_orioks_requests(section=section, session=session) diff --git a/db/admins_statistics.py b/db/admins_statistics.py deleted file mode 100644 index 4ebd3be..0000000 --- a/db/admins_statistics.py +++ /dev/null @@ -1,94 +0,0 @@ -import sqlite3 -import os -from dataclasses import dataclass - -from config import config - - -@dataclass -class AdminsStatisticsRowNames: - orioks_scheduled_requests: str = 'orioks_scheduled_requests' - orioks_success_logins: str = 'orioks_success_logins' - orioks_failed_logins: str = 'orioks_failed_logins' - - -def select_all_from_admins_statistics() -> dict: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_admins_statistics.sql'), 'r') as sql_file: - sql_script = sql_file.read() - rows = sql.execute(sql_script).fetchone() - db.close() - return { - 'Запланированные успешные запросы на сервера ОРИОКС': rows[0], - 'Успешные попытки авторизации ОРИОКС': f'{rows[1]} / {rows[1] + rows[2]}', - } - - -def update_inc_admins_statistics_row_name(row_name: str) -> None: - """ - row_name must only be in (orioks_scheduled_requests, orioks_success_logins, orioks_failed_logins) - """ - if row_name not in ('orioks_scheduled_requests', 'orioks_success_logins', 'orioks_failed_logins'): - raise Exception('update_inc_admins_statistics_row_name() -> row_name must only be in (' - 'orioks_scheduled_requests, orioks_success_logins, orioks_failed_logins)') - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_inc_admins_statistics_row_name.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script.format(row_name=row_name), { - 'row_name': row_name - }) - db.commit() - db.close() - - -def select_count_user_status_statistics() -> dict: - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_count_user_status_statistics.sql'), 'r') as sql_file: - sql_script = sql_file.read() - users_agreement_accepted = sql.execute( - sql_script.format(row_name='is_user_agreement_accepted'), { - 'value': True, - } - ).fetchone() - users_agreement_discarded = sql.execute( - sql_script.format(row_name='is_user_agreement_accepted'), { - 'value': False, - } - ).fetchone() - - users_orioks_authentication = sql.execute( - sql_script.format(row_name='is_user_orioks_authenticated'), { - 'value': True, - } - ).fetchone() - users_orioks_no_authentication = sql.execute( - sql_script.format(row_name='is_user_orioks_authenticated'), { - 'value': False, - } - ).fetchone() - db.close() - all_users = users_agreement_discarded[0] + users_agreement_accepted[0] - all_orioks_users = users_orioks_no_authentication[0] + users_orioks_authentication[0] - return { - 'Приняли пользовательское соглашение': f'{users_agreement_accepted[0]} / {all_users}', - 'Выполнили вход в ОРИОКС': f'{users_orioks_authentication[0]} / {all_orioks_users}', - } - - -def select_count_notify_settings_row_name(row_name: str) -> int: - """ - row_name must only be in (marks, news, discipline_sources, homeworks, requests) - """ - if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): - raise Exception('select_count_notify_settings_row_name() -> row_name must only be in (' - 'marks, news, discipline_sources, homeworks, requests)') - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_count_notify_settings_row_name.sql'), 'r') as sql_file: - sql_script = sql_file.read() - count_notify_settings_marks = sql.execute(sql_script.format(row_name=row_name)).fetchone() - return int(count_notify_settings_marks[0]) diff --git a/db/notify_settings.py b/db/notify_settings.py deleted file mode 100644 index 896fab1..0000000 --- a/db/notify_settings.py +++ /dev/null @@ -1,68 +0,0 @@ -import sqlite3 -import os - -from config import config - - -def get_user_notify_settings_to_dict(user_telegram_id: int) -> dict: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_from_user_notify_settings.sql'), 'r') as sql_file: - sql_script = sql_file.read() - raw = sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone() - db.close() - return { - 'marks': True if raw[0] else False, - 'news': True if raw[1] else False, - 'discipline_sources': True if raw[2] else False, - 'homeworks': True if raw[3] else False, - 'requests': True if raw[4] else False, - } - - -def update_user_notify_settings(user_telegram_id: int, row_name: str, to_value: bool) -> None: - """ - row_name must only be in (marks, news, discipline_sources, homeworks, requests) - """ - if row_name not in ('marks', 'news', 'discipline_sources', 'homeworks', 'requests'): - raise Exception('update_user_notify_settings() -> row_name must only be in (' - 'marks, news, discipline_sources, homeworks, requests)') - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_set_row_name.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script.format(row_name=row_name), { - 'to_value': to_value, - 'user_telegram_id': user_telegram_id - }) - db.commit() - db.close() - - -def update_user_notify_settings_reset_to_default(user_telegram_id: int) -> None: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_notify_settings_reset_to_default.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'user_telegram_id': user_telegram_id, - 'marks': True, - 'news': False, - 'discipline_sources': False, - 'homeworks': False, - 'requests': False, - }) - db.commit() - db.close() - - -def select_all_news_enabled_users() -> set: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_news_enabled_users.sql'), 'r') as sql_file: - sql_script = sql_file.read() - result = set() - for user in sql.execute(sql_script).fetchall(): - result.add(user[0]) - db.close() - return result diff --git a/db/sql/create_user_notify_settings.sql b/db/sql/create_user_notify_settings.sql deleted file mode 100644 index 557558d..0000000 --- a/db/sql/create_user_notify_settings.sql +++ /dev/null @@ -1,8 +0,0 @@ -CREATE TABLE IF NOT EXISTS user_notify_settings ( - user_telegram_id INTEGER unique, - marks TINYINT, - news TINYINT, - discipline_sources TINYINT, - homeworks TINYINT, - requests TINYINT -); \ No newline at end of file diff --git a/db/sql/create_user_status.sql b/db/sql/create_user_status.sql deleted file mode 100644 index 0dd24f7..0000000 --- a/db/sql/create_user_status.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE IF NOT EXISTS user_status ( - user_telegram_id INTEGER unique, - is_user_agreement_accepted TINYINT, - is_user_orioks_authenticated TINYINT, - orioks_login_attempts TINYINT -); \ No newline at end of file diff --git a/db/sql/init_user_notify_settings.sql b/db/sql/init_user_notify_settings.sql deleted file mode 100644 index 84e14c1..0000000 --- a/db/sql/init_user_notify_settings.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT OR IGNORE INTO user_notify_settings -VALUES (:user_telegram_id, :marks, :news, :discipline_sources, :homeworks, :requests); \ No newline at end of file diff --git a/db/sql/init_user_status.sql b/db/sql/init_user_status.sql deleted file mode 100644 index e40246c..0000000 --- a/db/sql/init_user_status.sql +++ /dev/null @@ -1,2 +0,0 @@ -INSERT OR IGNORE INTO user_status -VALUES (:user_telegram_id, :is_user_agreement_accepted, :is_user_orioks_authenticated, :orioks_login_attempts); \ No newline at end of file diff --git a/db/sql/select_all_from_admins_statistics.sql b/db/sql/select_all_from_admins_statistics.sql deleted file mode 100644 index b50da4a..0000000 --- a/db/sql/select_all_from_admins_statistics.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT orioks_scheduled_requests, orioks_success_logins, orioks_failed_logins FROM admins_statistics LIMIT 1; \ No newline at end of file diff --git a/db/sql/select_all_from_user_notify_settings.sql b/db/sql/select_all_from_user_notify_settings.sql deleted file mode 100644 index 09c0547..0000000 --- a/db/sql/select_all_from_user_notify_settings.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT marks, news, discipline_sources, homeworks, requests FROM user_notify_settings WHERE user_telegram_id = :user_telegram_id; diff --git a/db/sql/select_all_news_enabled_users.sql b/db/sql/select_all_news_enabled_users.sql deleted file mode 100644 index 4f1f406..0000000 --- a/db/sql/select_all_news_enabled_users.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - user_notify_settings.user_telegram_id -FROM user_notify_settings - INNER JOIN user_status on user_notify_settings.user_telegram_id = user_status.user_telegram_id -WHERE - user_notify_settings.news = TRUE - AND - user_status.is_user_orioks_authenticated = TRUE; \ No newline at end of file diff --git a/db/sql/select_all_orioks_authenticated_users.sql b/db/sql/select_all_orioks_authenticated_users.sql deleted file mode 100644 index 7a6335c..0000000 --- a/db/sql/select_all_orioks_authenticated_users.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT user_telegram_id FROM user_status WHERE is_user_orioks_authenticated = TRUE; \ No newline at end of file diff --git a/db/sql/select_count_notify_settings_row_name.sql b/db/sql/select_count_notify_settings_row_name.sql deleted file mode 100644 index 64b1dd1..0000000 --- a/db/sql/select_count_notify_settings_row_name.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT - COUNT(*) -FROM user_notify_settings - INNER JOIN user_status on user_notify_settings.user_telegram_id = user_status.user_telegram_id -WHERE - user_notify_settings.{row_name} = TRUE - AND - user_status.is_user_orioks_authenticated = TRUE; \ No newline at end of file diff --git a/db/sql/select_count_notify_settings_statistics.sql b/db/sql/select_count_notify_settings_statistics.sql deleted file mode 100644 index bc25b12..0000000 --- a/db/sql/select_count_notify_settings_statistics.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT COUNT(*) FROM user_notify_settings WHERE {row_name} = TRUE; \ No newline at end of file diff --git a/db/sql/select_count_user_status_statistics.sql b/db/sql/select_count_user_status_statistics.sql deleted file mode 100644 index abcb79b..0000000 --- a/db/sql/select_count_user_status_statistics.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT COUNT(*) FROM user_status WHERE {row_name} = :value; \ No newline at end of file diff --git a/db/sql/select_is_user_agreement_accepted_from_user_status.sql b/db/sql/select_is_user_agreement_accepted_from_user_status.sql deleted file mode 100644 index 14e7ebf..0000000 --- a/db/sql/select_is_user_agreement_accepted_from_user_status.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT is_user_agreement_accepted FROM user_status WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/select_is_user_orioks_authenticated_from_user_status.sql b/db/sql/select_is_user_orioks_authenticated_from_user_status.sql deleted file mode 100644 index 3e5e148..0000000 --- a/db/sql/select_is_user_orioks_authenticated_from_user_status.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT is_user_orioks_authenticated FROM user_status WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/select_user_orioks_attempts_from_user_status.sql b/db/sql/select_user_orioks_attempts_from_user_status.sql deleted file mode 100644 index b5add26..0000000 --- a/db/sql/select_user_orioks_attempts_from_user_status.sql +++ /dev/null @@ -1 +0,0 @@ -SELECT orioks_login_attempts FROM user_status WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/update_inc_admins_statistics_row_name.sql b/db/sql/update_inc_admins_statistics_row_name.sql deleted file mode 100644 index 64703ce..0000000 --- a/db/sql/update_inc_admins_statistics_row_name.sql +++ /dev/null @@ -1,2 +0,0 @@ -UPDATE admins_statistics -SET {row_name} = {row_name} + 1; \ No newline at end of file diff --git a/db/sql/update_inc_user_orioks_attempts.sql b/db/sql/update_inc_user_orioks_attempts.sql deleted file mode 100644 index 4380b64..0000000 --- a/db/sql/update_inc_user_orioks_attempts.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE user_status SET orioks_login_attempts = :to_value WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/update_user_notify_settings_reset_to_default.sql b/db/sql/update_user_notify_settings_reset_to_default.sql deleted file mode 100644 index 7f8708f..0000000 --- a/db/sql/update_user_notify_settings_reset_to_default.sql +++ /dev/null @@ -1,9 +0,0 @@ -UPDATE user_notify_settings -SET - marks = :marks, - news = :news, - discipline_sources = :discipline_sources, - homeworks = :homeworks, - requests = :requests -WHERE - user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/update_user_notify_settings_set_row_name.sql b/db/sql/update_user_notify_settings_set_row_name.sql deleted file mode 100644 index c894fed..0000000 --- a/db/sql/update_user_notify_settings_set_row_name.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE user_notify_settings SET {row_name} = :to_value WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/update_user_status_set_is_user_agreement_accepted.sql b/db/sql/update_user_status_set_is_user_agreement_accepted.sql deleted file mode 100644 index 80c8427..0000000 --- a/db/sql/update_user_status_set_is_user_agreement_accepted.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE user_status SET is_user_agreement_accepted = :is_user_agreement_accepted WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/sql/update_user_status_set_is_user_orioks_authenticated.sql b/db/sql/update_user_status_set_is_user_orioks_authenticated.sql deleted file mode 100644 index 7873c04..0000000 --- a/db/sql/update_user_status_set_is_user_orioks_authenticated.sql +++ /dev/null @@ -1 +0,0 @@ -UPDATE user_status SET is_user_orioks_authenticated = :is_user_orioks_authenticated WHERE user_telegram_id = :user_telegram_id; \ No newline at end of file diff --git a/db/user_first_add.py b/db/user_first_add.py deleted file mode 100644 index 00da705..0000000 --- a/db/user_first_add.py +++ /dev/null @@ -1,38 +0,0 @@ -import sqlite3 -import os - -from config import config - - -def user_first_add_to_db(user_telegram_id: int) -> None: - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_user_status.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script) - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_user_status.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'user_telegram_id': user_telegram_id, - 'is_user_agreement_accepted': False, - 'is_user_orioks_authenticated': False, - 'orioks_login_attempts': 0 - }) - - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'create_user_notify_settings.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script) - - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'init_user_notify_settings.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'user_telegram_id': user_telegram_id, - 'marks': True, - 'news': False, - 'discipline_sources': False, - 'homeworks': False, - 'requests': False - }) - db.commit() - db.close() diff --git a/db/user_status.py b/db/user_status.py deleted file mode 100644 index f153c7e..0000000 --- a/db/user_status.py +++ /dev/null @@ -1,92 +0,0 @@ -import sqlite3 -from typing import Set -import os - -from config import config - - -def get_user_agreement_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_agreement_accepted_from_user_status.sql'), - 'r') as sql_file: - sql_script = sql_file.read() - is_user_agreement_accepted = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) - db.close() - return is_user_agreement_accepted - - -def get_user_orioks_authenticated_status(user_telegram_id: int) -> bool: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_is_user_orioks_authenticated_from_user_status.sql'), - 'r') as sql_file: - sql_script = sql_file.read() - is_user_orioks_authenticated = bool(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) - db.close() - return is_user_orioks_authenticated - - -def update_user_agreement_status(user_telegram_id: int, is_user_agreement_accepted: bool) -> None: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_agreement_accepted.sql'), - 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'is_user_agreement_accepted': is_user_agreement_accepted, - 'user_telegram_id': user_telegram_id - }) - db.commit() - db.close() - - -def update_user_orioks_authenticated_status(user_telegram_id: int, is_user_orioks_authenticated: bool) -> None: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'update_user_status_set_is_user_orioks_authenticated.sql'), - 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'is_user_orioks_authenticated': is_user_orioks_authenticated, - 'user_telegram_id': user_telegram_id - }) - db.commit() - db.close() - - -def select_all_orioks_authenticated_users() -> Set[int]: - db = sqlite3.connect(config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(config.PATH_TO_SQL_FOLDER, 'select_all_orioks_authenticated_users.sql'), 'r') as sql_file: - sql_script = sql_file.read() - result = set() - for user in sql.execute(sql_script).fetchall(): - result.add(user[0]) - db.close() - return result - - -def get_user_orioks_attempts(user_telegram_id: int) -> int: - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'select_user_orioks_attempts_from_user_status.sql'), - 'r') as sql_file: - sql_script = sql_file.read() - attempts = int(sql.execute(sql_script, {'user_telegram_id': user_telegram_id}).fetchone()[0]) - db.close() - return attempts - - -def update_inc_user_orioks_attempts(user_telegram_id: int) -> None: - db = sqlite3.connect(Config.PATH_TO_DB) - sql = db.cursor() - with open(os.path.join(Config.PATH_TO_SQL_FOLDER, 'update_inc_user_orioks_attempts.sql'), 'r') as sql_file: - sql_script = sql_file.read() - sql.execute(sql_script, { - 'to_value': int(get_user_orioks_attempts(user_telegram_id=user_telegram_id)) + 1, - 'user_telegram_id': user_telegram_id - }) - db.commit() - db.close() From 462ee2fee28d994417ebd7d7f2dd13622e7835d7 Mon Sep 17 00:00:00 2001 From: Kirill Kulikov Date: Mon, 12 Sep 2022 02:32:31 +0300 Subject: [PATCH 5/5] chore(refactoring): abstract keyboard models + typos --- README.md | 2 - app/__init__.py | 3 +- app/fixtures/AbstractFixture.py | 5 +- .../admins/AdminStatisticsCommandHandler.py | 4 +- .../orioks/OrioksAuthCancelCommandHandler.py | 4 +- .../orioks/OrioksLogoutCommandHandler.py | 4 +- .../NotificationSettingsCommandHandler.py | 61 +------------------ app/keyboards/AbstractInlineKeyboard.py | 9 +++ app/keyboards/AbstractReplyKeyboard.py | 9 +++ app/keyboards/__init__.py | 4 ++ .../AuthorizationReplyKeyboard.py | 15 +++++ app/keyboards/authorization/__init__.py | 3 + .../NotifySettingsInlineKeyboard.py | 59 ++++++++++++++++++ .../NotifySettingsReplyKeyboard.py | 15 +++++ app/keyboards/notify_settings/__init__.py | 5 ++ app/menus/orioks/OrioksAuthFailedMenu.py | 4 +- app/menus/start/StartMenu.py | 6 +- app/menus/start/__init__.py | 2 + app/migrations/env.py | 4 ++ checking/homeworks/get_orioks_homeworks.py | 2 +- checking/marks/get_orioks_marks.py | 2 +- checking/news/get_orioks_news.py | 2 +- checking/requests/get_orioks_requests.py | 2 +- keyboards.py | 10 --- 24 files changed, 147 insertions(+), 89 deletions(-) create mode 100644 app/keyboards/AbstractInlineKeyboard.py create mode 100644 app/keyboards/AbstractReplyKeyboard.py create mode 100644 app/keyboards/__init__.py create mode 100644 app/keyboards/authorization/AuthorizationReplyKeyboard.py create mode 100644 app/keyboards/authorization/__init__.py create mode 100644 app/keyboards/notify_settings/NotifySettingsInlineKeyboard.py create mode 100644 app/keyboards/notify_settings/NotifySettingsReplyKeyboard.py create mode 100644 app/keyboards/notify_settings/__init__.py delete mode 100644 keyboards.py diff --git a/README.md b/README.md index 9716c27..6eb7c01 100644 --- a/README.md +++ b/README.md @@ -89,8 +89,6 @@ cp setenv-example.sh setenv.sh && vim setenv.sh source setenv.sh ``` - - 7. Запуск Бота ```bash python main.py diff --git a/app/__init__.py b/app/__init__.py index 6199bad..b989c2c 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -6,7 +6,6 @@ from aiogram.utils import executor - from config import config @@ -48,6 +47,6 @@ def _settings_before_start() -> None: def run(): from checking import on_startup - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.INFO) _settings_before_start() executor.start_polling(dispatcher, skip_updates=True, on_startup=on_startup.on_startup) diff --git a/app/fixtures/AbstractFixture.py b/app/fixtures/AbstractFixture.py index 38a1745..6d20219 100644 --- a/app/fixtures/AbstractFixture.py +++ b/app/fixtures/AbstractFixture.py @@ -1,5 +1,5 @@ from abc import abstractmethod -from typing import Type, List, Dict +from typing import Type from app.models import BaseModel @@ -16,7 +16,7 @@ def fill_method_name(self) -> str: return 'fill' @abstractmethod - def values(self) -> List[Dict]: + def values(self) -> list[dict]: raise NotImplementedError def need_to_add_values(self) -> bool: @@ -35,3 +35,4 @@ def insert_data(self) -> bool: model = self.model() getattr(model, self.fill_method_name)(**value) model.save() + return True diff --git a/app/handlers/commands/admins/AdminStatisticsCommandHandler.py b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py index 9e7e5c8..3c7bf47 100644 --- a/app/handlers/commands/admins/AdminStatisticsCommandHandler.py +++ b/app/handlers/commands/admins/AdminStatisticsCommandHandler.py @@ -5,7 +5,7 @@ from app.handlers.commands.settings import NotificationSettingsCommandHandler from app.helpers import AdminHelper -from config import Config +from config import config class AdminStatisticsCommandHandler(AbstractCommandHandler): @@ -43,7 +43,7 @@ async def process(message: types.Message, *args, **kwargs): row_name='discipline_sources') + AdminHelper.get_count_notify_settings_by_row_name(row_name='homeworks') + AdminHelper.get_count_notify_settings_by_row_name(row_name='requests') * 3 - ) * Config.ORIOKS_SECONDS_BETWEEN_REQUESTS / 60 + ) * config.ORIOKS_SECONDS_BETWEEN_REQUESTS / 60 msg += markdown.text( markdown.text('Примерное время выполнения одной волны запросов'), markdown.text( diff --git a/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py b/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py index d369476..6c48eee 100644 --- a/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksAuthCancelCommandHandler.py @@ -1,8 +1,8 @@ from aiogram import types from aiogram.utils import markdown -import keyboards from app.handlers import AbstractCommandHandler +from app.keyboards.authorization import AuthorizationReplyKeyboard class OrioksAuthCancelCommandHandler(AbstractCommandHandler): @@ -23,6 +23,6 @@ async def process(message: types.Message, *args, **kwargs): 'Если ты боишься вводить свои данные, ознакомься со следующей информацией'), sep='\n', ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + reply_markup=await AuthorizationReplyKeyboard.show(), disable_web_page_preview=True, ) diff --git a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py index c189afa..2500196 100644 --- a/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py +++ b/app/handlers/commands/orioks/OrioksLogoutCommandHandler.py @@ -1,10 +1,10 @@ from aiogram import types from aiogram.utils import markdown -import keyboards from app.handlers import AbstractCommandHandler from app.helpers import OrioksHelper +from app.keyboards.authorization import AuthorizationReplyKeyboard class OrioksLogoutCommandHandler(AbstractCommandHandler): @@ -19,7 +19,7 @@ async def process(message: types.Message, *args, **kwargs): markdown.text('Теперь ты НЕ будешь получать уведомления от Бота.'), sep='\n', ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + reply_markup=await AuthorizationReplyKeyboard.show(), ) # UserHelper.update_authorization_status(user_telegram_id=user_telegram_id, is_authenticated=False) diff --git a/app/handlers/commands/settings/NotificationSettingsCommandHandler.py b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py index 6ce81e9..f64bea7 100644 --- a/app/handlers/commands/settings/NotificationSettingsCommandHandler.py +++ b/app/handlers/commands/settings/NotificationSettingsCommandHandler.py @@ -3,68 +3,17 @@ import app from app.handlers import AbstractCommandHandler -from app.helpers import UserHelper -from app.models.users import UserNotifySettings +from app.keyboards.notify_settings import NotifySettingsInlineKeyboard class NotificationSettingsCommandHandler(AbstractCommandHandler): - notify_settings_names_to_vars = { - 'marks': 'Оценки', - 'news': 'Новости', - 'discipline_sources': 'Ресурсы', - 'homeworks': 'Домашние задания', - 'requests': 'Заявки', - } - - @staticmethod - def _get_section_name_with_status(attribute_name: str, is_on_off: UserNotifySettings) -> str: - emoji = '🔔' if getattr(is_on_off, attribute_name) else '❌' - return f'{emoji} {NotificationSettingsCommandHandler.notify_settings_names_to_vars[attribute_name]}' - - @staticmethod - def init_notify_settings_inline_btns(is_on_off: dict) -> types.InlineKeyboardMarkup: - """ - is_on_off = { - 'Обучение': False, - 'Новости': False, - 'Ресурсы': False, - 'Домашние задания': False, - 'Заявки': False, - } - """ - inline_kb_full: types.InlineKeyboardMarkup = types.InlineKeyboardMarkup(row_width=1) - inline_kb_full.add( - types.InlineKeyboardButton( - NotificationSettingsCommandHandler._get_section_name_with_status('marks', is_on_off), - callback_data='notify_settings-marks' - ), - types.InlineKeyboardButton( - NotificationSettingsCommandHandler._get_section_name_with_status('news', is_on_off), - callback_data='notify_settings-news' - ), - types.InlineKeyboardButton( - NotificationSettingsCommandHandler._get_section_name_with_status('discipline_sources', is_on_off), - callback_data='notify_settings-discipline_sources' - ), - types.InlineKeyboardButton( - NotificationSettingsCommandHandler._get_section_name_with_status('homeworks', is_on_off), - callback_data='notify_settings-homeworks' - ), - types.InlineKeyboardButton( - NotificationSettingsCommandHandler._get_section_name_with_status('requests', is_on_off), - callback_data='notify_settings-requests' - ) - ) - return inline_kb_full - @staticmethod async def process(message: types.Message, *args, **kwargs): await NotificationSettingsCommandHandler.send_user_settings(message.from_user.id, callback_query=None) @staticmethod async def send_user_settings(user_id: int, callback_query: types.CallbackQuery = None) -> types.Message: - is_on_off_dict = UserHelper.get_user_settings_by_telegram_id(user_telegram_id=user_id) text = markdown.text( markdown.text( markdown.text('📓'), @@ -118,13 +67,9 @@ async def send_user_settings(user_id: int, callback_query: types.CallbackQuery = return await app.bot.send_message( user_id, text=text, - reply_markup=NotificationSettingsCommandHandler.init_notify_settings_inline_btns( - is_on_off=is_on_off_dict - ), + reply_markup=NotifySettingsInlineKeyboard.show(user_id), ) return await callback_query.message.edit_text( text=text, - reply_markup=NotificationSettingsCommandHandler.init_notify_settings_inline_btns( - is_on_off=is_on_off_dict - ), + reply_markup=NotifySettingsInlineKeyboard.show(user_id), ) diff --git a/app/keyboards/AbstractInlineKeyboard.py b/app/keyboards/AbstractInlineKeyboard.py new file mode 100644 index 0000000..1a808c6 --- /dev/null +++ b/app/keyboards/AbstractInlineKeyboard.py @@ -0,0 +1,9 @@ +from abc import abstractmethod + + +class AbstractInlineKeyboard: + + @staticmethod + @abstractmethod + async def show(user_telegram_id: int) -> None: + raise NotImplementedError diff --git a/app/keyboards/AbstractReplyKeyboard.py b/app/keyboards/AbstractReplyKeyboard.py new file mode 100644 index 0000000..bcbe3bb --- /dev/null +++ b/app/keyboards/AbstractReplyKeyboard.py @@ -0,0 +1,9 @@ +from abc import abstractmethod + + +class AbstractReplyKeyboard: + + @staticmethod + @abstractmethod + async def show() -> None: + raise NotImplementedError diff --git a/app/keyboards/__init__.py b/app/keyboards/__init__.py new file mode 100644 index 0000000..bb7abdd --- /dev/null +++ b/app/keyboards/__init__.py @@ -0,0 +1,4 @@ +from .AbstractReplyKeyboard import AbstractReplyKeyboard +from .AbstractInlineKeyboard import AbstractInlineKeyboard + +__all__ = ['AbstractReplyKeyboard', 'AbstractInlineKeyboard'] diff --git a/app/keyboards/authorization/AuthorizationReplyKeyboard.py b/app/keyboards/authorization/AuthorizationReplyKeyboard.py new file mode 100644 index 0000000..135f338 --- /dev/null +++ b/app/keyboards/authorization/AuthorizationReplyKeyboard.py @@ -0,0 +1,15 @@ +from aiogram import types + +from app.keyboards import AbstractReplyKeyboard + + +class AuthorizationReplyKeyboard(AbstractReplyKeyboard): + + @staticmethod + async def show() -> types.ReplyKeyboardMarkup: + keyboard_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True) + keyboard_markup.row(types.KeyboardButton('Авторизация')) + + more_btns_text = ('Руководство', 'О проекте') + keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text)) + return keyboard_markup diff --git a/app/keyboards/authorization/__init__.py b/app/keyboards/authorization/__init__.py new file mode 100644 index 0000000..3c39376 --- /dev/null +++ b/app/keyboards/authorization/__init__.py @@ -0,0 +1,3 @@ +from .AuthorizationReplyKeyboard import AuthorizationReplyKeyboard + +__all__ = ['AuthorizationReplyKeyboard'] diff --git a/app/keyboards/notify_settings/NotifySettingsInlineKeyboard.py b/app/keyboards/notify_settings/NotifySettingsInlineKeyboard.py new file mode 100644 index 0000000..d88abcb --- /dev/null +++ b/app/keyboards/notify_settings/NotifySettingsInlineKeyboard.py @@ -0,0 +1,59 @@ +from aiogram import types + +from app.helpers import UserHelper +from app.keyboards import AbstractInlineKeyboard +from app.models.users import UserNotifySettings + + +class NotifySettingsInlineKeyboard(AbstractInlineKeyboard): + + notify_settings_names_to_vars = { + 'marks': 'Оценки', + 'news': 'Новости', + 'discipline_sources': 'Ресурсы', + 'homeworks': 'Домашние задания', + 'requests': 'Заявки', + } + + @staticmethod + def _get_section_name_with_status(attribute_name: str, is_on_off: UserNotifySettings) -> str: + emoji = '🔔' if getattr(is_on_off, attribute_name) else '❌' + return f'{emoji} {NotifySettingsInlineKeyboard.notify_settings_names_to_vars[attribute_name]}' + + @staticmethod + def show(user_telegram_id: int) -> types.InlineKeyboardMarkup: + """ + is_on_off = { + 'Обучение': False, + 'Новости': False, + 'Ресурсы': False, + 'Домашние задания': False, + 'Заявки': False, + } + """ + is_on_off = UserHelper.get_user_settings_by_telegram_id(user_telegram_id) + + inline_kb_full: types.InlineKeyboardMarkup = types.InlineKeyboardMarkup(row_width=1) + inline_kb_full.add( + types.InlineKeyboardButton( + NotifySettingsInlineKeyboard._get_section_name_with_status('marks', is_on_off), + callback_data='notify_settings-marks' + ), + types.InlineKeyboardButton( + NotifySettingsInlineKeyboard._get_section_name_with_status('news', is_on_off), + callback_data='notify_settings-news' + ), + types.InlineKeyboardButton( + NotifySettingsInlineKeyboard._get_section_name_with_status('discipline_sources', is_on_off), + callback_data='notify_settings-discipline_sources' + ), + types.InlineKeyboardButton( + NotifySettingsInlineKeyboard._get_section_name_with_status('homeworks', is_on_off), + callback_data='notify_settings-homeworks' + ), + types.InlineKeyboardButton( + NotifySettingsInlineKeyboard._get_section_name_with_status('requests', is_on_off), + callback_data='notify_settings-requests' + ) + ) + return inline_kb_full diff --git a/app/keyboards/notify_settings/NotifySettingsReplyKeyboard.py b/app/keyboards/notify_settings/NotifySettingsReplyKeyboard.py new file mode 100644 index 0000000..8cc9e2e --- /dev/null +++ b/app/keyboards/notify_settings/NotifySettingsReplyKeyboard.py @@ -0,0 +1,15 @@ +from aiogram import types + +from app.keyboards import AbstractReplyKeyboard + + +class NotifySettingsReplyKeyboard(AbstractReplyKeyboard): + + @staticmethod + async def show() -> types.ReplyKeyboardMarkup: + keyboard_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True) + keyboard_markup.row(types.KeyboardButton('Настройка уведомлений')) + + more_btns_text = ('Руководство', 'О проекте') + keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text)) + return keyboard_markup diff --git a/app/keyboards/notify_settings/__init__.py b/app/keyboards/notify_settings/__init__.py new file mode 100644 index 0000000..cfa728c --- /dev/null +++ b/app/keyboards/notify_settings/__init__.py @@ -0,0 +1,5 @@ +from .NotifySettingsReplyKeyboard import NotifySettingsReplyKeyboard +from .NotifySettingsInlineKeyboard import NotifySettingsInlineKeyboard + + +__all__ = ['NotifySettingsReplyKeyboard', 'NotifySettingsInlineKeyboard'] diff --git a/app/menus/orioks/OrioksAuthFailedMenu.py b/app/menus/orioks/OrioksAuthFailedMenu.py index 028fbea..562fc96 100644 --- a/app/menus/orioks/OrioksAuthFailedMenu.py +++ b/app/menus/orioks/OrioksAuthFailedMenu.py @@ -1,7 +1,7 @@ from aiogram.utils import markdown import app -import keyboards +from app.keyboards.authorization import AuthorizationReplyKeyboard from app.helpers import UserHelper from app.menus.AbstractMenu import AbstractMenu @@ -18,5 +18,5 @@ async def show(chat_id: int, telegram_user_id: int) -> None: markdown.text('Попробуйте ещё раз: /login'), sep='\n', ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация') + reply_markup=await AuthorizationReplyKeyboard.show(), ) diff --git a/app/menus/start/StartMenu.py b/app/menus/start/StartMenu.py index 6d92052..0da4c96 100644 --- a/app/menus/start/StartMenu.py +++ b/app/menus/start/StartMenu.py @@ -4,7 +4,7 @@ from app.helpers import UserHelper from app.menus.AbstractMenu import AbstractMenu -import keyboards +from app.keyboards.notify_settings import NotifySettingsReplyKeyboard class StartMenu(AbstractMenu): @@ -24,7 +24,7 @@ async def show(chat_id: int, telegram_user_id: int) -> None: markdown.text('Выполнить вход в аккаунт ОРИОКС: /login'), sep='\n', ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Авторизация'), + reply_markup=await NotifySettingsReplyKeyboard.show(), ) else: await app.bot.send_message( @@ -36,5 +36,5 @@ async def show(chat_id: int, telegram_user_id: int) -> None: markdown.text('Выполнить выход из аккаунта ОРИОКС: /logout'), sep='\n', ), - reply_markup=keyboards.main_menu_keyboard(first_btn_text='Настройка уведомлений') + reply_markup=await NotifySettingsReplyKeyboard.show(), ) diff --git a/app/menus/start/__init__.py b/app/menus/start/__init__.py index ebee60d..f109b2d 100644 --- a/app/menus/start/__init__.py +++ b/app/menus/start/__init__.py @@ -1 +1,3 @@ from .StartMenu import StartMenu + +__all__ = ['StartMenu'] diff --git a/app/migrations/env.py b/app/migrations/env.py index 4c8cf0f..f6bfb30 100644 --- a/app/migrations/env.py +++ b/app/migrations/env.py @@ -8,6 +8,10 @@ from app.models import DeclarativeModelBase from config import config as application_config +from app.models.users import UserStatus, UserNotifySettings +from app.models.admins import AdminStatistics + + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/checking/homeworks/get_orioks_homeworks.py b/checking/homeworks/get_orioks_homeworks.py index 4cf4d35..023dfa9 100644 --- a/checking/homeworks/get_orioks_homeworks.py +++ b/checking/homeworks/get_orioks_homeworks.py @@ -120,7 +120,7 @@ async def user_homeworks_check(user_telegram_id: int, session: aiohttp.ClientSes try: homeworks_dict = await get_orioks_homeworks(session=session) except OrioksParseDataException: - logging.info('(HOMEWORKS) exception: utils.exceptions.OrioksCantParseData') + logging.info('(HOMEWORKS) [%s] exception: utils.exceptions.OrioksCantParseData' % (user_telegram_id,)) CommonHelper.safe_delete(path=path_users_to_file) return None if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): diff --git a/checking/marks/get_orioks_marks.py b/checking/marks/get_orioks_marks.py index ec85bb6..e25a10c 100644 --- a/checking/marks/get_orioks_marks.py +++ b/checking/marks/get_orioks_marks.py @@ -107,7 +107,7 @@ async def user_marks_check(user_telegram_id: int, session: aiohttp.ClientSession await TelegramMessageHelper.message_to_admins(message=f'FileNotFoundError - {user_telegram_id}') raise Exception(f'FileNotFoundError - {user_telegram_id}') from exception except OrioksParseDataException: - logging.info('(MARKS) exception: utils.exceptions.OrioksCantParseData') + logging.info('(MARKS) [%s] exception: utils.exceptions.OrioksCantParseData' % (user_telegram_id,)) CommonHelper.safe_delete(path=path_users_to_file) return None diff --git a/checking/news/get_orioks_news.py b/checking/news/get_orioks_news.py index 50b9bd5..5889a73 100644 --- a/checking/news/get_orioks_news.py +++ b/checking/news/get_orioks_news.py @@ -77,7 +77,7 @@ async def get_current_new(user_telegram_id: int, session: aiohttp.ClientSession) try: last_news_id = await get_orioks_news(session=session) except OrioksParseDataException as exception: - logging.info('(NEWS) exception: utils.exceptions.OrioksCantParseData') + logging.info('(NEWS) [%s] exception: utils.exceptions.OrioksCantParseData' % (user_telegram_id,)) CommonHelper.safe_delete(path=path_users_to_file) raise OrioksParseDataException from exception return await get_news_by_news_id(news_id=last_news_id['last_id'], session=session) diff --git a/checking/requests/get_orioks_requests.py b/checking/requests/get_orioks_requests.py index 1bc5d36..dcd9ea0 100644 --- a/checking/requests/get_orioks_requests.py +++ b/checking/requests/get_orioks_requests.py @@ -121,7 +121,7 @@ async def _user_requests_check_with_subsection(user_telegram_id: int, section: s try: requests_dict = await get_orioks_requests(section=section, session=session) except OrioksParseDataException: - logging.info('(REQUESTS) exception: utils.exceptions.OrioksCantParseData') + logging.info('(REQUESTS) [%s] exception: utils.exceptions.OrioksCantParseData' % (user_telegram_id,)) CommonHelper.safe_delete(path=path_users_to_file) return None if student_json_file not in os.listdir(os.path.dirname(path_users_to_file)): diff --git a/keyboards.py b/keyboards.py deleted file mode 100644 index c5717fc..0000000 --- a/keyboards.py +++ /dev/null @@ -1,10 +0,0 @@ -from aiogram import types - - -def main_menu_keyboard(first_btn_text: str) -> types.ReplyKeyboardMarkup: - keyboard_markup = types.ReplyKeyboardMarkup(row_width=2, resize_keyboard=True) - keyboard_markup.row(types.KeyboardButton(str(first_btn_text))) - - more_btns_text = ('Руководство', 'О проекте') - keyboard_markup.add(*(types.KeyboardButton(text) for text in more_btns_text)) - return keyboard_markup