├── __init__.py ├── bot ├── __init__.py ├── states │ ├── __init__.py │ └── SQLAlchemyStorage.py ├── handlers │ ├── channels │ │ └── __init__.py │ ├── groups │ │ └── __init__.py │ ├── errors │ │ ├── __init__.py │ │ └── error_handler.py │ ├── users │ │ ├── __init__.py │ │ ├── help.py │ │ ├── start.py │ │ └── echo.py │ └── __init__.py ├── keyboards │ ├── default │ │ └── __init__.py │ ├── inline │ │ ├── __init__.py │ │ └── callback_datas.py │ └── __init__.py ├── utils │ ├── misc │ │ ├── __init__.py │ │ └── throttling.py │ ├── __init__.py │ ├── set_bot_commands.py │ └── notify_admins.py ├── filters │ └── __init__.py ├── middlewares │ ├── __init__.py │ └── throttling.py ├── loader.py ├── main.py ├── texts.py └── .gitignore ├── server ├── __init__.py ├── model_views │ ├── AdminConfig.py │ ├── BotSettingsView.py │ ├── HiddenModelView.py │ ├── __init__.py │ ├── OrdersModelView.py │ ├── mixins │ │ └── AuthMixin.py │ ├── TextsModelView.py │ ├── AdminModelView.py │ ├── CKEditorModelView.py │ └── HomeView.py ├── templates │ ├── home.html │ ├── text_model_view.html │ ├── static │ │ └── js │ │ │ ├── home_chart.js │ │ │ └── bot_settings.js │ ├── bot_settings_view.html │ ├── base.html │ └── login.html ├── auth.py └── main.py ├── .env ├── database ├── __init__.py ├── models │ ├── __init__.py │ ├── texts.py │ ├── product.py │ ├── order.py │ ├── admin.py │ └── user.py ├── db_config.py └── loader.py ├── daemon.py ├── config.py ├── requirements.txt ├── README.md └── .gitignore /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/states/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | BOT_SECRET_TOKEN="TOKEN_HERE" -------------------------------------------------------------------------------- /bot/handlers/channels/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/handlers/groups/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/keyboards/default/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bot/keyboards/inline/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /database/__init__.py: -------------------------------------------------------------------------------- 1 | from .loader import db -------------------------------------------------------------------------------- /bot/keyboards/inline/callback_datas.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /bot/handlers/errors/__init__.py: -------------------------------------------------------------------------------- 1 | from . import error_handler 2 | -------------------------------------------------------------------------------- /bot/utils/misc/__init__.py: -------------------------------------------------------------------------------- 1 | from .throttling import rate_limit 2 | -------------------------------------------------------------------------------- /bot/keyboards/__init__.py: -------------------------------------------------------------------------------- 1 | from . import default 2 | from . import inline 3 | -------------------------------------------------------------------------------- /bot/utils/__init__.py: -------------------------------------------------------------------------------- 1 | from . import misc 2 | from .notify_admins import on_startup_notify 3 | -------------------------------------------------------------------------------- /bot/handlers/users/__init__.py: -------------------------------------------------------------------------------- 1 | from . import echo 2 | from . import help 3 | from . import start 4 | -------------------------------------------------------------------------------- /server/model_views/AdminConfig.py: -------------------------------------------------------------------------------- 1 | 2 | class Categories: 3 | MANAGEMENT = 'Management' 4 | 5 | 6 | -------------------------------------------------------------------------------- /bot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import channels 2 | from . import errors 3 | from . import groups 4 | from . import users 5 | -------------------------------------------------------------------------------- /bot/filters/__init__.py: -------------------------------------------------------------------------------- 1 | # from .is_admin import AdminFilter 2 | 3 | 4 | if __name__ == "filters": 5 | #dp.filters_factory.bind(is_admin) 6 | pass 7 | -------------------------------------------------------------------------------- /database/models/__init__.py: -------------------------------------------------------------------------------- 1 | from .admin import AdminUser 2 | from .order import Order 3 | from .product import Product 4 | from .texts import Texts 5 | from .user import User 6 | -------------------------------------------------------------------------------- /bot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from bot.loader import dp 2 | from .throttling import ThrottlingMiddleware 3 | 4 | 5 | if __name__ == "middlewares": 6 | dp.middleware.setup(ThrottlingMiddleware()) 7 | -------------------------------------------------------------------------------- /bot/loader.py: -------------------------------------------------------------------------------- 1 | from aiogram import Bot, Dispatcher, types 2 | 3 | from bot.states.SQLAlchemyStorage import SQLAlchemyStorage 4 | from config import BOT_TOKEN 5 | 6 | bot = Bot(token=BOT_TOKEN, parse_mode=types.ParseMode.HTML) 7 | storage = SQLAlchemyStorage() 8 | dp = Dispatcher(bot, storage=storage) 9 | -------------------------------------------------------------------------------- /bot/utils/set_bot_commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | 3 | 4 | async def set_default_commands(dp): 5 | await dp.bot.set_my_commands( 6 | [ 7 | types.BotCommand("start", "Запустить бота"), 8 | types.BotCommand("help", "Вывести справку"), 9 | ] 10 | ) 11 | -------------------------------------------------------------------------------- /server/model_views/BotSettingsView.py: -------------------------------------------------------------------------------- 1 | from flask_admin import BaseView, expose 2 | 3 | from server.model_views.mixins.AuthMixin import AuthMixin 4 | 5 | 6 | class BotSettingsView(AuthMixin, BaseView): 7 | @expose('/') 8 | def index(self): 9 | return self.render('bot_settings_view.html') 10 | -------------------------------------------------------------------------------- /bot/utils/notify_admins.py: -------------------------------------------------------------------------------- 1 | from aiogram import Dispatcher 2 | from loguru import logger 3 | 4 | from config import ADMINS 5 | 6 | 7 | async def on_startup_notify(dp: Dispatcher): 8 | for admin in ADMINS: 9 | try: 10 | await dp.bot.send_message(admin, "Bot was started") 11 | 12 | except Exception as err: 13 | logger.error(err) 14 | -------------------------------------------------------------------------------- /server/model_views/HiddenModelView.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqla import ModelView 2 | 3 | from server.model_views.mixins.AuthMixin import AuthMixin 4 | 5 | 6 | class HiddenModelView(AuthMixin, ModelView): 7 | 8 | def is_visible(self): 9 | return False 10 | 11 | def __init__(self, model, session, **kwargs): 12 | super(HiddenModelView, self).__init__(model, session, **kwargs) -------------------------------------------------------------------------------- /server/model_views/__init__.py: -------------------------------------------------------------------------------- 1 | from .AdminModelView import AdminModelView 2 | from .BotSettingsView import BotSettingsView 3 | from .CKEditorModelView import CKEditorModelView 4 | from .HiddenModelView import HiddenModelView 5 | from .OrdersModelView import OrderModelView 6 | from .TextsModelView import TextsModelView 7 | 8 | __all__ = [BotSettingsView, OrderModelView, CKEditorModelView, HiddenModelView] 9 | 10 | -------------------------------------------------------------------------------- /server/model_views/OrdersModelView.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqla import ModelView 2 | 3 | from database.models import Order 4 | from server.model_views.mixins.AuthMixin import AuthMixin 5 | 6 | 7 | class OrderModelView(AuthMixin, ModelView): 8 | form_excluded_columns = ('product_id') 9 | 10 | def __init__(self, session, **kwargs): 11 | super(OrderModelView, self).__init__(Order, session, **kwargs) 12 | -------------------------------------------------------------------------------- /bot/handlers/users/help.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters.builtin import CommandHelp 3 | 4 | from bot.loader import dp 5 | 6 | 7 | @dp.message_handler(CommandHelp()) 8 | async def bot_help(message: types.Message): 9 | text = ("Список команд: ", 10 | "/start - Начать диалог", 11 | "/help - Получить справку") 12 | 13 | await message.answer("\n".join(text)) 14 | -------------------------------------------------------------------------------- /database/db_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from config import DEBUG, LOCAL_DATABASE_URL, DATABASE_URL, USE_LOCAL_VARIABLES 4 | basedir = os.path.abspath(os.path.dirname(__file__)) 5 | 6 | 7 | class Config(object): 8 | DEBUG = DEBUG 9 | TESTING = False 10 | CSRF_ENABLED = True 11 | SQLALCHEMY_DATABASE_URI = LOCAL_DATABASE_URL if USE_LOCAL_VARIABLES else DATABASE_URL 12 | SQLALCHEMY_TRACK_MODIFICATIONS = False 13 | -------------------------------------------------------------------------------- /server/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% block body %} 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | {% endblock %} -------------------------------------------------------------------------------- /bot/utils/misc/throttling.py: -------------------------------------------------------------------------------- 1 | def rate_limit(limit: int, key=None): 2 | """ 3 | Decorator for configuring rate limit and key in different functions. 4 | 5 | :param limit: 6 | :param key: 7 | :return: 8 | """ 9 | 10 | def decorator(func): 11 | setattr(func, 'throttling_rate_limit', limit) 12 | if key: 13 | setattr(func, 'throttling_key', key) 14 | return func 15 | 16 | return decorator 17 | -------------------------------------------------------------------------------- /server/model_views/mixins/AuthMixin.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from flask_login import current_user 3 | from werkzeug.utils import redirect 4 | 5 | 6 | class AuthMixin(object): 7 | def is_accessible(self): 8 | if current_user.is_authenticated: 9 | return True 10 | return False 11 | 12 | def _handle_view(self, name, **kwargs): 13 | if not self.is_accessible(): 14 | return redirect(url_for('auth.login')) 15 | -------------------------------------------------------------------------------- /bot/main.py: -------------------------------------------------------------------------------- 1 | from aiogram import executor 2 | 3 | from bot.loader import dp 4 | from bot.utils.notify_admins import on_startup_notify 5 | from bot.utils.set_bot_commands import set_default_commands 6 | 7 | 8 | async def on_startup(dispatcher): 9 | await set_default_commands(dispatcher) 10 | await on_startup_notify(dispatcher) 11 | 12 | 13 | def bot_init(): 14 | executor.start_polling(dp, on_startup=on_startup) 15 | 16 | 17 | if __name__ == '__main__': 18 | bot_init() 19 | -------------------------------------------------------------------------------- /server/templates/text_model_view.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/model/edit.html' %} 2 | 3 | {% block tail %} 4 | {{ super() }} 5 | 6 | {{ ckeditor.config(name='value', custom_config="uiColor: '#e9eff0', enterMode: 2, 7 | fillEmptyBlocks: false, disallowedContent: 'div(*)  ', 8 | allowedContent: true, 9 | toolbarGroups: [{ name: 'links' }, { name: 'basicstyles', groups: [ 'basicstyles', 'cleanup' ] }]") }} 10 | {% endblock %} -------------------------------------------------------------------------------- /database/models/texts.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String 2 | 3 | from database.loader import db 4 | 5 | 6 | class Texts(db.Model): 7 | __tablename__ = 'texts' 8 | id = Column(Integer, primary_key=True) 9 | 10 | name = Column(String(length=80), nullable=False) 11 | value = Column(db.UnicodeText) 12 | 13 | def __init__(self, name, value="empty text"): 14 | self.name = name 15 | self.value = value 16 | 17 | 18 | if __name__ == '__main__': 19 | db.create_all() 20 | db.session.commit() 21 | -------------------------------------------------------------------------------- /daemon.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | from time import sleep 3 | 4 | from loguru import logger 5 | 6 | 7 | class DaemonConfig: 8 | OPERATING_HOURS = (0,) 9 | ERROR_COOLDOWN_TIME = 5 * 60 10 | COOLDOWN_TIME = 60 * 60 11 | 12 | 13 | def daemon_init(): 14 | while True: 15 | if datetime.now().hour in DaemonConfig.OPERATING_HOURS: 16 | try: 17 | pass 18 | except Exception as error: 19 | logger.error(str(error)) 20 | sleep(DaemonConfig.ERROR_COOLDOWN_TIME) 21 | else: 22 | sleep(DaemonConfig.COOLDOWN_TIME) 23 | -------------------------------------------------------------------------------- /server/model_views/TextsModelView.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import requests 3 | from loguru import logger 4 | 5 | from .CKEditorModelView import CKEditorModelView 6 | from .mixins.AuthMixin import AuthMixin 7 | 8 | 9 | class TextsModelView(AuthMixin, CKEditorModelView): 10 | def after_model_change(self, form, model, is_created): 11 | if not is_created: # Model was updated 12 | try: 13 | ans = requests.post(flask.request.url_root + 'restart_bot') 14 | logger.info(ans.text) 15 | except Exception as error: 16 | logger.error(f"Bot restart error: {error}") 17 | -------------------------------------------------------------------------------- /database/models/product.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import Column, Integer, String, Float 2 | from sqlalchemy.orm import relationship 3 | 4 | from database.loader import db 5 | 6 | 7 | class Product(db.Model): 8 | id = Column(Integer, primary_key=True) 9 | 10 | name = Column(String(50)) 11 | description = Column(String(200)) 12 | weight = Column(Float) 13 | price = Column(Integer) 14 | available_quantity = Column(Integer, default=10) 15 | image_url = Column(String(200), nullable=True) 16 | 17 | orders = relationship('Order', backref='product') 18 | 19 | def __str__(self): 20 | return self.name 21 | -------------------------------------------------------------------------------- /bot/handlers/users/start.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher.filters.builtin import CommandStart 3 | 4 | from bot.loader import dp 5 | from bot.texts import _ 6 | from database.models import User 7 | 8 | 9 | @dp.message_handler(CommandStart()) 10 | async def bot_start(message: types.Message): 11 | referer_id = '' 12 | command_args = message.text.split('start ') 13 | if len(command_args) > 1: 14 | referer_id = command_args[1] 15 | User.register(message.from_user, referer_id=referer_id, chat_id=message.chat.id) 16 | 17 | await message.answer(_('start_text').format(message.from_user.full_name)) 18 | -------------------------------------------------------------------------------- /database/models/order.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy import Column, Integer, DateTime, ForeignKey 4 | 5 | from database.loader import db 6 | 7 | 8 | class Order(db.Model): 9 | id = Column(Integer, primary_key=True) 10 | 11 | product_id = Column(Integer, ForeignKey('product.id')) 12 | was_created = Column(DateTime(), default=datetime.datetime.now()) 13 | 14 | user_id = Column(Integer, ForeignKey('user.id')) 15 | 16 | def __init__(self, name): 17 | self.name = name 18 | 19 | def __str__(self): 20 | return f'{self.user.alias if self.user else ""} {self.product}' 21 | 22 | def __repr__(self): 23 | return self.__str__() 24 | -------------------------------------------------------------------------------- /bot/handlers/users/echo.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher import FSMContext 3 | 4 | from bot.loader import dp 5 | 6 | 7 | @dp.message_handler(state=None) 8 | async def bot_echo(message: types.Message): 9 | await message.answer(f"{message.text}" 10 | f"Вы были зарегестрированы {None}") 11 | 12 | 13 | @dp.message_handler(state="*", content_types=types.ContentTypes.ANY) 14 | async def bot_echo_all(message: types.Message, state: FSMContext): 15 | state = await state.get_state() 16 | await message.answer(f"Состояние {state}.\n" 17 | f"\nСообщение:\n" 18 | f"{message}\n") 19 | -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from dotenv import load_dotenv 4 | 5 | load_dotenv() # take environment variables from .env. 6 | 7 | DEBUG = os.getenv('DEBUG', 'False') == 'True' 8 | USE_LOCAL_VARIABLES = os.getenv('USE_LOCAL_VARIABLES', 'True') == 'True' 9 | 10 | # bot 11 | BOT_TOKEN = os.getenv('BOT_SECRET_TOKEN') 12 | ADMINS = [492621220] 13 | 14 | # server 15 | PORT = int(os.environ.get("PORT", 5000)) 16 | PRODUCTION_HOST = '0.0.0.0' 17 | LOCAL_HOST = '127.0.0.1' 18 | 19 | # main admin data 20 | ADMIN_EMAIL = os.getenv('ADMIN_PASSWORD', 'admin@admin') 21 | ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD', 'admin') 22 | 23 | # database 24 | LOCAL_DATABASE_URL = 'sqlite:///Main.db' 25 | DATABASE_URL = os.getenv('DATABASE_URL') 26 | -------------------------------------------------------------------------------- /database/loader.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from flask import Flask 4 | from flask_sqlalchemy import SQLAlchemy 5 | from loguru import logger 6 | 7 | from database.db_config import Config 8 | 9 | if os.path.exists('../server/templates'): 10 | templates_folder = os.path.abspath('../server/templates') 11 | else: 12 | # to run with command 13 | templates_folder = os.path.abspath('./server/templates') 14 | 15 | logger.info(f"Admin templates folder: {templates_folder}") 16 | app = Flask(__name__, template_folder=templates_folder, static_folder=templates_folder + '/static', ) 17 | app.config.from_object(Config) 18 | db = SQLAlchemy(app) 19 | 20 | if __name__ == '__main__': 21 | # db.drop_all() 22 | db.create_all() 23 | db.session.commit() 24 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.13 2 | aiohttp==3.7.4.post0 3 | alembic==1.6.5 4 | async-timeout==3.0.1 5 | asyncpg==0.23.0 6 | attrs==21.2.0 7 | Babel==2.9.1 8 | certifi==2021.5.30 9 | chardet==4.0.0 10 | click==8.0.1 11 | colorama==0.4.4 12 | Flask==2.0.1 13 | Flask-Admin==1.5.8 14 | Flask-CKEditor==0.4.6 15 | Flask-Login==0.5.0 16 | Flask-Migrate==3.0.1 17 | Flask-SQLAlchemy==2.5.1 18 | greenlet==1.1.0 19 | idna==2.10 20 | itsdangerous==2.0.1 21 | Jinja2==3.0.1 22 | Mako==1.1.4 23 | MarkupSafe==2.0.1 24 | multidict==5.1.0 25 | python-dateutil==2.8.1 26 | python-editor==1.0.4 27 | pytz==2021.1 28 | requests==2.25.1 29 | six==1.16.0 30 | SQLAlchemy==1.3.24 31 | typing-extensions==3.10.0.0 32 | urllib3==1.26.5 33 | Werkzeug==2.0.1 34 | WTForms==2.3.3 35 | yarl==1.6.3 36 | 37 | loguru~=0.5.3 38 | python-dotenv~=0.19.0 -------------------------------------------------------------------------------- /server/model_views/AdminModelView.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqla import ModelView 2 | from flask_login import current_user 3 | 4 | from database.models import AdminUser 5 | from server.model_views.mixins.AuthMixin import AuthMixin 6 | 7 | 8 | class AdminModelView(AuthMixin, ModelView): 9 | form_excluded_columns = ('password',) 10 | column_exclude_list = ('password',) 11 | 12 | def __init__(self, session, **kwargs): 13 | super(AdminModelView, self).__init__(AdminUser, session, **kwargs) 14 | 15 | @property 16 | def can_create(self): 17 | return current_user.is_super_admin 18 | 19 | @property 20 | def can_edit(self): 21 | return current_user.is_super_admin 22 | 23 | @property 24 | def can_delete(self): 25 | return current_user.is_super_admin and AdminUser.query.filter_by(is_super_admin=True).count() > 1 26 | -------------------------------------------------------------------------------- /server/model_views/CKEditorModelView.py: -------------------------------------------------------------------------------- 1 | from flask_admin.contrib.sqla import ModelView 2 | from wtforms import TextAreaField 3 | from wtforms.widgets import TextArea 4 | 5 | 6 | class CKTextAreaWidget(TextArea): 7 | def __call__(self, field, **kwargs): 8 | if kwargs.get('class'): 9 | kwargs['class'] += " ckeditor" 10 | else: 11 | kwargs.setdefault('class', 'ckeditor') 12 | return super(CKTextAreaWidget, self).__call__(field, **kwargs) 13 | 14 | 15 | class CKTextAreaField(TextAreaField): 16 | widget = CKTextAreaWidget() 17 | 18 | 19 | class CKEditorModelView(ModelView): 20 | column_list = ['id', 'value'] 21 | can_delete = False 22 | can_create = False 23 | form_overrides = dict(value=CKTextAreaField) 24 | 25 | create_template = 'text_model_view.html' 26 | edit_template = 'text_model_view.html' 27 | -------------------------------------------------------------------------------- /server/templates/static/js/home_chart.js: -------------------------------------------------------------------------------- 1 | let context = $("#lineChart").get(0).getContext('2d'); 2 | 3 | let chartData = $('#data').data('chart_data') 4 | 5 | let chart = new Chart(context, { 6 | type: 'line', 7 | data: { 8 | labels: chartData.labels, 9 | datasets: [ 10 | { 11 | label: "registrations", 12 | data: chartData.registrationsCount, 13 | fill: false, 14 | lineTension: 0.2, 15 | backgroundColor: "#27ab4f" 16 | }, 17 | { 18 | label: "orders", 19 | data: chartData.ordersCount, 20 | fill: false, 21 | lineTension: 0.2, 22 | backgroundColor: "#9fe817", 23 | }, 24 | ] 25 | }, 26 | options: { 27 | responsive: true 28 | }, 29 | }) -------------------------------------------------------------------------------- /database/models/admin.py: -------------------------------------------------------------------------------- 1 | from flask_login import UserMixin 2 | from sqlalchemy import Column, Integer, String, Boolean 3 | from werkzeug.security import generate_password_hash 4 | 5 | from database.loader import db 6 | 7 | 8 | class AdminUser(UserMixin, db.Model): 9 | """ 10 | Admin panel authentication model 11 | """ 12 | id = Column(Integer, primary_key=True) 13 | email = Column(String(100), unique=True) 14 | password = Column(String(100)) 15 | name = Column(String(100)) 16 | is_super_admin = Column(Boolean(), default=False) 17 | 18 | @staticmethod 19 | def register(email, name, password, is_super_admin=False): 20 | new_user = AdminUser(email=email, password=generate_password_hash(password, method='sha256'), 21 | is_super_admin=is_super_admin, name=name) 22 | db.session.add(new_user) 23 | db.session.commit() 24 | return new_user 25 | -------------------------------------------------------------------------------- /server/templates/bot_settings_view.html: -------------------------------------------------------------------------------- 1 | {% extends 'admin/master.html' %} 2 | {% block body %} 3 | 4 | 5 |
6 | 9 | 10 |
11 | 12 |
13 |
14 | 15 | 16 |
17 | 18 | 19 | 20 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Telegram bot template 2 | 3 | Template for creating scalable bots with aiogram 4 | 5 | ### What's in the template? 6 | 7 | * Admin panel with the ability to view/edit/delete the database + analytics charts + ability to edit texts used in the 8 | bot + bot total control 9 | * All necessary directories for bot development 10 | * Basic database tables (ORM) 11 | 12 | ### Development 13 | 14 | #### Technologies 15 | 16 | * Python 3.8 17 | * Aiogram 18 | * Flask 19 | * SQLAlchemy 20 | * multiprocessing 21 | 22 | #### Project structure 23 | 24 | * bot 25 | * filters 26 | * handlers 27 | * keyboards 28 | * middlewares 29 | * states 30 | * utils 31 | * texts 32 | * server (admin part) 33 | * model_views 34 | * templates 35 | * database 36 | * models 37 | 38 | Application package is in `server/main.py` 39 | 40 | #### TODO 41 | 42 | Change flask-sqlalchemy to async sqlalchemy 43 | 44 | #### Have any questions?? 45 | 46 | Telegram: @nmzgnv 47 | -------------------------------------------------------------------------------- /server/auth.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, render_template, redirect, url_for, flash, request 2 | from flask_login import login_user, logout_user 3 | from werkzeug.security import check_password_hash 4 | 5 | from database.models import AdminUser 6 | 7 | auth = Blueprint('auth', __name__) 8 | 9 | 10 | @auth.route('/login', methods=['GET']) 11 | def login(): 12 | return render_template('login.html') 13 | 14 | 15 | @auth.route('/login', methods=['POST']) 16 | def login_post(): 17 | email = request.form.get('email') 18 | password = request.form.get('password') 19 | remember = True if request.form.get('remember') else False 20 | 21 | user = AdminUser.query.filter_by(email=email).first() 22 | if (not user) or (not check_password_hash(user.password, password)): 23 | flash('Please check your login details and try again.') 24 | return redirect(url_for('auth.login')) 25 | 26 | login_user(user, remember=remember) 27 | 28 | return redirect("/admin") 29 | 30 | 31 | @auth.route('/logout') 32 | def logout(): 33 | logout_user() 34 | return redirect(url_for('auth.login')) 35 | -------------------------------------------------------------------------------- /server/templates/static/js/bot_settings.js: -------------------------------------------------------------------------------- 1 | const sureText = "Are you sure?"; 2 | const successText = "Success!"; 3 | const errorText = "Error"; 4 | 5 | $.get("/get_current_bot_token", (data) => { 6 | $("#tokenInputField").val(data.token); 7 | }); 8 | 9 | const SendRequestIfConfirmed = (url) => { 10 | if (window.confirm(sureText)) { 11 | $.post(url, {}) 12 | .done((data) => { 13 | alert(successText); 14 | }) 15 | .fail(() => { 16 | alert(errorText); 17 | }); 18 | } 19 | } 20 | 21 | $("#changeTokenButton").click(() => { 22 | if (window.confirm(sureText)) { 23 | let data = {token: $('#tokenInputField').val()}; 24 | $.post("/change_token", data, () => { 25 | console.log(successText) 26 | }) 27 | .done((data) => { 28 | alert("Token changed!"); 29 | }) 30 | .fail(() => { 31 | alert(errorText); 32 | }); 33 | } 34 | }); 35 | 36 | $("#stopBotButton").click(() => { 37 | SendRequestIfConfirmed("/stop_bot"); 38 | }); 39 | 40 | $("#restartBotButton").click(() => { 41 | SendRequestIfConfirmed("/restart_bot"); 42 | }); -------------------------------------------------------------------------------- /server/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Admin panel 8 | 9 | 10 | 11 | 12 |
13 |
14 | 28 |
29 | 30 |
31 |
32 | {% block content %} 33 | {% endblock %} 34 |
35 |
36 |
37 | 38 | 39 | -------------------------------------------------------------------------------- /bot/middlewares/throttling.py: -------------------------------------------------------------------------------- 1 | from aiogram import types, Dispatcher 2 | from aiogram.dispatcher import DEFAULT_RATE_LIMIT 3 | from aiogram.dispatcher.handler import CancelHandler, current_handler 4 | from aiogram.dispatcher.middlewares import BaseMiddleware 5 | from aiogram.utils.exceptions import Throttled 6 | 7 | 8 | class ThrottlingMiddleware(BaseMiddleware): 9 | def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'): 10 | self.rate_limit = limit 11 | self.prefix = key_prefix 12 | super(ThrottlingMiddleware, self).__init__() 13 | 14 | async def on_process_message(self, message: types.Message, data: dict): 15 | handler = current_handler.get() 16 | dispatcher = Dispatcher.get_current() 17 | if handler: 18 | limit = getattr(handler, "throttling_rate_limit", self.rate_limit) 19 | key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}") 20 | else: 21 | limit = self.rate_limit 22 | key = f"{self.prefix}_message" 23 | try: 24 | await dispatcher.throttle(key, rate=limit) 25 | except Throttled as t: 26 | await self.message_throttled(message, t) 27 | raise CancelHandler() 28 | 29 | async def message_throttled(self, message: types.Message, throttled: Throttled): 30 | if throttled.exceeded_count <= 2: 31 | await message.reply("Too many requests!") 32 | -------------------------------------------------------------------------------- /server/templates/login.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
5 |

Login

6 |
7 | {% with messages = get_flashed_messages() %} 8 | {% if messages %} 9 |
10 | {{ messages[0] }} 11 |
12 | {% endif %} 13 | {% endwith %} 14 |
15 |
16 |
17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | 31 |
32 | 33 |
34 |
35 |
36 | {% endblock %} -------------------------------------------------------------------------------- /server/model_views/HomeView.py: -------------------------------------------------------------------------------- 1 | import json 2 | from datetime import datetime, timedelta 3 | 4 | from flask_admin import expose, AdminIndexView 5 | from sqlalchemy import func 6 | 7 | from database.models import User, Order 8 | from server.model_views.mixins.AuthMixin import AuthMixin 9 | 10 | 11 | class HomeView(AuthMixin, AdminIndexView): 12 | @staticmethod 13 | def get_chart_data(period=30): 14 | """ 15 | :param period: number of last days to receive data 16 | :return: json object 17 | """ 18 | from_date = datetime.now().date() - timedelta(days=period) 19 | 20 | labels = [] 21 | user_registrations = [] 22 | order_creations = [] 23 | 24 | for i in range(1, period + 1): 25 | current_date = from_date + timedelta(days=i) 26 | labels.append(current_date.strftime('%d-%m-%Y')) 27 | 28 | registrations_count = User.query.filter(func.date(User.was_registered) == current_date).count() 29 | user_registrations.append(registrations_count) 30 | 31 | orders_count = Order.query.filter(func.date(Order.was_created) == current_date).count() 32 | order_creations.append(orders_count) 33 | 34 | return json.dumps({ 35 | 'period': period, 36 | 'labels': labels, 37 | 'registrationsCount': user_registrations, 38 | 'ordersCount': order_creations, 39 | }) 40 | 41 | @expose('/') 42 | def index(self): 43 | return self.render('home.html', chart_data=self.get_chart_data()) 44 | -------------------------------------------------------------------------------- /bot/texts.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from loguru import logger 4 | 5 | from database.loader import db 6 | from database.models import Texts 7 | 8 | base_texts = { 9 | "start_text": 'Это {}. Привет!', 10 | 'require_channel_subscribe_text': 'Для продолжения подпишитесь на {}', 11 | } 12 | 13 | cached_texts = {} 14 | 15 | 16 | def format_text_to_send(message_text): 17 | """ 18 | :param message_text: text with html tags 19 | :return: clean text for sending in telegram 20 | """ 21 | message_text = re.sub("(?s)]*)?>.*?", "", message_text) # removing divs with content 22 | message_text = re.sub("
|
|
|

", '\n', message_text) 23 | message_text = re.sub("

|

||
  • |
  • |
      |
    |
    |
    | ", "", message_text) 24 | return message_text 25 | 26 | 27 | def caсhe_texts(): 28 | global cached_texts 29 | db.create_all() 30 | 31 | if Texts.query.count() != len(base_texts.keys()): 32 | # filling database by base texts 33 | deleted_rows_num = db.session.query(Texts).delete() 34 | logger.info(f"Was deleted: {deleted_rows_num} rows from Texts table") 35 | 36 | for name, value in base_texts.items(): 37 | text = Texts(name, value) 38 | cached_texts[name] = format_text_to_send(value) 39 | db.session.add(text) 40 | 41 | db.session.commit() 42 | else: 43 | for item in Texts.query.all(): 44 | cached_texts[item.name] = format_text_to_send(item.value) 45 | 46 | 47 | caсhe_texts() 48 | cached_keys = cached_texts.keys() 49 | 50 | 51 | def _(text_name): 52 | global cached_texts, cached_keys 53 | assert text_name in cached_keys, f'Text with name "{text_name}" not found' 54 | return cached_texts[text_name] 55 | -------------------------------------------------------------------------------- /database/models/user.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy import Column, Integer, String, Boolean, DateTime, and_ 4 | from sqlalchemy.orm import relationship 5 | 6 | from database.loader import db 7 | 8 | 9 | class User(db.Model): 10 | id = Column(Integer, primary_key=True) 11 | 12 | telegram_id = Column(String(40), unique=True) 13 | chat_id = Column(String(40)) 14 | 15 | alias = Column(String(40)) 16 | 17 | referer_id = Column(String(40)) 18 | is_banned = Column(Boolean) 19 | 20 | state = Column(String(50), default=None, nullable=True) 21 | state_data = Column(String(1000), default=None, nullable=True) 22 | 23 | balance = Column(Integer, default=0) 24 | orders = relationship("Order", backref='user') 25 | was_registered = Column(DateTime, default=datetime.datetime.now()) 26 | 27 | def __init__(self, telegram_id, alias, referer_id='', chat_id=''): 28 | self.telegram_id = str(telegram_id) 29 | self.chat_id = str(chat_id) 30 | self.alias = alias 31 | self.referer_id = referer_id 32 | self.orders = [] 33 | self.is_banned = False 34 | self.balance = 0 35 | 36 | def __repr__(self): 37 | return f'{self.alias} : {self.telegram_id}' 38 | 39 | def get_registration_date(self): 40 | return self.was_registered.date() 41 | 42 | @staticmethod 43 | def register(telegram_user, chat_id, referer_id=''): 44 | if not User.query.filter_by(telegram_id=str(telegram_user.id)).first(): 45 | if not User.query.filter_by(telegram_id=referer_id).first(): 46 | referer_id = '' 47 | 48 | db.session.add(User(telegram_user.id, telegram_user.username, referer_id=referer_id, chat_id=chat_id)) 49 | db.session.commit() 50 | 51 | @staticmethod 52 | def get(user_id: str = '', chat_id: str = ''): 53 | user = User.query.filter(and_(User.telegram_id == user_id, User.chat_id == chat_id)).first() 54 | 55 | if not user: 56 | ValueError('User does not exist') 57 | 58 | return user 59 | -------------------------------------------------------------------------------- /bot/handlers/errors/error_handler.py: -------------------------------------------------------------------------------- 1 | from aiogram.utils.exceptions import (Unauthorized, InvalidQueryID, TelegramAPIError, 2 | CantDemoteChatCreator, MessageNotModified, MessageToDeleteNotFound, 3 | MessageTextIsEmpty, RetryAfter, 4 | CantParseEntities, MessageCantBeDeleted) 5 | from loguru import logger 6 | 7 | from bot.loader import dp 8 | 9 | 10 | @dp.errors_handler() 11 | async def errors_handler(update, exception): 12 | """ 13 | Exceptions handler. Catches all exceptions within task factory tasks. 14 | :param dispatcher: 15 | :param update: 16 | :param exception: 17 | :return: stdout logging 18 | """ 19 | 20 | if isinstance(exception, CantDemoteChatCreator): 21 | logger.exception("Can't demote chat creator") 22 | return True 23 | 24 | if isinstance(exception, MessageNotModified): 25 | logger.exception('Message is not modified') 26 | return True 27 | if isinstance(exception, MessageCantBeDeleted): 28 | logger.exception('Message cant be deleted') 29 | return True 30 | 31 | if isinstance(exception, MessageToDeleteNotFound): 32 | logger.exception('Message to delete not found') 33 | return True 34 | 35 | if isinstance(exception, MessageTextIsEmpty): 36 | logger.exception('MessageTextIsEmpty') 37 | return True 38 | 39 | if isinstance(exception, Unauthorized): 40 | logger.exception(f'Unauthorized: {exception}') 41 | return True 42 | 43 | if isinstance(exception, InvalidQueryID): 44 | logger.exception(f'InvalidQueryID: {exception} \nUpdate: {update}') 45 | return True 46 | 47 | if isinstance(exception, TelegramAPIError): 48 | logger.exception(f'TelegramAPIError: {exception} \nUpdate: {update}') 49 | return True 50 | if isinstance(exception, RetryAfter): 51 | logger.exception(f'RetryAfter: {exception} \nUpdate: {update}') 52 | return True 53 | if isinstance(exception, CantParseEntities): 54 | logger.exception(f'CantParseEntities: {exception} \nUpdate: {update}') 55 | return True 56 | 57 | logger.exception(f'Update: {update} \n{exception}') 58 | -------------------------------------------------------------------------------- /bot/.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .venv 106 | */.env 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .idea/* -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.db 3 | .env 4 | 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | share/python-wheels/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | MANIFEST 32 | 33 | # PyInstaller 34 | # Usually these files are written by a python script from a template 35 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 36 | *.manifest 37 | *.spec 38 | 39 | # Installer logs 40 | pip-log.txt 41 | pip-delete-this-directory.txt 42 | 43 | # Unit test / coverage reports 44 | htmlcov/ 45 | .tox/ 46 | .nox/ 47 | .coverage 48 | .coverage.* 49 | .cache 50 | nosetests.xml 51 | coverage.xml 52 | *.cover 53 | *.py,cover 54 | .hypothesis/ 55 | .pytest_cache/ 56 | cover/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | db.sqlite3 66 | db.sqlite3-journal 67 | 68 | # Flask stuff: 69 | instance/ 70 | .webassets-cache 71 | 72 | # Scrapy stuff: 73 | .scrapy 74 | 75 | # Sphinx documentation 76 | docs/_build/ 77 | 78 | # PyBuilder 79 | .pybuilder/ 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | # For a library or package, you might want to ignore these files since the code is 91 | # intended to run in multiple environments; otherwise, check them in: 92 | # .python-version 93 | 94 | # pipenv 95 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 96 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 97 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 98 | # install all needed dependencies. 99 | #Pipfile.lock 100 | 101 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 102 | __pypackages__/ 103 | 104 | # Celery stuff 105 | celerybeat-schedule 106 | celerybeat.pid 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | .dmypy.json 133 | dmypy.json 134 | 135 | # Pyre type checker 136 | .pyre/ 137 | 138 | # pytype static type analyzer 139 | .pytype/ 140 | 141 | # Cython debug symbols 142 | cython_debug/ -------------------------------------------------------------------------------- /server/main.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import sys 3 | 4 | sys.path.insert(0, str(pathlib.Path().resolve()).split('\server')[0]) 5 | 6 | from loguru import logger 7 | import multiprocessing 8 | import os 9 | 10 | from flask_admin import Admin 11 | from flask import redirect, request, make_response 12 | from flask_admin.contrib.sqla import ModelView 13 | from flask_ckeditor import CKEditor 14 | from flask_login import LoginManager 15 | from flask_migrate import Migrate 16 | 17 | from bot.main import bot_init 18 | from config import PORT, USE_LOCAL_VARIABLES, LOCAL_HOST, PRODUCTION_HOST, BOT_TOKEN, ADMIN_EMAIL, ADMIN_PASSWORD 19 | 20 | from daemon import daemon_init 21 | from database.loader import app, db 22 | from database.models import User, Texts, Order, Product, AdminUser 23 | from server.model_views import BotSettingsView 24 | from server.model_views.AdminConfig import Categories 25 | from server.model_views.AdminModelView import AdminModelView 26 | from server.model_views.HomeView import HomeView 27 | from server.model_views.TextsModelView import TextsModelView 28 | from server.auth import auth as auth_blueprint 29 | 30 | logger.add("debug.log", format='{time} {level} {message}', level="DEBUG", rotation="00:00", compression="zip") 31 | 32 | telegram_bot = multiprocessing.Process(target=bot_init) 33 | daemon = multiprocessing.Process(target=daemon_init) 34 | daemon.daemon = True 35 | 36 | ckeditor = CKEditor(app) 37 | migrate = Migrate(app, db) 38 | 39 | 40 | @app.route('/') 41 | def start_page(): 42 | return redirect('/admin') 43 | 44 | 45 | @app.route('/change_token', methods=['POST']) 46 | def change_token(): 47 | new_token = request.form.get('token') 48 | os.environ["BOT_TOKEN"] = new_token 49 | logger.info("Bot token was changed. Restarting the bot...") 50 | restart_bot() 51 | return make_response({'result': 'success'}, 200) 52 | 53 | 54 | @app.route('/get_current_bot_token', methods=['GET']) 55 | def get_current_token(): 56 | token = str(BOT_TOKEN) 57 | return make_response({'token': token}, 200) 58 | 59 | 60 | def stop_bot_process(): 61 | global telegram_bot 62 | telegram_bot.terminate() 63 | telegram_bot.kill() 64 | logger.info(f"Bot is off") 65 | 66 | 67 | @app.route('/stop_bot', methods=['POST']) 68 | def stop_bot(): 69 | stop_bot_process() 70 | return make_response({'result': 'success'}, 200) 71 | 72 | 73 | @app.route('/restart_bot', methods=['POST']) 74 | def restart_bot(): 75 | global telegram_bot 76 | stop_bot_process() 77 | telegram_bot = multiprocessing.Process(target=bot_init) 78 | telegram_bot.start() 79 | logger.info(f"Bot was launched") 80 | return make_response({'result': 'success'}, 200) 81 | 82 | 83 | def run_modules(): 84 | global telegram_bot, daemon 85 | jobs = [telegram_bot, daemon] 86 | 87 | telegram_bot.start() 88 | daemon.start() 89 | 90 | 91 | def init_admin_panel(): 92 | admin = Admin(app, name='Admin panel', template_mode='bootstrap3', 93 | index_view=HomeView(name='Home', menu_icon_type='glyph', menu_icon_value='glyphicon-home')) 94 | 95 | admin.add_view(ModelView(User, db.session, name='Users')) 96 | admin.add_view(ModelView(Order, db.session, name='Orders')) 97 | admin.add_view(ModelView(Product, db.session, name='Products')) 98 | admin.add_view(AdminModelView(db.session, category=Categories.MANAGEMENT)) 99 | admin.add_view(BotSettingsView(name='Bot', endpoint='bot_settings', category=Categories.MANAGEMENT)) 100 | admin.add_view(TextsModelView(Texts, db.session, category=Categories.MANAGEMENT)) 101 | 102 | 103 | def init_server(): 104 | db.create_all() 105 | db.session.commit() 106 | 107 | init_admin_panel() 108 | 109 | app.secret_key = 'secret' 110 | app.config['SESSION_TYPE'] = 'filesystem' 111 | app.register_blueprint(auth_blueprint) 112 | host = LOCAL_HOST if USE_LOCAL_VARIABLES else PRODUCTION_HOST 113 | 114 | login_manager = LoginManager() 115 | login_manager.login_view = 'auth.login' 116 | login_manager.init_app(app) 117 | 118 | @login_manager.user_loader 119 | def load_user(user_id): 120 | return AdminUser.query.get(int(user_id)) 121 | 122 | if AdminUser.query.count() == 0: 123 | AdminUser.register(ADMIN_EMAIL, 'admin', ADMIN_PASSWORD, is_super_admin=True) 124 | 125 | run_modules() 126 | app.run(host=host, port=PORT, use_reloader=False) # use_reloader=False to avoid conflict with multiprocessing 127 | 128 | 129 | if __name__ == '__main__': 130 | init_server() 131 | -------------------------------------------------------------------------------- /bot/states/SQLAlchemyStorage.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Union, Optional, AnyStr, Dict 3 | 4 | from aiogram.dispatcher.storage import BaseStorage 5 | 6 | from database import db 7 | from database.models import User 8 | 9 | 10 | class SQLAlchemyStorage(BaseStorage): 11 | 12 | async def close(self): 13 | pass 14 | 15 | async def wait_closed(self): 16 | pass 17 | 18 | @classmethod 19 | def check_address(cls, *, 20 | chat: Union[str, int, None] = None, 21 | user: Union[str, int, None] = None, 22 | ) -> (Union[str, int], Union[str, int]): 23 | """ 24 | In all storage's methods chat or user is always required. 25 | If one of them is not provided, you have to set missing value based on the provided one. 26 | This method performs the check described above. 27 | :param chat: chat_id 28 | :param user: user_id 29 | :return: 30 | """ 31 | if chat is None and user is None: 32 | raise ValueError('`user` or `chat` parameter is required but no one is provided!') 33 | 34 | if user is None: 35 | user = chat 36 | 37 | elif chat is None: 38 | chat = user 39 | 40 | return str(chat), str(user) 41 | 42 | async def set_state(self, *, 43 | chat: Union[str, int, None] = None, 44 | user: Union[str, int, None] = None, 45 | state: Optional[AnyStr] = None): 46 | chat_id, user_id = self.check_address(chat=chat, user=user) 47 | user = User.get(user_id=chat_id, chat_id=chat_id) 48 | 49 | if state is None: 50 | user.state = None 51 | else: 52 | user.state = self.resolve_state(state) 53 | 54 | db.session.commit() 55 | 56 | async def get_state(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, 57 | default: Optional[str] = None) -> Optional[str]: 58 | chat_id, user_id = self.check_address(chat=chat, user=user) 59 | user = User.get(user_id, chat_id) 60 | 61 | if user and user.state: 62 | return user.state 63 | 64 | return self.resolve_state(default) 65 | 66 | async def set_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, 67 | data: Dict = None): 68 | chat_id, user_id = self.check_address(chat=chat, user=user) 69 | user = User.get(user_id, chat_id) 70 | 71 | user.state_data = json.dumps(data) 72 | db.session.commit() 73 | 74 | async def get_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, 75 | default: Optional[dict] = None) -> Dict: 76 | chat_id, user_id = self.check_address(chat=chat, user=user) 77 | 78 | user = User.get(user_id, chat_id) 79 | 80 | if user.state_data: 81 | return json.loads(user.state_data) 82 | 83 | return default or {} 84 | 85 | async def update_data(self, *, chat: Union[str, int, None] = None, user: Union[str, int, None] = None, 86 | data: Dict = None, **kwargs): 87 | if data is None: 88 | data = {} 89 | temp_data = await self.get_data(chat=chat, user=user, default={}) 90 | temp_data.update(data, **kwargs) 91 | await self.set_data(chat=chat, user=user, data=temp_data) 92 | 93 | def has_bucket(self): 94 | return False 95 | 96 | async def get_bucket(self, *, 97 | chat: Union[str, int, None] = None, 98 | user: Union[str, int, None] = None, 99 | default: Optional[dict] = None) -> Dict: 100 | raise NotImplementedError 101 | 102 | async def set_bucket(self, *, 103 | chat: Union[str, int, None] = None, 104 | user: Union[str, int, None] = None, 105 | bucket: Dict = None): 106 | raise NotImplementedError 107 | 108 | async def update_bucket(self, *, 109 | chat: Union[str, int, None] = None, 110 | user: Union[str, int, None] = None, 111 | bucket: Dict = None, 112 | **kwargs): 113 | raise NotImplementedError 114 | 115 | async def reset_bucket(self, *, 116 | chat: Union[str, int, None] = None, 117 | user: Union[str, int, None] = None): 118 | await self.set_data(chat=chat, user=user, data={}) 119 | --------------------------------------------------------------------------------