refactor for new aiogram

This commit is contained in:
Aleksei Krugliak 2024-01-13 13:41:40 -03:00
parent 4c334e0e32
commit 49942b83ed
38 changed files with 105 additions and 388 deletions

0
.gitignore vendored Normal file → Executable file
View File

8
Dockerfile Normal file → Executable file
View File

@ -1,16 +1,16 @@
FROM python:3.8 AS builder
FROM python:3.12 AS builder
COPY requirements.txt .
# install dependencies to the local user directory (eg. /root/.local)
RUN pip install --user -r requirements.txt
RUN pip3 install --user -r requirements.txt
# second stage
FROM python:3.8-slim
FROM python:3.12-slim
WORKDIR /code
# copy only the dependencies that are needed for our application and the source files
COPY --from=builder /root/.local /root/.local
COPY ./src .
COPY ./source .
# update PATH
ENV PATH=/root/.local:$PATH

12
README.md Normal file → Executable file
View File

@ -1,3 +1,13 @@
# expense-bot
## Local run
```bash
python3 -m venv venv
source venv/bin/activate
pip3 install -r requirements.txt
```
# [WIP]
# Build Image
@ -14,4 +24,4 @@ $ podman push [hash]|[localhost/expense-bot:0.1.0] docker://quay.io/ksemele/expe
$ podman login quay.io
$ podman pull quay.io/ksemele/expense-bot
$ podman pull quay.io/ksemele/expense-bot:0.1.0
```
```

2
requirements.txt Normal file → Executable file
View File

@ -1,3 +1,3 @@
aiogram<3.0
aiogram~=3.3.0
environs==9.5.0
requests~=2.27.1

55
source/app.py Executable file
View File

@ -0,0 +1,55 @@
from loader import *
@dp.message(CommandStart())
async def command_start_handler(message: Message) -> None:
"""
This handler receives messages with `/start` command
"""
# Most event objects have aliases for API methods that can be called in events' context
# For example if you want to answer to incoming message you can use `message.answer(...)` alias
# and the target chat will be passed to :ref:`aiogram.methods.send_message.SendMessage`
# method automatically or call API method directly via
# Bot instance: `bot.send_message(chat_id=message.chat.id, ...)`
await message.answer(f"Hello, {hbold(message.from_user.full_name)}!")
# @dp.message()
# async def echo_handler(message: types.Message) -> None:
# """
# Handler will forward receive a message back to the sender
# By default, message handler will handle all message types (like a text, photo, sticker etc.)
# """
# try:
# # Send a copy of the received message
# await message.send_copy(chat_id=message.chat.id)
# except TypeError:
# # But not all the types is supported to be copied so need to handle it
# await message.answer("Nice try!")
@dp.message(Command("currencies"))
async def get_currencies(message: types.Message):
url = "https://nbg.gov.ge/gw/api/ct/monetarypolicy/currencies/en/json/"
date = datetime.date.today().isoformat()
payload = {"currencies": ["USD", "EUR"], "date": date}
r = requests.get(url, params=payload)
# print(r.text)
usd = json.loads(r.text)
for elem in usd:
# print(elem['currencies'])
for each in elem["currencies"]:
# print(each)
# print(each['code'] + " " + each['rateFormated'])
await message.answer(each["code"] + " " + each["rateFormated"])
# print(r.status_code)
async def main() -> None:
await dp.start_polling(bot)
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
asyncio.run(main())

0
src/data/__init__.py → source/data/__init__.py Normal file → Executable file
View File

8
source/data/config.py Executable file
View File

@ -0,0 +1,8 @@
from environs import Env
env = Env()
env.read_env()
BOT_TOKEN = env.str("BOT_TOKEN")
ADMINS = env.list("ADMINS")
IP = env.str("HOST_IP")

26
source/loader.py Executable file
View File

@ -0,0 +1,26 @@
import asyncio
import logging
import sys
import json
import requests
import datetime
from os import getenv
from aiogram import Bot, Dispatcher, Router, types
from aiogram.enums import ParseMode
from aiogram.filters import CommandStart
from aiogram.types import Message
from aiogram.utils.markdown import hbold
from aiogram.filters import (
Command,
CommandObject,
ExceptionMessageFilter,
ExceptionTypeFilter,
)
# envs from .env file
from data import config
# Initialize Bot instance with a default parse mode which will be passed to all API calls
# And the run events dispatching
bot = Bot(token=config.BOT_TOKEN, parse_mode=ParseMode.HTML)
dp = Dispatcher()

View File

@ -1,19 +0,0 @@
from aiogram import executor
from loader import dp
import middlewares, filters, handlers
from utils.notify_admins import on_startup_notify
from utils.set_bot_commands import set_default_commands
async def on_startup(dispatcher):
# Устанавливаем дефолтные команды
await set_default_commands(dispatcher)
# Уведомляет про запуск
await on_startup_notify(dispatcher)
if __name__ == '__main__':
executor.start_polling(dp, on_startup=on_startup)

View File

@ -1,9 +0,0 @@
from environs import Env
# Теперь используем вместо библиотеки python-dotenv библиотеку environs
env = Env()
env.read_env()
BOT_TOKEN = env.str("BOT_TOKEN") # Забираем значение типа str
ADMINS = env.list("ADMINS") # Тут у нас будет список из админов
IP = env.str("ip") # Тоже str, но для айпи адреса хоста

View File

@ -1,9 +0,0 @@
from aiogram import Dispatcher
from loader import dp
# from .is_admin import AdminFilter
if __name__ == "filters":
# dp.filters_factory.bind(AdminFilter)
pass

View File

@ -1,5 +0,0 @@
from . import errors
from . import users
from . import groups
from . import channels

View File

@ -1 +0,0 @@
from . import error_handler

View File

@ -1,36 +0,0 @@
import logging
from aiogram.utils.exceptions import (TelegramAPIError,
MessageNotModified,
CantParseEntities)
from loader import dp
@dp.errors_handler()
async def errors_handler(update, exception):
"""
Exceptions handler. Catches all exceptions within task factory tasks.
:param dispatcher:
:param update:
:param exception:
:return: stdout logging
"""
if isinstance(exception, MessageNotModified):
logging.exception('Message is not modified')
# do something here?
return True
if isinstance(exception, CantParseEntities):
# or here
logging.exception(f'CantParseEntities: {exception} \nUpdate: {update}')
return True
# MUST BE THE LAST CONDITION (ЭТО УСЛОВИЕ ВСЕГДА ДОЛЖНО БЫТЬ В КОНЦЕ)
if isinstance(exception, TelegramAPIError):
logging.exception(f'TelegramAPIError: {exception} \nUpdate: {update}')
return True
# At least you have tried.
logging.exception(f'Update: {update} \n{exception}')

View File

@ -1,7 +0,0 @@
from . import help
from . import start
from . import echo
from . import add
from . import settings
from . import test
from . import currencies

View File

@ -1,25 +0,0 @@
from aiogram import types
from loader import dp
import sqlite3 as sl
@dp.message_handler(commands="add")
async def bot_add(message: types.Message):
await message.answer(f"This is ADD command, {message.from_user.username}!")
con = sl.connect('test.db')
# @dp.message_handler(state=None) # todo сделать стейты. сейчас это хэндлит все сообщения в любых состояниях!
# async def bot_add_parse_message(message: types.Message):
# words = message.text.split()
# # Uncomment to see debug in console
# # print('Split message:')
# # i = 0
# # for each in words:
# # print('[' + str(i) + ']: ' + '[' + each + ']')
# # i += 1
# # print('[end]')
#
# if len(words) == 2:
# await message.answer('[added] ' + words[0] + ' ' + words[1])
# else:
# await message.answer("can't parse. wrong arguments!")

View File

@ -1,22 +0,0 @@
from aiogram import types
from loader import dp
import json
@dp.message_handler(commands="currencies")
async def get_currencies(message: types.Message):
import requests
import datetime
url = 'https://nbg.gov.ge/gw/api/ct/monetarypolicy/currencies/en/json/'
date = datetime.date.today().isoformat()
payload = {'currencies': ['USD', 'EUR'], 'date': date}
r = requests.get(url, params=payload)
print(r.text)
usd = json.loads(r.text)
for elem in usd:
# print(elem['currencies'])
for each in elem['currencies']:
# print(each)
# print(each['code'] + " " + each['rateFormated'])
await message.answer(each['code'] + " " + each['rateFormated'])
# print(r.status_code)

View File

@ -1,21 +0,0 @@
from aiogram import types
from aiogram.dispatcher import FSMContext
# from loader import dp
#
#
# # Эхо хендлер, куда летят текстовые сообщения без указанного состояния
# @dp.message_handler(state=None)
# async def bot_echo(message: types.Message):
# await message.answer(f"Эхо без состояния."
# f"Сообщение:\n"
# f"{message.text}")
#
#
# # Эхо хендлер, куда летят ВСЕ сообщения с указанным состоянием
# @dp.message_handler(state="*", content_types=types.ContentTypes.ANY)
# async def bot_echo_all(message: types.Message, state: FSMContext):
# state = await state.get_state()
# await message.answer(f"Эхо в состоянии <code>{state}</code>.\n"
# f"\nСодержание сообщения:\n"
# f"<code>{message}</code>")

View File

@ -1,20 +0,0 @@
from aiogram import types
from aiogram.dispatcher.filters.builtin import CommandHelp
from loader import dp
@dp.message_handler(CommandHelp())
async def bot_help(message: types.Message):
text = ("Available commands: ",
"/start - Launch bot.",
"/help - Commands list, little help.",
"/add - Add expense.",
"/edit - Edit expense.",
"/list - See my expenses.",
"/settings - Configure bot.",
"/currencies - Get Official Georgian course",
"/test - Test command."
)
await message.answer("\n".join(text))

View File

@ -1,9 +0,0 @@
from aiogram import types
from aiogram.dispatcher.filters.builtin import CommandSettings
from loader import dp
@dp.message_handler(CommandSettings())
async def bot_settings(message: types.Message):
await message.answer(f"Settings being here...")

View File

@ -1,9 +0,0 @@
from aiogram import types
from aiogram.dispatcher.filters.builtin import CommandStart
from loader import dp
@dp.message_handler(CommandStart())
async def bot_start(message: types.Message):
await message.answer(f"Hello, {message.from_user.username}!")

View File

@ -1,76 +0,0 @@
from aiogram import types
from loader import dp
@dp.message_handler(commands="test")
async def cmd_random(message: types.Message):
keyboard = types.InlineKeyboardMarkup()
keyboard.add(types.InlineKeyboardButton(text="Нажми меня", callback_data="random_value"))
keyboard.add(types.InlineKeyboardButton(text="Нажми меня 2", callback_data="random_value2"))
await message.answer("test reply 1-2-3", reply_markup=keyboard)
@dp.callback_query_handler(text="random_value")
async def send_random_value(call: types.CallbackQuery):
await call.message.answer('test reply 1')
# shows alert with 'Ok' button
await call.answer(text="Спасибо, что воспользовались ботом!", show_alert=True)
# или просто await call.answer()
@dp.callback_query_handler(text="random_value2")
async def send_random_value(call: types.CallbackQuery):
await call.message.answer('test reply 2')
await call.answer()
########################################################################################
from aiogram.utils.callback_data import CallbackData
from aiogram.utils.exceptions import MessageNotModified
from contextlib import suppress
# fabnum - префикс, action - название аргумента, которым будем передавать значение
callback_numbers = CallbackData("fabnum", "action")
# Здесь хранятся пользовательские данные.
# Т.к. это словарь в памяти, то при перезапуске он очистится
user_data = {}
def get_keyboard_fab():
buttons = [
types.InlineKeyboardButton(text="-1", callback_data=callback_numbers.new(action="decr")),
types.InlineKeyboardButton(text="+1", callback_data=callback_numbers.new(action="incr")),
types.InlineKeyboardButton(text="Подтвердить", callback_data=callback_numbers.new(action="finish"))
]
keyboard = types.InlineKeyboardMarkup(row_width=2)
keyboard.add(*buttons)
return keyboard
async def update_num_text_fab(message: types.Message, new_value: int):
with suppress(MessageNotModified):
await message.edit_text(f"Укажите число: {new_value}", reply_markup=get_keyboard_fab())
@dp.message_handler(commands="numbers_fab")
async def cmd_numbers(message: types.Message):
user_data[message.from_user.id] = 0
await message.answer("Укажите число: 0", reply_markup=get_keyboard_fab())
@dp.callback_query_handler(callback_numbers.filter(action=["incr", "decr"]))
async def callbacks_num_change_fab(call: types.CallbackQuery, callback_data: dict):
user_value = user_data.get(call.from_user.id, 0)
action = callback_data["action"]
if action == "incr":
user_data[call.from_user.id] = user_value + 1
await update_num_text_fab(call.message, user_value + 1)
elif action == "decr":
user_data[call.from_user.id] = user_value - 1
await update_num_text_fab(call.message, user_value - 1)
await call.answer()
@dp.callback_query_handler(callback_numbers.filter(action=["finish"]))
async def callbacks_num_finish_fab(call: types.CallbackQuery):
user_value = user_data.get(call.from_user.id, 0)
await call.message.edit_text(f"Итого: {user_value}")
await call.answer()

View File

@ -1,2 +0,0 @@
from . import default
from . import inline

View File

@ -1 +0,0 @@

View File

@ -1,8 +0,0 @@
from aiogram import Bot, Dispatcher, types
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from data import config
bot = Bot(token=config.BOT_TOKEN, parse_mode=types.ParseMode.HTML)
storage = MemoryStorage()
dp = Dispatcher(bot, storage=storage)

View File

@ -1,8 +0,0 @@
from aiogram import Dispatcher
from loader import dp
from .throttling import ThrottlingMiddleware
if __name__ == "middlewares":
dp.middleware.setup(ThrottlingMiddleware())

View File

@ -1,37 +0,0 @@
import asyncio
from aiogram import types, Dispatcher
from aiogram.dispatcher import DEFAULT_RATE_LIMIT
from aiogram.dispatcher.handler import CancelHandler, current_handler
from aiogram.dispatcher.middlewares import BaseMiddleware
from aiogram.utils.exceptions import Throttled
class ThrottlingMiddleware(BaseMiddleware):
"""
Simple middleware
"""
def __init__(self, limit=DEFAULT_RATE_LIMIT, key_prefix='antiflood_'):
self.rate_limit = limit
self.prefix = key_prefix
super(ThrottlingMiddleware, self).__init__()
async def on_process_message(self, message: types.Message, data: dict):
handler = current_handler.get()
dispatcher = Dispatcher.get_current()
if handler:
limit = getattr(handler, "throttling_rate_limit", self.rate_limit)
key = getattr(handler, "throttling_key", f"{self.prefix}_{handler.__name__}")
else:
limit = self.rate_limit
key = f"{self.prefix}_message"
try:
await dispatcher.throttle(key, rate=limit)
except Throttled as t:
await self.message_throttled(message, t)
raise CancelHandler()
async def message_throttled(self, message: types.Message, throttled: Throttled):
if throttled.exceeded_count <= 2:
await message.reply("Too many requests!")

View File

View File

@ -1,3 +0,0 @@
from . import db_api
from . import misc
from .notify_admins import on_startup_notify

View File

@ -1,2 +0,0 @@
from .throttling import rate_limit
from . import logging

View File

@ -1,6 +0,0 @@
import logging
logging.basicConfig(format=u'%(filename)s [LINE:%(lineno)d] #%(levelname)-8s [%(asctime)s] %(message)s',
level=logging.INFO,
# level=logging.DEBUG, # Можно заменить на другой уровень логгирования.
)

View File

@ -1,16 +0,0 @@
def rate_limit(limit: int, key=None):
"""
Decorator for configuring rate limit and key in different functions.
:param limit:
:param key:
:return:
"""
def decorator(func):
setattr(func, 'throttling_rate_limit', limit)
if key:
setattr(func, 'throttling_key', key)
return func
return decorator

View File

@ -1,14 +0,0 @@
import logging
from aiogram import Dispatcher
from data.config import ADMINS
async def on_startup_notify(dp: Dispatcher):
for admin in ADMINS:
try:
await dp.bot.send_message(admin, "Voodoo21-expense started")
except Exception as err:
logging.exception(err)

View File

@ -1,17 +0,0 @@
from aiogram import types
async def set_default_commands(dp):
await dp.bot.set_my_commands(
[
types.BotCommand("start", "Launch bot."),
types.BotCommand("help", "Commands list, little help."),
types.BotCommand("add", "Add expense."),
types.BotCommand("edit", "Edit expense."),
types.BotCommand("list", "See my expenses."),
types.BotCommand("settings", "Configure bot."),
types.BotCommand("currencies", "USD\EUR - GEL."),
types.BotCommand("test", "Test command."),
# types.BotCommand("settings3", "Configure bot."),
]
)