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."),
- ]
- )