diff --git a/app/bot.py b/app/bot.py
index eee7c72..a640815 100644
--- a/app/bot.py
+++ b/app/bot.py
@@ -6,17 +6,8 @@ from aiogram import Bot, Dispatcher
from aiogram.enums import ParseMode
from aiogram.fsm.storage.redis import RedisStorage
-from app.callbacks import location, menu, notes, profile, travel
+from app import callbacks, handlers, middlewares
from app.config import Config
-from app.handlers import (
- create_travel_command,
- help_command,
- menu_command,
- profile_command,
- start_command,
- travels_command,
-)
-from app.middlewares.throttling import ThrottlingMiddleware
async def main() -> None:
@@ -29,20 +20,21 @@ async def main() -> None:
dp = Dispatcher(storage=storage)
bot = Bot(bot_token, parse_mode=ParseMode.HTML)
- dp.message.middleware(ThrottlingMiddleware(0.5))
+ dp.message.middleware(middlewares.throttling.ThrottlingMiddleware(0.5))
dp.include_routers(
- start_command.router,
- help_command.router,
- menu_command.router,
- profile_command.router,
- create_travel_command.router,
- travels_command.router,
- menu.router,
- profile.router,
- travel.router,
- location.router,
- notes.router,
+ handlers.start_command.router,
+ handlers.help_command.router,
+ handlers.menu_command.router,
+ handlers.profile_command.router,
+ handlers.create_travel_command.router,
+ handlers.travels_command.router,
+ callbacks.menu.router,
+ callbacks.profile.router,
+ callbacks.travel.router,
+ callbacks.location.router,
+ callbacks.notes.router,
+ callbacks.fallback.router,
)
await bot.delete_webhook(drop_pending_updates=True)
diff --git a/app/callbacks/__init__.py b/app/callbacks/__init__.py
new file mode 100644
index 0000000..829ff92
--- /dev/null
+++ b/app/callbacks/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ("fallback", "location", "menu", "profile", "travel", "notes")
+
+from app.callbacks import fallback, location, menu, notes, profile, travel
diff --git a/app/callbacks/fallback.py b/app/callbacks/fallback.py
new file mode 100644
index 0000000..30b0bb9
--- /dev/null
+++ b/app/callbacks/fallback.py
@@ -0,0 +1,17 @@
+__all__ = ("router",)
+
+from aiogram import Router
+from aiogram.filters import StateFilter
+from aiogram.types import CallbackQuery
+
+router = Router(name="fallback_callback")
+
+
+@router.callback_query(~StateFilter(None))
+async def in_state_callback(callback: CallbackQuery):
+ await callback.answer("Fallback text in state", show_alert=True)
+
+
+@router.callback_query()
+async def fallback_callback(callback: CallbackQuery):
+ await callback.answer("Fallback text", show_alert=True)
diff --git a/app/callbacks/notes.py b/app/callbacks/notes.py
index 6a331b5..a71f177 100644
--- a/app/callbacks/notes.py
+++ b/app/callbacks/notes.py
@@ -17,7 +17,7 @@ from app.states.travel import (
)
-router = Router(name="menu_callback")
+router = Router(name="notes_callback")
@router.callback_query(
@@ -110,12 +110,26 @@ async def create_note_file_id(message: Message, state: FSMContext):
data["author_id"] = message.from_user.id
- session.add(Note(**data))
+ note = Note(**data)
+
+ session.add(note)
session.commit()
+ if not note or note == []:
+ return
+
+ if note.file_type == "photo":
+ file_name = f"Photo ID: {note.id}"
+ elif note.file_type == "video":
+ file_name = f"Video ID: {note.id}"
+ elif note.file_type == "voice":
+ file_name = f"Voice ID: {note.id}"
+ else:
+ file_name = note.file_name
+
await message.answer(
- messages.NOTE_ADDED.format(file_name=data["file_name"]),
+ messages.NOTE_ADDED.format(file_name=file_name),
)
await state.clear()
@@ -299,13 +313,21 @@ async def travel_notedelete_callback(
note = Note().get_note_queryset_by_id(note_id)
- note_first = note.first()
- file_name = note_first.file_name
- travel = note_first.travel
-
if not note or note == []:
return
+ note_first = note.first()
+ travel = note_first.travel
+
+ if note_first.file_type == "photo":
+ file_name = f"Photo ID: {note_first.id}"
+ elif note_first.file_type == "video":
+ file_name = f"Video ID: {note_first.id}"
+ elif note_first.file_type == "voice":
+ file_name = f"Voice ID: {note_first.id}"
+ else:
+ file_name = note_first.file_name
+
note.delete()
session.commit()
diff --git a/app/callbacks/travel.py b/app/callbacks/travel.py
index d48d7f1..8953cae 100644
--- a/app/callbacks/travel.py
+++ b/app/callbacks/travel.py
@@ -11,6 +11,7 @@ from app.config import Config
from app.filters.user import Registered, RegisteredCallback
from app.keyboards.builders import travels_keyboard
from app.keyboards.travel import get as travel_get
+from app.keyboards.routes import get as routes_get
from app.models.travel import Travel
from app.models.user import User
from app.states.travel import (
@@ -19,7 +20,7 @@ from app.states.travel import (
from app.utils.states import delete_message_from_state, handle_validation_error
-router = Router(name="menu_callback")
+router = Router(name="travel_callback")
@router.callback_query(
@@ -301,3 +302,35 @@ async def delete_travel_callback(
)
await callback.answer()
+
+
+@router.callback_query(
+ F.data.startswith("travel_routes"),
+ RegisteredCallback(),
+ StateFilter(None),
+)
+async def travel_routes_callback(
+ callback: CallbackQuery,
+):
+ if (
+ callback.message is None
+ or callback.data is None
+ or not isinstance(callback.message, Message)
+ ):
+ return
+
+ travel_id = int(callback.data.replace("travel_routes_", ""))
+
+ travel = Travel().get_travel_by_id(travel_id)
+
+ if not travel:
+ await callback.answer()
+
+ return
+
+ await callback.message.edit_text(
+ travel.get_travel_text(),
+ reply_markup=routes_get(travel),
+ )
+
+ await callback.answer()
diff --git a/app/handlers/__init__.py b/app/handlers/__init__.py
new file mode 100644
index 0000000..d416092
--- /dev/null
+++ b/app/handlers/__init__.py
@@ -0,0 +1,17 @@
+__all__ = (
+ "start_command",
+ "help_command",
+ "menu_command",
+ "profile_command",
+ "create_travel_command",
+ "travels_command",
+)
+
+from app.handlers import (
+ create_travel_command,
+ help_command,
+ menu_command,
+ profile_command,
+ start_command,
+ travels_command,
+)
diff --git a/app/keyboards/__init__.py b/app/keyboards/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/app/keyboards/routes.py b/app/keyboards/routes.py
new file mode 100644
index 0000000..19fb829
--- /dev/null
+++ b/app/keyboards/routes.py
@@ -0,0 +1,63 @@
+__all__ = ("get",)
+
+from aiogram import types
+from aiogram.utils.keyboard import InlineKeyboardBuilder
+
+from app.models.travel import Travel
+from app.utils.geo import get_location_by_name
+from app.utils.map import get_url_map
+
+
+def get(travel: Travel):
+ locations = Travel().get_sorted_locations(travel, asc=False)
+ coordinats = []
+
+ for location in locations:
+ geocode = get_location_by_name(location.location)
+ coordinats.append(
+ [geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
+ )
+
+ builder = InlineKeyboardBuilder()
+
+ builder.row(
+ types.InlineKeyboardButton(
+ text="πΊοΈ Route by car",
+ web_app=types.WebAppInfo(
+ url=get_url_map(
+ coordinats=coordinats,
+ profile="car",
+ ),
+ ),
+ ),
+ )
+ builder.row(
+ types.InlineKeyboardButton(
+ text="πΊοΈ Route on foot",
+ web_app=types.WebAppInfo(
+ url=get_url_map(
+ coordinats=coordinats,
+ profile="foot",
+ ),
+ ),
+ ),
+ )
+ builder.row(
+ types.InlineKeyboardButton(
+ text="πΊοΈ Route by bike",
+ web_app=types.WebAppInfo(
+ url=get_url_map(
+ coordinats=coordinats,
+ profile="bike",
+ ),
+ ),
+ ),
+ )
+ builder.row(
+ types.InlineKeyboardButton(
+ text="β¬
οΈ",
+ callback_data=f"travel_detail_{travel.id}",
+ ),
+ )
+
+ return builder.as_markup()
diff --git a/app/keyboards/travel.py b/app/keyboards/travel.py
index 4e72b60..b9cbd9f 100644
--- a/app/keyboards/travel.py
+++ b/app/keyboards/travel.py
@@ -4,20 +4,9 @@ from aiogram import types
from aiogram.utils.keyboard import InlineKeyboardBuilder
from app.models.travel import Travel
-from app.utils.geo import get_location_by_name
-from app.utils.map import get_url_map
def get(travel: Travel):
- locations = Travel().get_sorted_locations(travel, asc=False)
- coordinats = []
-
- for location in locations:
- geocode = get_location_by_name(location.location)
- coordinats.append(
- [geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
- )
-
builder = InlineKeyboardBuilder()
builder.row(
@@ -62,35 +51,8 @@ def get(travel: Travel):
)
builder.row(
types.InlineKeyboardButton(
- text="πΊοΈ Route by car",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="car",
- ),
- ),
- ),
- )
- builder.row(
- types.InlineKeyboardButton(
- text="πΊοΈ Route on foot",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="foot",
- ),
- ),
- ),
- )
- builder.row(
- types.InlineKeyboardButton(
- text="πΊοΈ Route by bike",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="bike",
- ),
- ),
+ text="πΊοΈ Routes",
+ callback_data=f"travel_routes_{travel.id}",
),
)
builder.row(
@@ -110,15 +72,6 @@ def get(travel: Travel):
def get_public(travel: Travel):
- locations = Travel().get_sorted_locations(travel, asc=False)
- coordinats = []
-
- for location in locations:
- geocode = get_location_by_name(location.location)
- coordinats.append(
- [geocode[1].raw.get("lat"), geocode[1].raw.get("lon")],
- )
-
builder = InlineKeyboardBuilder()
builder.row(
@@ -145,35 +98,8 @@ def get_public(travel: Travel):
)
builder.row(
types.InlineKeyboardButton(
- text="πΊοΈ Route by car",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="car",
- ),
- ),
- ),
- )
- builder.row(
- types.InlineKeyboardButton(
- text="πΊοΈ Route on foot",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="foot",
- ),
- ),
- ),
- )
- builder.row(
- types.InlineKeyboardButton(
- text="πΊοΈ Route by bike",
- web_app=types.WebAppInfo(
- url=get_url_map(
- coordinats=coordinats,
- profile="bike",
- ),
- ),
+ text="πΊοΈ Routes",
+ callback_data=f"travel_routes_{travel.id}",
),
)
builder.row(
diff --git a/app/messages.py b/app/messages.py
index 8f1a93a..94b9261 100644
--- a/app/messages.py
+++ b/app/messages.py
@@ -1,10 +1,24 @@
# flake8: noqa
+# Menu
MENU = "Menu:"
+
+# Notes
NOTES = "π Notes:\n"
-NOTE_DETAIL = "π Note detail:\n\n\tFile name: {file_name}\n\tFile type: {file_type}\n\tPublic: {public}"
+NOTE_DETAIL = (
+ "π Note detail:\n\n"
+ "\tFile name: {file_name}\n"
+ "\tFile type: {file_type}\n"
+ "\tPublic: {public}"
+)
+ADD_NOTE = (
+ "βοΈ Send me a file, photo, video, or voice message to add a note."
+ "\nEnter /cancel to cancel creating."
+)
NOTE_ADDED = "β
Note {file_name} added successfully."
NOTE_DELETED = "β Note {file_name} deleted."
+
+# Locations
LOCATIONS = "πΊοΈ Locations:"
LOCATION_DELETED = "β Location deleted."
LOCATION_DETAIL = (
@@ -16,48 +30,77 @@ LOCATION_DETAIL = (
LOCATION_WEATHER = (
"π€οΈ {location} weather:\n\n"
"\tβοΈ Weather: {weather_main}\n"
- "\tπ‘οΈ Current tempurature: {temp} Β°C\n"
+ "\tπ‘οΈ Current temperature: {temp} Β°C\n"
"\tπ€ Feels like: {feels_like} Β°C\n"
- "\tβοΈ Min. tempurature: {temp_min} Β°C\n"
- "\tπ₯ Max. tempurature: {temp_max} Β°C\n"
+ "\tβοΈ Min. temperature: {temp_min} Β°C\n"
+ "\tπ₯ Max. temperature: {temp_max} Β°C\n"
"\tβ¬οΈ Pressure: {pressure} hektopascals\n"
"\tπ¨ Humidity: {humidity}%\n"
)
+
+# Sights
SIGHTS_HEADER = "πΊοΈ Sights:\n"
-SIGHTS_FOOTER = "Found {sights_count} sights within {distance} m from: {location}."
+SIGHTS_FOOTER = (
+ "Found {sights_count} sights within {distance} m from: {location}."
+)
NO_SIGHTS_FOUND = (
"No sights found within {distance} m from: {location}."
)
SIGHT_DETAIL = "πΊοΈ Sight detail:\n\n"
-CREATE_LOCATION = "βοΈ Lets create new location!"
-ENTER_LOCATION = "Enter location:\nFormat: country, city, ... etc\nExample: Kremlin, Moscow, Russia\nEnter /cancel to cancel creating."
+
+# Location Creation
+CREATE_LOCATION = "βοΈ Lets create a new location!"
+ENTER_LOCATION = (
+ "Enter location:\n"
+ "Format: country, city, ... etc\n"
+ "Example: Kremlin, Moscow, Russia\n"
+ "Enter /cancel to cancel creating."
+)
CONFIRM_LOCATION = "Is this location correct: {location}?"
-CONFIRMATION_REEJECTED = (
+CONFIRMATION_REJECTED = (
"β Confirmation rejected. Please re-enter the location."
)
-OVERLAPPING_LOCATION = "Dates overlap with another location in the same travel(enter /cancel if you cant fix this)."
-ENTER_LOCATION_DATE_START = "Enter location start datetime(in UTC) in this format:\nFormat: YYYY-MM-DD HH:MM\nExample: 2022-01-01 00:00"
-ENTER_LOCATION_DATE_END = "Enter location end datetime(in UTC) in this format:\nFormat: YYYY-MM-DD HH:MM\nExample: 2022-01-01 00:00"
+OVERLAPPING_LOCATION = "Dates overlap with another location in the same travel(enter /cancel if you can't fix this)."
+ENTER_LOCATION_DATE_START = (
+ "Enter location start datetime(in UTC) in this format:\n"
+ "Format: YYYY-MM-DD HH:MM\n"
+ "Example: 2022-01-01 00:00"
+)
+ENTER_LOCATION_DATE_END = (
+ "Enter location end datetime(in UTC) in this format:\n"
+ "Format: YYYY-MM-DD HH:MM\n"
+ "Example: 2022-01-01 00:00>"
+)
INVALID_DATE_END = "End date can't be earlier or equal to start date."
LOCATION_ADDED = "β
Location added"
-ADD_NOTE = "βοΈ Send me file, photo, video or voice nessage to add note.\nEnter /cancel to cancel creating."
-
+# Travel Management
DELETED_TRAVEL = "β
Travel deleted"
-TRAVELS = "π Travels:\nπ - owner"
+TRAVELS = (
+ "π Travels:"
+ "\nπ - owner"
+)
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
CREATE_TRAVEL = (
- "π§³ Let's create new travel!\nEnter /cancel to cancel creating."
+ "π§³ Let's create a new travel!\n"
+ "Enter /cancel to cancel creating."
)
TRAVEL_UPDATED = "β
Travel updated"
-EDIT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to set it to None):\nMaximum length: 100 characters"
+EDIT_TRAVEL_DESCRIPTION = (
+ "Enter travel description (enter /skip if you want to set it to None):\n"
+ "Maximum length: 100 characters"
+)
INPUT_TRAVEL_TITLE = (
- "Enter travel title:\nMaximum length: 30 characters"
+ "Enter travel title:\n"
+ "Maximum length: 30 characters"
)
INPUT_TRAVEL_CALLBACK = (
"All right, travel {key} is set to: {value}"
)
-INPUT_TRAVEL_DESCRIPTION = "Enter travel description (enter /skip if you want to skip this step):\nMaximum length: 100 characters"
+INPUT_TRAVEL_DESCRIPTION = (
+ "Enter travel description (enter /skip if you want to skip this step):\n"
+ "Maximum length: 100 characters"
+)
INPUT_TRAVEL_DESCRIPTION_SKIPPED = "β
Sure. You can always fill it later."
TRAVEL_CREATED = "Travel {title} successfully created! You can now view and edit it in the travels list (/travels command)."
ACTION_CANCELED = "β Action canceled"
@@ -68,11 +111,12 @@ TRAVEL_DETAIL = (
"\tDescription: {description}\n"
)
+# User Interaction
WELCOME_MESSAGE = "Hello, {name}! Welcome to the βοΈ Travel Agent bot! Let's start our journey by filling out some information about you."
WELCOME_AGAIN_MESSAGE = "Hello, {name}! Welcome back to the βοΈ Travel Agent bot! If you get lost, you can always call the /help command for assistance."
HELP_MESSAGE = (
- "Welcome to the βοΈ Travel Agent bot! Here is list of commands you can use:\n\n"
+ "Welcome to the βοΈ Travel Agent bot! Here is a list of commands you can use:\n\n"
"/start - Start the bot\n"
"/help - Show this message\n"
"/menu - Show the main menu\n"
@@ -83,18 +127,39 @@ HELP_MESSAGE = (
"β If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram."
)
-REGISTERED_MESSAGE = "You have successfully registered. Welcome to the βοΈ Travel Agent bot! \nYou can view and edit your profile using the /profile command."
+REGISTERED_MESSAGE = (
+ "You have successfully registered. Welcome to the βοΈ Travel Agent bot!\n"
+ "You can view and edit your profile using the /profile command."
+)
-INPUT_USERNAME = "Enter your username (this will be used to interact with other users):\nAllowed characters: a-z, A-Z, 0-9, _\nLength: 5-20 characters"
-INPUT_AGE = "Enter your age:\nRange: 13-120"
-INPUT_SEX = "Enter your sex:\nOptions: Male or Female"
-INPUT_BIO = "Enter your bio (enter /skip if you want to skip this step):\nMaximum length: 100 characters"
+INPUT_USERNAME = (
+ "Enter your username (this will be used to interact with other users):\n"
+ "Allowed characters: a-z, A-Z, 0-9, _\n"
+ "Length: 5-20 characters"
+)
+INPUT_AGE = (
+ "Enter your age:\n"
+ "Range: 13-120"
+)
+INPUT_SEX = (
+ "Enter your sex:\n"
+ "Options: Male or Female"
+)
+INPUT_BIO = (
+ "Enter your bio (enter /skip if you want to skip this step):\n"
+ "Maximum length: 100 characters"
+)
INPUT_BIO_SKIPPED = "β
Sure. You can always fill it later."
-INPUT_LOCATION = "Enter your location in this format:\nFormat: country, city\nExample: Russia, Moscow"
+INPUT_LOCATION = (
+ "Enter your location in this format:\n"
+ "Format: country, city\n"
+ "Example: Russia, Moscow"
+)
INPUT_CALLBACK = "β
All right, your {key} is set to: {value}"
VALIDATION_ERROR = "β Invalid input. Please try again."
CANCEL_CHANGE = "Enter /cancel to cancel change."
+# User Profile
PROFILE = (
"π€ Your profile:\n\n"
"\tUsername: {username}\n"
@@ -106,9 +171,21 @@ PROFILE = (
"\tDate joined: {date_joined} UTC\n"
)
NOT_SET = "Not set"
-EDIT_USERNAME = "Enter your username:\nAllowed characters: a-z, A-Z, 0-9, _\nLength: 5-20 characters"
-EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\nMaximum length: 100 characters"
-PROFILE_UPDATED = "β
Profile {key} updated\n\tOld value: {old_value}\n\tNew value: {new_value}"
+EDIT_USERNAME = (
+ "Enter your username:\n"
+ "Allowed characters: a-z, A-Z, 0-9, _\n"
+ "Length: 5-20 characters"
+)
+EDIT_BIO = (
+ "Enter your bio (enter /skip if you want to set it to None):\n"
+ "Maximum length: 100 characters"
+)
+PROFILE_UPDATED = (
+ "β
Profile {key} updated\n"
+ "\tOld value: {old_value}\n"
+ "\tNew value: {new_value}"
+)
CHANGE_CANCELED = "β Change canceled"
-PROCCESSING = "βοΈ Processing..."
+# Processing
+PROCESSING = "βοΈ Processing..."
diff --git a/app/middlewares/__init__.py b/app/middlewares/__init__.py
new file mode 100644
index 0000000..ba11b70
--- /dev/null
+++ b/app/middlewares/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ("throttling",)
+
+from app.middlewares import throttling
diff --git a/app/models/travel.py b/app/models/travel.py
index 480c047..2343a3a 100644
--- a/app/models/travel.py
+++ b/app/models/travel.py
@@ -243,7 +243,7 @@ class Note(Base):
def get_note_text(self):
return messages.NOTE_DETAIL.format(
file_name=self.file_name,
- file_type=self.file_type,
+ file_type=self.file_type.capitalize(),
public="Yes" if self.public else "No",
)