diff --git a/.gitignore b/.gitignore old mode 100644 new mode 100755 diff --git a/Dockerfile b/Dockerfile old mode 100644 new mode 100755 index 4654adf..49af7b3 --- a/Dockerfile +++ b/Dockerfile @@ -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 diff --git a/README.md b/README.md old mode 100644 new mode 100755 index 3cbb1ba..0234f77 --- a/README.md +++ b/README.md @@ -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 -``` \ No newline at end of file +``` diff --git a/requirements.txt b/requirements.txt old mode 100644 new mode 100755 index 2822215..3f43ec5 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -aiogram<3.0 +aiogram~=3.3.0 environs==9.5.0 requests~=2.27.1 \ No newline at end of file diff --git a/source/app.py b/source/app.py new file mode 100755 index 0000000..634383d --- /dev/null +++ b/source/app.py @@ -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()) diff --git a/src/data/__init__.py b/source/data/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from src/data/__init__.py rename to source/data/__init__.py diff --git a/source/data/config.py b/source/data/config.py new file mode 100755 index 0000000..42f46ce --- /dev/null +++ b/source/data/config.py @@ -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") diff --git a/source/loader.py b/source/loader.py new file mode 100755 index 0000000..eca09bc --- /dev/null +++ b/source/loader.py @@ -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() diff --git a/src/app.py b/src/app.py deleted file mode 100644 index b080cca..0000000 --- a/src/app.py +++ /dev/null @@ -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) - diff --git a/src/data/config.py b/src/data/config.py deleted file mode 100644 index e216714..0000000 --- a/src/data/config.py +++ /dev/null @@ -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, но для айпи адреса хоста diff --git a/src/filters/__init__.py b/src/filters/__init__.py deleted file mode 100644 index 40de5d0..0000000 --- a/src/filters/__init__.py +++ /dev/null @@ -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 diff --git a/src/handlers/__init__.py b/src/handlers/__init__.py deleted file mode 100644 index ed49383..0000000 --- a/src/handlers/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from . import errors -from . import users -from . import groups -from . import channels - diff --git a/src/handlers/channels/__init__.py b/src/handlers/channels/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/handlers/errors/__init__.py b/src/handlers/errors/__init__.py deleted file mode 100644 index 96f9c99..0000000 --- a/src/handlers/errors/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from . import error_handler diff --git a/src/handlers/errors/error_handler.py b/src/handlers/errors/error_handler.py deleted file mode 100644 index d1b6aa1..0000000 --- a/src/handlers/errors/error_handler.py +++ /dev/null @@ -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}') diff --git a/src/handlers/groups/__init__.py b/src/handlers/groups/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/handlers/users/__init__.py b/src/handlers/users/__init__.py deleted file mode 100644 index d0f292a..0000000 --- a/src/handlers/users/__init__.py +++ /dev/null @@ -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 diff --git a/src/handlers/users/add.py b/src/handlers/users/add.py deleted file mode 100644 index 0d944c4..0000000 --- a/src/handlers/users/add.py +++ /dev/null @@ -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!") diff --git a/src/handlers/users/currencies.py b/src/handlers/users/currencies.py deleted file mode 100644 index ac119d0..0000000 --- a/src/handlers/users/currencies.py +++ /dev/null @@ -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) diff --git a/src/handlers/users/echo.py b/src/handlers/users/echo.py deleted file mode 100644 index 783d559..0000000 --- a/src/handlers/users/echo.py +++ /dev/null @@ -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"Эхо в состоянии {state}.\n" -# f"\nСодержание сообщения:\n" -# f"{message}") diff --git a/src/handlers/users/help.py b/src/handlers/users/help.py deleted file mode 100644 index c103dee..0000000 --- a/src/handlers/users/help.py +++ /dev/null @@ -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)) diff --git a/src/handlers/users/settings.py b/src/handlers/users/settings.py deleted file mode 100644 index c01ef78..0000000 --- a/src/handlers/users/settings.py +++ /dev/null @@ -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...") diff --git a/src/handlers/users/start.py b/src/handlers/users/start.py deleted file mode 100644 index ec529e7..0000000 --- a/src/handlers/users/start.py +++ /dev/null @@ -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}!") diff --git a/src/handlers/users/test.py b/src/handlers/users/test.py deleted file mode 100644 index cc6f0e2..0000000 --- a/src/handlers/users/test.py +++ /dev/null @@ -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() diff --git a/src/keyboards/__init__.py b/src/keyboards/__init__.py deleted file mode 100644 index 8abaca9..0000000 --- a/src/keyboards/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from . import default -from . import inline diff --git a/src/keyboards/default/__init__.py b/src/keyboards/default/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/keyboards/inline/__init__.py b/src/keyboards/inline/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/src/keyboards/inline/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/loader.py b/src/loader.py deleted file mode 100644 index ba42d33..0000000 --- a/src/loader.py +++ /dev/null @@ -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) diff --git a/src/middlewares/__init__.py b/src/middlewares/__init__.py deleted file mode 100644 index 0631e10..0000000 --- a/src/middlewares/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -from aiogram import Dispatcher - -from loader import dp -from .throttling import ThrottlingMiddleware - - -if __name__ == "middlewares": - dp.middleware.setup(ThrottlingMiddleware()) diff --git a/src/middlewares/throttling.py b/src/middlewares/throttling.py deleted file mode 100644 index 4037c01..0000000 --- a/src/middlewares/throttling.py +++ /dev/null @@ -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!") diff --git a/src/states/__init__.py b/src/states/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/__init__.py b/src/utils/__init__.py deleted file mode 100644 index 47874ab..0000000 --- a/src/utils/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import db_api -from . import misc -from .notify_admins import on_startup_notify diff --git a/src/utils/db_api/__init__.py b/src/utils/db_api/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/misc/__init__.py b/src/utils/misc/__init__.py deleted file mode 100644 index 3f7248a..0000000 --- a/src/utils/misc/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -from .throttling import rate_limit -from . import logging diff --git a/src/utils/misc/logging.py b/src/utils/misc/logging.py deleted file mode 100644 index e5b24b9..0000000 --- a/src/utils/misc/logging.py +++ /dev/null @@ -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, # Можно заменить на другой уровень логгирования. - ) diff --git a/src/utils/misc/throttling.py b/src/utils/misc/throttling.py deleted file mode 100644 index c881c9e..0000000 --- a/src/utils/misc/throttling.py +++ /dev/null @@ -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 diff --git a/src/utils/notify_admins.py b/src/utils/notify_admins.py deleted file mode 100644 index 3bcdd6d..0000000 --- a/src/utils/notify_admins.py +++ /dev/null @@ -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) diff --git a/src/utils/set_bot_commands.py b/src/utils/set_bot_commands.py deleted file mode 100644 index bd654b9..0000000 --- a/src/utils/set_bot_commands.py +++ /dev/null @@ -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."), - ] - )