├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bot ├── __init__.py ├── config.py ├── db.py ├── filters.py ├── handlers │ ├── __init__.py │ ├── commands.py │ ├── errors.py │ ├── inlines.py │ ├── messages.py │ ├── posts.py │ ├── queries.py │ └── schedules.py ├── keyboards.py ├── middlewares │ ├── __init__.py │ └── i18n.py ├── misc.py ├── models.py ├── states.py └── utils.py ├── locales ├── en │ └── LC_MESSAGES │ │ └── bot.po ├── ru │ └── LC_MESSAGES │ │ └── bot.po └── uk │ └── LC_MESSAGES │ └── bot.po ├── main.py └── requirements.txt /.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 | .hypothesis/ 51 | .pytest_cache/ 52 | 53 | # Translations 54 | *.mo 55 | *.pot 56 | 57 | # Django stuff: 58 | *.log 59 | local_settings.py 60 | db.sqlite3 61 | db.sqlite3-journal 62 | 63 | # Flask stuff: 64 | instance/ 65 | .webassets-cache 66 | 67 | # Scrapy stuff: 68 | .scrapy 69 | 70 | # Sphinx documentation 71 | docs/_build/ 72 | 73 | # PyBuilder 74 | target/ 75 | 76 | # Jupyter Notebook 77 | .ipynb_checkpoints 78 | 79 | # IPython 80 | profile_default/ 81 | ipython_config.py 82 | 83 | # pyenv 84 | .python-version 85 | 86 | # pipenv 87 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 88 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 89 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 90 | # install all needed dependencies. 91 | #Pipfile.lock 92 | 93 | # celery beat schedule file 94 | celerybeat-schedule 95 | 96 | # SageMath parsed files 97 | *.sage.py 98 | 99 | # Environments 100 | .env 101 | .venv 102 | env/ 103 | venv/ 104 | ENV/ 105 | env.bak/ 106 | venv.bak/ 107 | 108 | # Spyder project settings 109 | .spyderproject 110 | .spyproject 111 | 112 | # Rope project settings 113 | .ropeproject 114 | 115 | # mkdocs documentation 116 | /site 117 | 118 | # mypy 119 | .mypy_cache/ 120 | .dmypy.json 121 | dmypy.json 122 | 123 | # Pyre type checker 124 | .pyre/ 125 | 126 | # fastconf default files 127 | config.yml 128 | 129 | # VS Code 130 | .vscode/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Arwichok 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | init: 2 | python3 main.py init 3 | make compiletext 4 | 5 | gettext: 6 | pybabel extract bot/ -o locales/bot.pot 7 | 8 | createtexts: 9 | echo {en,ru,uk} | xargs -n1 pybabel init -i locales/bot.pot -d locales -D bot -l 10 | 11 | updatetext: 12 | pybabel update -d locales -D bot -i locales/bot.pot 13 | 14 | compiletext: 15 | pybabel compile -d locales -D bot 16 | 17 | update: 18 | make gettext 19 | make updatetext 20 | 21 | build: 22 | make compiletext -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Asyncbot `DEPRECATED` 2 | 3 | Example project structure for aiogram bot 4 | 5 | 6 | ### Before run 7 | pip install -r requirements.txt 8 | make init 9 | 10 | ### Edit config.yml 11 | TOKEN: 12 | ... 13 | 14 | ### Run bot 15 | python main.py 16 | 17 | 18 | Me on Telegram [@Arwichok](https://t.me/arwichok) 19 | -------------------------------------------------------------------------------- /bot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arwichok/asyncbot/371c9f92b3c07881b054a99c1aefbca29da06983/bot/__init__.py -------------------------------------------------------------------------------- /bot/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import ssl 4 | 5 | import aiohttp 6 | import fastconf 7 | 8 | # Bot 9 | SKIP_UPDATES = True 10 | BOT_TOKEN = '' # 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 11 | LOGFILE = '' # logs/bot.log 12 | OWNER_ID = 0 # your id for access to admin panel 13 | 14 | # Proxy 15 | PROXY_URL = '' # http or socks5://user:pass@host:port 16 | PROXY_LOGIN = '' 17 | PROXY_PASS = '' 18 | 19 | # Webhook 20 | APP_HOST = 'localhost' # 192.168.1.1XX, or localhost if use nginx 21 | APP_PORT = 3001 22 | USE_WEBHOOK = False 23 | WEBHOOK_HOST = '' # example.com or ip 24 | WEBHOOK_PATH = '' # /webhook 25 | WEBHOOK_PORT = 443 26 | SSL_CERT = '' # path to ssl certificate 27 | SSL_KEY = '' # path to ssl private key, hide if use nginx proxy_pass 28 | 29 | # Database 30 | DB_URL = "sqlite:///db.sqlite3" # db.sqlite3 31 | 32 | # Redis 33 | REDIS_SETTINGS = {} 34 | 35 | # Init config 36 | fastconf.config(__name__) 37 | if 'init' in sys.argv: 38 | sys.exit(0) 39 | 40 | ROOT_DIR = os.path.dirname(os.path.abspath(sys.modules['__main__'].__file__)) 41 | 42 | # Webhook init 43 | WEBHOOK_URL = f'https://{WEBHOOK_HOST}:{WEBHOOK_PORT}{WEBHOOK_PATH}' 44 | WEBHOOK_SERVER = { 45 | 'host': APP_HOST, 46 | 'port': APP_PORT, 47 | 'webhook_path': WEBHOOK_PATH, 48 | } 49 | 50 | # ssl context 51 | if SSL_CERT and SSL_KEY: 52 | context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) 53 | context.load_cert_chain(SSL_CERT, SSL_KEY) 54 | WEBHOOK_SERVER['ssl_context'] = context 55 | 56 | # i18n 57 | I18N_DOMAIN = 'bot' 58 | LOCALES_DIR = os.path.join(ROOT_DIR, 'locales') 59 | 60 | # proxy_auth 61 | PROXY_AUTH = aiohttp.BasicAuth(login=PROXY_LOGIN, password=PROXY_PASS) 62 | -------------------------------------------------------------------------------- /bot/db.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | 3 | import sqlalchemy as sa 4 | from aiogram import Dispatcher 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.orm import scoped_session, sessionmaker 7 | 8 | from bot.config import DB_URL 9 | from bot.misc import executor 10 | 11 | engine = sa.create_engine(DB_URL) 12 | session_factory = sessionmaker(bind=engine) 13 | Session = scoped_session(session_factory) 14 | 15 | 16 | @contextlib.contextmanager 17 | def db_session(): 18 | session = Session() 19 | try: 20 | yield session 21 | except Exception: 22 | session.rollback() 23 | finally: 24 | Session.remove() 25 | 26 | 27 | class Base(declarative_base()): 28 | __abstract__ = True 29 | 30 | @classmethod 31 | def get(cls, session, whereclause): 32 | return session.query(cls).filter(whereclause).first() 33 | 34 | 35 | async def on_startup(dp: Dispatcher): 36 | Base.metadata.create_all(engine) 37 | 38 | 39 | executor.on_startup(on_startup) 40 | -------------------------------------------------------------------------------- /bot/filters.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from aiogram import types 4 | from aiogram.dispatcher.filters import BoundFilter 5 | 6 | from bot.misc import dp 7 | 8 | 9 | @dataclass 10 | class IsDigit(BoundFilter): 11 | key = 'is_digit' 12 | is_digit: bool 13 | 14 | async def check(self, msg: types.Message): 15 | return msg.text.isdigit() 16 | 17 | 18 | dp.filters_factory.bind(IsDigit) 19 | -------------------------------------------------------------------------------- /bot/handlers/__init__.py: -------------------------------------------------------------------------------- 1 | from . import ( 2 | commands, 3 | errors, 4 | inlines, 5 | messages, 6 | posts, 7 | queries, 8 | schedules, 9 | ) 10 | -------------------------------------------------------------------------------- /bot/handlers/commands.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | from aiogram.dispatcher import FSMContext 3 | from aiogram.utils.markdown import hcode 4 | 5 | import bot.keyboards as kb 6 | from bot.middlewares.i18n import _ 7 | from bot.misc import dp 8 | 9 | 10 | @dp.message_handler(text_startswith='/start ') 11 | async def deep_link(msg: types.Message): 12 | deep_link = msg.text[7:] 13 | await msg.answer(_("Deep link {}").format(deep_link)) 14 | 15 | 16 | @dp.message_handler(commands=['start']) 17 | async def start(msg: types.Message): 18 | await msg.answer(_("Hello! I'm Asyncbot.")) 19 | 20 | 21 | @dp.message_handler(commands=['settings']) 22 | async def settings(msg: types.Message): 23 | await msg.answer( 24 | _("Settings"), reply_markup=kb.settings()) 25 | 26 | 27 | @dp.message_handler(commands=['help']) 28 | async def help(msg: types.Message): 29 | await msg.answer(_("Help")) 30 | 31 | 32 | @dp.message_handler(commands=['privacy']) 33 | async def privacy(msg: types.Message): 34 | await msg.answer(_("Privacy")) 35 | 36 | 37 | @dp.message_handler(commands=['id']) 38 | async def getid(msg: types.Message): 39 | await msg.answer(_("User id: {uid}\nChat id: {cid}").format( 40 | uid=hcode(msg.from_user.id), 41 | cid=hcode(msg.chat.id))) 42 | 43 | 44 | @dp.message_handler(commands=['cancel'], state='*') 45 | async def cancel(msg: types.Message, state: FSMContext): 46 | await state.finish() 47 | await msg.answer(_("Canceled"), reply_markup=types.ReplyKeyboardRemove()) 48 | -------------------------------------------------------------------------------- /bot/handlers/errors.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram.utils.exceptions import InvalidQueryID, MessageCantBeEdited 4 | 5 | from bot.misc import dp 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | @dp.errors_handler(exception=InvalidQueryID) 11 | async def except_InvalidQueryID(update, exception): 12 | log.error(exception) 13 | return True 14 | -------------------------------------------------------------------------------- /bot/handlers/inlines.py: -------------------------------------------------------------------------------- 1 | from aiogram import types 2 | 3 | from bot.misc import dp 4 | 5 | 6 | @dp.inline_handler() 7 | async def example_echo(iq: types.InlineQuery): 8 | await iq.answer(results=[], 9 | switch_pm_text='To bot', 10 | switch_pm_parameter='sp') 11 | -------------------------------------------------------------------------------- /bot/handlers/messages.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import types 4 | 5 | from bot.misc import dp 6 | 7 | log = logging.getLogger(__name__) 8 | 9 | 10 | @dp.message_handler(is_digit=True) 11 | async def is_digit_message(msg: types.Message): 12 | await msg.answer('This message is digit') 13 | 14 | 15 | @dp.message_handler() 16 | async def is_not_digit(msg: types.Message): 17 | await msg.answer('Hello') 18 | -------------------------------------------------------------------------------- /bot/handlers/posts.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Arwichok/asyncbot/371c9f92b3c07881b054a99c1aefbca29da06983/bot/handlers/posts.py -------------------------------------------------------------------------------- /bot/handlers/queries.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from aiogram import types 4 | from aiogram.utils.markdown import hbold, hcode 5 | 6 | import bot.keyboards as kb 7 | from bot.middlewares.i18n import _ 8 | from bot.misc import dp 9 | from bot.models import User, get_words 10 | from bot.utils import lang_cd, page_cd, settings_cd, word_cd 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | @dp.callback_query_handler(settings_cd.filter(set='set')) 16 | async def settings(cq: types.CallbackQuery): 17 | await cq.answer() 18 | await cq.message.edit_text( 19 | _('Settings'), reply_markup=kb.settings()) 20 | 21 | 22 | @dp.callback_query_handler(settings_cd.filter(set='lang')) 23 | async def show_lang(cq: types.CallbackQuery, locale: str): 24 | await cq.answer() 25 | await cq.message.edit_text( 26 | _('Choose language'), 27 | reply_markup=kb.lang(locale)) 28 | 29 | 30 | @dp.callback_query_handler(settings_cd.filter(set='admin')) 31 | async def show_admin_panel(cq: types.CallbackQuery): 32 | await cq.answer() 33 | users_count = await User.count() 34 | await cq.message.edit_text( 35 | hbold(_('Admin panel')) + _('\nUsers count: {uc}').format(uc=users_count), 36 | reply_markup=kb.admin()) 37 | 38 | 39 | @dp.callback_query_handler(lang_cd.filter()) 40 | async def choose_lang(cq: types.CallbackQuery, 41 | callback_data: dict, 42 | user: User): 43 | await cq.answer() 44 | lang = callback_data['lang'] 45 | lang = None if lang == '0' else lang 46 | await user.set_language(lang) 47 | if lang is None: 48 | lang = await _.get_user_locale('', (cq, {})) 49 | await cq.message.edit_text( 50 | _("Settings", locale=lang), 51 | reply_markup=kb.settings(locale=lang)) 52 | 53 | 54 | @dp.callback_query_handler(page_cd.filter()) 55 | async def pagination(cq: types.CallbackQuery, 56 | callback_data: dict): 57 | await cq.answer() 58 | data = callback_data['page'] 59 | if not data.isdigit(): 60 | return 61 | page = int(data) 62 | words, last = get_words(page, count=2) 63 | await cq.message.edit_text( 64 | f"Inline Pagination | {page}", 65 | reply_markup=kb.page(page, last, words) 66 | ) 67 | 68 | 69 | @dp.callback_query_handler(word_cd.filter()) 70 | async def word(cq: types.CallbackQuery, 71 | callback_data: dict): 72 | word = callback_data['word'] 73 | await cq.answer(word) -------------------------------------------------------------------------------- /bot/handlers/schedules.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from aiogram import Dispatcher 4 | 5 | from bot.config import OWNER_ID 6 | from bot.misc import bot, executor, aiosched 7 | 8 | 9 | # @aiosched.scheduled_job('interval', seconds=10) 10 | # async def hello(): 11 | # await bot.send_message(OWNER_ID, 'Hello') 12 | 13 | 14 | async def on_startup(dp: Dispatcher): 15 | aiosched.start() 16 | 17 | 18 | async def on_shutdown(dp: Dispatcher): 19 | aiosched.shutdown(wait=True) 20 | 21 | 22 | executor.on_startup(on_startup) 23 | executor.on_shutdown(on_shutdown) 24 | -------------------------------------------------------------------------------- /bot/keyboards.py: -------------------------------------------------------------------------------- 1 | from typing import List, Tuple 2 | 3 | from aiogram.types import InlineKeyboardButton, InlineKeyboardMarkup, User 4 | from babel import Locale 5 | 6 | from bot.middlewares.i18n import _ 7 | from bot.utils import lang_cd, page_cd, settings_cd, word_cd 8 | from bot.models import is_owner 9 | 10 | 11 | def settings(locale: str=None): 12 | kb = InlineKeyboardMarkup() 13 | kb.add(InlineKeyboardButton( 14 | _('Language', locale=locale), 15 | callback_data=settings_cd.new('lang'))) 16 | kb.add(InlineKeyboardButton( 17 | _('Pagination', locale=locale), 18 | callback_data=page_cd.new('0'))) 19 | if is_owner(): 20 | kb.add(InlineKeyboardButton( 21 | _('Admin panel', locale=locale), 22 | callback_data=settings_cd.new('admin'))) 23 | return kb 24 | 25 | 26 | def lang(locale: str): 27 | kb = InlineKeyboardMarkup() 28 | kb.add(InlineKeyboardButton( 29 | '[Auto]' if locale is None else 'Auto', 30 | callback_data=lang_cd.new('0'))) 31 | for i in _.available_locales: 32 | label = Locale(i).display_name.capitalize() 33 | kb.add(InlineKeyboardButton( 34 | f'[{label}]' if i == locale else label, 35 | callback_data=lang_cd.new(i))) 36 | return kb 37 | 38 | 39 | def page_btns(page: int=0, last: int=0) -> List[InlineKeyboardButton]: 40 | btns = [] 41 | def choosed(start, steps=3): 42 | for i in range(start, start+steps): 43 | btns.append((f'• {i} •', '_') if i == page else (str(i), str(i))) 44 | 45 | if last < 5: 46 | choosed(0, last+1) 47 | else: 48 | if page < 3: 49 | choosed(0) 50 | btns.extend([('3 ›', '3'), (f'{last} »', str(last))]) 51 | elif page > (last - 3): 52 | btns.extend([('« 0', '0'), (f'‹ {last-4}', str(last-4))]) 53 | choosed(last-2) 54 | else: 55 | btns.extend([ 56 | ('« 0', '0'), 57 | (f'‹ {page-1}', str(page-1)), 58 | (f'• {page} •', '_'), 59 | (f'{page+1} ›', str(page+1)), 60 | (f'{last} »', str(last)) 61 | ]) 62 | 63 | # if up to 5 [ 1 ][ 2 ][ 3 ][ 4 ][ 5 ] 64 | # if more 65 | # if page before 3 66 | # [ 1 ][ 2 ][ *3* ][ 4> ][ 9>> ] 67 | # if page after last-3 68 | # [ <<1 ][ <6 ][ *7* ][ 8 ][ 9 ] 69 | # else 70 | # [ <<1 ][ <4 ][ *5* ][ 6> ][ 9>> ] 71 | 72 | return [InlineKeyboardButton(l, callback_data=page_cd.new(d)) for l, d in btns] 73 | 74 | 75 | def page(page: int=0, last: int=0, blanks: List[str]=[]): 76 | kb = InlineKeyboardMarkup(row_width=5) 77 | for l in blanks: 78 | kb.add(InlineKeyboardButton(l, callback_data=word_cd.new(l))) 79 | # back = page - 1 if page > 0 else last 80 | # forward = page + 1 if page < last else 0 81 | # b = InlineKeyboardButton(str(back), callback_data=page_cd.new(str(back))) 82 | # f = InlineKeyboardButton(str(forward), callback_data=page_cd.new(str(forward))) 83 | # kb.add(b, f) 84 | kb.add(*page_btns(page, last)) 85 | s = InlineKeyboardButton('<< ⚙ Settings', callback_data=settings_cd.new('set')) 86 | return kb.add(s) 87 | 88 | 89 | def admin(): 90 | kb = InlineKeyboardMarkup() 91 | kb.add(InlineKeyboardButton( 92 | _('Settings'), callback_data=settings_cd.new('set'))) 93 | return kb 94 | -------------------------------------------------------------------------------- /bot/middlewares/__init__.py: -------------------------------------------------------------------------------- 1 | from . import i18n 2 | -------------------------------------------------------------------------------- /bot/middlewares/i18n.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Tuple 3 | 4 | from aiogram import types 5 | from aiogram.contrib.middlewares.i18n import I18nMiddleware 6 | 7 | from bot.config import I18N_DOMAIN, LOCALES_DIR 8 | from bot.misc import dp 9 | from bot.models import User 10 | 11 | 12 | log = logging.getLogger(__name__) 13 | 14 | 15 | class ACLMiddleware(I18nMiddleware): 16 | def get_tg_lang(self, tg_user: types.User) -> str: 17 | lang = tg_user.language_code 18 | if lang: 19 | lang = lang.split('-')[0] 20 | else: 21 | lang = 'en' 22 | return lang 23 | 24 | async def get_user_locale(self, action: str, args: Tuple[Any]): 25 | tg_user = types.User.get_current() 26 | *_, data = args 27 | if tg_user is None: 28 | data['locale'] = 'en' 29 | return 'en' 30 | is_new, user = await User.get_user(tg_user) 31 | args[0].conf['is_new_user'] = is_new 32 | data['locale'] = user.locale 33 | data['user'] = user 34 | lang = user.locale or self.get_tg_lang(tg_user) 35 | return lang 36 | 37 | 38 | _ = i18n = dp.middleware.setup(ACLMiddleware(I18N_DOMAIN, LOCALES_DIR)) 39 | -------------------------------------------------------------------------------- /bot/misc.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | 4 | from aiogram import Bot, Dispatcher 5 | from aiogram.contrib.fsm_storage.memory import MemoryStorage 6 | from aiogram.contrib.fsm_storage.redis import RedisStorage2 7 | from aiogram.utils.executor import Executor 8 | from apscheduler.schedulers.asyncio import AsyncIOScheduler 9 | 10 | from bot.config import (BOT_TOKEN, LOGFILE, PROXY_AUTH, PROXY_URL, 11 | REDIS_SETTINGS, SKIP_UPDATES) 12 | 13 | 14 | logging.basicConfig(level=logging.INFO, filename=LOGFILE) 15 | storage = RedisStorage2(**REDIS_SETTINGS) if REDIS_SETTINGS else MemoryStorage() 16 | loop = asyncio.get_event_loop() 17 | aiosched = AsyncIOScheduler() 18 | 19 | bot = Bot(token=BOT_TOKEN, parse_mode='HTML', proxy=PROXY_URL, proxy_auth=PROXY_AUTH) 20 | dp = Dispatcher(bot, storage=storage) 21 | executor = Executor(dp, skip_updates=SKIP_UPDATES) 22 | -------------------------------------------------------------------------------- /bot/models.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import math 3 | 4 | from aiogram import types 5 | 6 | from bot.db import Base, db_session, sa 7 | from bot.utils import aiowrap 8 | from bot.config import OWNER_ID 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | class User(Base): 14 | __tablename__ = 'users' 15 | id = sa.Column(sa.Integer, unique=True, nullable=False, primary_key=True) 16 | locale = sa.Column(sa.String(length=2), default=None) 17 | # TODO add is_admin, is_stoped 18 | 19 | @classmethod 20 | @aiowrap 21 | def get_user(cls, tg_user: types.User) -> (bool, 'User'): 22 | with db_session() as session: 23 | user = cls.get(session, cls.id == tg_user.id) 24 | is_new = False 25 | if user is None: 26 | user = cls(id=tg_user.id) 27 | session.add(user) 28 | session.commit() 29 | user = cls(id=user.id) 30 | is_new = True 31 | 32 | return is_new, user 33 | 34 | @aiowrap 35 | def set_language(self, language: str): 36 | with db_session() as session: 37 | user = User.get(session, User.id == self.id) 38 | user.locale = language 39 | session.commit() 40 | 41 | @classmethod 42 | @aiowrap 43 | def count(cls): 44 | with db_session() as session: 45 | return session.query(sa.func.count(cls.id)).scalar() 46 | 47 | 48 | WORDS = [ 49 | 'acoustics', 50 | 'purple', 51 | 'diligent', 52 | 'glib', 53 | 'living', 54 | 'vigorous', 55 | 'brief', 56 | 'time', 57 | 'bushes', 58 | 'nifty', 59 | 'bad', 60 | 'fresh', 61 | 'eatable', 62 | 'rice', 63 | 'brainy', 64 | 'like', 65 | 'thread', 66 | ] 67 | 68 | 69 | def get_words(page=0, count=5): 70 | start = page * count 71 | end = start + count 72 | words = WORDS[start:end] 73 | last_page = math.floor(len(WORDS) / count) 74 | return (words, last_page) 75 | 76 | 77 | def is_owner() -> bool: 78 | tg_user = types.User.get_current() 79 | return tg_user and tg_user.id == OWNER_ID 80 | 81 | # TODO 82 | # add is_admin() for knowing who can edit bot 83 | -------------------------------------------------------------------------------- /bot/states.py: -------------------------------------------------------------------------------- 1 | from aiogram.dispatcher.filters.state import State, StatesGroup 2 | -------------------------------------------------------------------------------- /bot/utils.py: -------------------------------------------------------------------------------- 1 | import contextvars 2 | import functools 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | from aiogram.utils.callback_data import CallbackData 6 | from aiogram import types 7 | 8 | from bot.misc import loop 9 | 10 | _executor = ThreadPoolExecutor() 11 | 12 | reaction_cd = CallbackData('rctn', 'r') 13 | settings_cd = CallbackData('settings', 'set') 14 | lang_cd = CallbackData('lang', 'lang') 15 | page_cd = CallbackData('page', 'page') 16 | word_cd = CallbackData('word', 'word') 17 | 18 | 19 | def aiowrap(func): 20 | """ 21 | Wrapping for use sync func in async code 22 | """ 23 | @functools.wraps(func) 24 | def wrapping(*args, **kwargs): 25 | new_func = functools.partial(func, *args, **kwargs) 26 | ctx = contextvars.copy_context() 27 | ctx_func = functools.partial(ctx.run, new_func) 28 | return loop.run_in_executor(_executor, ctx_func) 29 | return wrapping 30 | 31 | 32 | # class Blank: 33 | # def __init__(self, text, url=None, cd=None): 34 | # self.text = text 35 | # self.url = url 36 | # self.cd = cd 37 | 38 | # def button(self): 39 | # return -------------------------------------------------------------------------------- /locales/en/LC_MESSAGES/bot.po: -------------------------------------------------------------------------------- 1 | # English translations for PROJECT. 2 | # Copyright (C) 2019 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2019. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2019-07-29 05:15+0300\n" 11 | "PO-Revision-Date: 2019-07-18 12:23+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: en\n" 14 | "Language-Team: en \n" 15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=utf-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Generated-By: Babel 2.6.0\n" 20 | 21 | #: bot/keyboards.py:11 22 | msgid "Language" 23 | msgstr "" 24 | 25 | #: bot/keyboards.py:14 26 | msgid "Pagination" 27 | msgstr "" 28 | 29 | #: bot/handlers/commands.py:20 30 | msgid "Hello! I am asyncbot." 31 | 32 | #: bot/handlers/queries.py:32 bot/keyboards.py:17 33 | msgid "Admin panel" 34 | msgstr "" 35 | 36 | #: bot/handlers/commands.py:24 bot/handlers/queries.py:18 37 | #: bot/handlers/queries.py:45 bot/keyboards.py:49 38 | msgid "Settings" 39 | msgstr "" 40 | 41 | #: bot/handlers/commands.py:13 42 | msgid "Deep link {}" 43 | msgstr "" 44 | 45 | #: bot/handlers/commands.py:18 46 | msgid "Hello! I'm Asyncbot." 47 | msgstr "" 48 | 49 | #: bot/handlers/commands.py:29 50 | msgid "Help" 51 | msgstr "" 52 | 53 | #: bot/handlers/commands.py:34 54 | msgid "Privacy" 55 | msgstr "" 56 | 57 | #: bot/handlers/commands.py:39 58 | msgid "" 59 | "User id: {uid}\n" 60 | "Chat id: {cid}" 61 | msgstr "" 62 | 63 | #: bot/handlers/commands.py:47 64 | msgid "Canceled" 65 | msgstr "" 66 | 67 | #: bot/handlers/queries.py:24 68 | msgid "Choose language" 69 | msgstr "" 70 | 71 | #: bot/handlers/queries.py:32 72 | msgid "" 73 | "\n" 74 | "Users count: {uc}" 75 | msgstr "" 76 | 77 | #: bot/handlers/queries.py:55 78 | msgid "Page {page}/{count}:\n" 79 | msgstr "" 80 | 81 | #~ msgid "Admin panel\n" 82 | #~ msgstr "" 83 | 84 | #~ msgid "Users count: {uc}" 85 | #~ msgstr "" 86 | 87 | -------------------------------------------------------------------------------- /locales/ru/LC_MESSAGES/bot.po: -------------------------------------------------------------------------------- 1 | # Russian translations for PROJECT. 2 | # Copyright (C) 2019 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2019. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2019-07-29 05:15+0300\n" 11 | "PO-Revision-Date: 2019-07-18 12:23+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: ru\n" 14 | "Language-Team: ru \n" 15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 16 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=utf-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Generated-By: Babel 2.6.0\n" 21 | 22 | #: bot/keyboards.py:11 23 | msgid "Language" 24 | msgstr "Язык" 25 | 26 | #: bot/keyboards.py:14 27 | msgid "Pagination" 28 | msgstr "Страницы" 29 | 30 | #: bot/handlers/queries.py:32 bot/keyboards.py:17 31 | msgid "Admin panel" 32 | msgstr "Админка" 33 | 34 | #: bot/handlers/commands.py:24 bot/handlers/queries.py:18 35 | #: bot/handlers/queries.py:45 bot/keyboards.py:49 36 | msgid "Settings" 37 | msgstr "Настройки" 38 | 39 | #: bot/handlers/commands.py:13 40 | msgid "Deep link {}" 41 | msgstr "" 42 | 43 | #: bot/handlers/commands.py:18 44 | msgid "Hello! I'm Asyncbot." 45 | msgstr "Привет! Я Asyncbot." 46 | 47 | #: bot/handlers/commands.py:29 48 | msgid "Help" 49 | msgstr "Помощь" 50 | 51 | #: bot/handlers/commands.py:34 52 | msgid "Privacy" 53 | msgstr "Приватность" 54 | 55 | #: bot/handlers/commands.py:39 56 | msgid "" 57 | "User id: {uid}\n" 58 | "Chat id: {cid}" 59 | msgstr "" 60 | "ID пользователя: {uid}\n" 61 | "ID чата: {cid}" 62 | 63 | #: bot/handlers/commands.py:47 64 | msgid "Canceled" 65 | msgstr "Отменено" 66 | 67 | #: bot/handlers/queries.py:24 68 | msgid "Choose language" 69 | msgstr "Выберите язык" 70 | 71 | #: bot/handlers/queries.py:32 72 | msgid "" 73 | "\n" 74 | "Users count: {uc}" 75 | msgstr "" 76 | "\n" 77 | "Пользователей: {uc}" 78 | 79 | #: bot/handlers/queries.py:55 80 | msgid "Page {page}/{count}:\n" 81 | msgstr "Страница {page}/{count}:\n" 82 | -------------------------------------------------------------------------------- /locales/uk/LC_MESSAGES/bot.po: -------------------------------------------------------------------------------- 1 | # Ukrainian translations for PROJECT. 2 | # Copyright (C) 2019 ORGANIZATION 3 | # This file is distributed under the same license as the PROJECT project. 4 | # FIRST AUTHOR , 2019. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: PROJECT VERSION\n" 9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" 10 | "POT-Creation-Date: 2019-07-29 05:15+0300\n" 11 | "PO-Revision-Date: 2019-07-18 12:23+0300\n" 12 | "Last-Translator: FULL NAME \n" 13 | "Language: uk\n" 14 | "Language-Team: uk \n" 15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " 16 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" 17 | "MIME-Version: 1.0\n" 18 | "Content-Type: text/plain; charset=utf-8\n" 19 | "Content-Transfer-Encoding: 8bit\n" 20 | "Generated-By: Babel 2.6.0\n" 21 | 22 | #: bot/keyboards.py:11 23 | msgid "Language" 24 | msgstr "Мова" 25 | 26 | #: bot/keyboards.py:14 27 | msgid "Pagination" 28 | msgstr "Пагінація" 29 | 30 | #: bot/handlers/queries.py:32 bot/keyboards.py:17 31 | msgid "Admin panel" 32 | msgstr "Адмінка" 33 | 34 | #: bot/handlers/commands.py:24 bot/handlers/queries.py:18 35 | #: bot/handlers/queries.py:45 bot/keyboards.py:49 36 | msgid "Settings" 37 | msgstr "Налаштування" 38 | 39 | #: bot/handlers/commands.py:13 40 | msgid "Deep link {}" 41 | msgstr "" 42 | 43 | #: bot/handlers/commands.py:18 44 | msgid "Hello! I'm Asyncbot." 45 | msgstr "Привіт! Я Asyncbot." 46 | 47 | #: bot/handlers/commands.py:29 48 | msgid "Help" 49 | msgstr "Допомога" 50 | 51 | #: bot/handlers/commands.py:34 52 | msgid "Privacy" 53 | msgstr "Приватність" 54 | 55 | #: bot/handlers/commands.py:39 56 | msgid "" 57 | "User id: {uid}\n" 58 | "Chat id: {cid}" 59 | msgstr "" 60 | "ID користувача: {uid}\n" 61 | "ID чату: {cid}" 62 | 63 | #: bot/handlers/commands.py:47 64 | msgid "Canceled" 65 | msgstr "Відмінено" 66 | 67 | #: bot/handlers/queries.py:24 68 | msgid "Choose language" 69 | msgstr "Виберіть мову" 70 | 71 | #: bot/handlers/queries.py:32 72 | msgid "" 73 | "\n" 74 | "Users count: {uc}" 75 | msgstr "" 76 | "\n" 77 | "Користувачів" 78 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from aiogram import Dispatcher 4 | 5 | from bot import db, filters, handlers, middlewares 6 | from bot.config import USE_WEBHOOK, WEBHOOK_SERVER, WEBHOOK_URL, SSL_CERT 7 | from bot.misc import executor 8 | 9 | 10 | async def on_startup_polling(dp: Dispatcher): 11 | await dp.bot.delete_webhook() 12 | 13 | 14 | async def on_startup_webhook(dp: Dispatcher): 15 | cert = open(SSL_CERT, 'rb') if SSL_CERT else None 16 | await dp.bot.set_webhook(WEBHOOK_URL, certificate=cert) 17 | 18 | 19 | async def on_shutdown_webhook(dp: Dispatcher): 20 | await dp.bot.delete_webhook() 21 | 22 | 23 | def main(): 24 | executor.on_startup(on_startup_polling, webhook=0) 25 | executor.on_startup(on_startup_webhook, polling=0) 26 | executor.on_shutdown(on_shutdown_webhook, polling=0) 27 | 28 | if USE_WEBHOOK: 29 | executor.start_webhook(**WEBHOOK_SERVER) 30 | else: 31 | executor.start_polling() 32 | 33 | 34 | if __name__ == '__main__': 35 | main() 36 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram>=2.2,<3 2 | APScheduler>=3.6.0,<4 3 | fastconf 4 | SQLAlchemy>=1.3.1,<2 5 | aioredis --------------------------------------------------------------------------------