You've already forked Travel-Agent
feat: Added notes creation, view and deletion, added route planning, added location list with current weather and nearby locations, code improvements and fixes
This commit is contained in:
+4
-2
@@ -6,7 +6,7 @@ from aiogram import Bot, Dispatcher
|
||||
from aiogram.enums import ParseMode
|
||||
from aiogram.fsm.storage.redis import RedisStorage
|
||||
|
||||
from app.callbacks import menu, profile, travels
|
||||
from app.callbacks import location, menu, notes, profile, travel
|
||||
from app.config import Config
|
||||
from app.handlers import (
|
||||
create_travel_command,
|
||||
@@ -40,7 +40,9 @@ async def main() -> None:
|
||||
travels_command.router,
|
||||
menu.router,
|
||||
profile.router,
|
||||
travels.router,
|
||||
travel.router,
|
||||
location.router,
|
||||
notes.router,
|
||||
)
|
||||
|
||||
await bot.delete_webhook(drop_pending_updates=True)
|
||||
|
||||
@@ -7,258 +7,25 @@ from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
import sqlalchemy as sa
|
||||
|
||||
from app import messages, session
|
||||
from app.config import Config
|
||||
from app.filters.user import Registered, RegisteredCallback
|
||||
from app.keyboards.builders import travels_keyboard
|
||||
from app.keyboards.builders import locations_keyboard, sights_keyboard
|
||||
from app.keyboards.confirm_location import get as confirm_location_get
|
||||
from app.keyboards.travel import get as travel_get
|
||||
from app.keyboards.location import get as location_get
|
||||
from app.models.travel import Location, Travel
|
||||
from app.models.user import User
|
||||
from app.states.travel import CreateLocationState, TravelAlteringState
|
||||
from app.states.travel import (
|
||||
CreateLocationState,
|
||||
)
|
||||
from app.utils.geo import get_location_by_name
|
||||
from app.utils.sights import find_trips, get_info_by_xid
|
||||
from app.utils.states import delete_message_from_state, handle_validation_error
|
||||
from app.utils.weather import get_current_weather
|
||||
|
||||
|
||||
router = Router(name="menu_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data == "travels",
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travels_index_callback(callback: CallbackQuery) -> None:
|
||||
page = 0
|
||||
|
||||
if callback.from_user is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
try:
|
||||
await callback.message.edit_text(messages.NO_TRAVELS)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(
|
||||
travels,
|
||||
page,
|
||||
pages,
|
||||
user.telegram_id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travels_page"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travels_callback(callback: CallbackQuery) -> None:
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
page = int(callback.data.replace("travels_page_", ""))
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
try:
|
||||
await callback.message.edit_text(messages.NO_TRAVELS)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(
|
||||
travels,
|
||||
page,
|
||||
pages,
|
||||
user.telegram_id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_detail"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_detail_callback(callback: CallbackQuery) -> None:
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
travel_id = int(callback.data.replace("travel_detail_", ""))
|
||||
|
||||
travel = Travel().get_travel_by_id(travel_id)
|
||||
|
||||
if not travel:
|
||||
return
|
||||
|
||||
await callback.message.edit_text(
|
||||
travel.get_travel_text(),
|
||||
reply_markup=travel_get(travel_id),
|
||||
)
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_change"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_change_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if (
|
||||
callback.data is None
|
||||
or callback.message is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
travel_id, column = callback.data.replace("travel_change_", "").split("_")
|
||||
|
||||
travel = Travel().get_travel_by_id(travel_id)
|
||||
|
||||
if not travel:
|
||||
return
|
||||
|
||||
if column == "title":
|
||||
message = await callback.message.answer(
|
||||
f"{messages.INPUT_TRAVEL_TITLE}\n{messages.CANCEL_CHANGE}",
|
||||
)
|
||||
elif column == "description":
|
||||
message = await callback.message.answer(
|
||||
f"{messages.EDIT_TRAVEL_DESCRIPTION}\n{messages.CANCEL_CHANGE}",
|
||||
)
|
||||
|
||||
await state.update_data(
|
||||
column=column,
|
||||
travel_message_id=callback.message.message_id,
|
||||
input_message_id=message.message_id,
|
||||
travel_id=travel_id,
|
||||
)
|
||||
await state.set_state(TravelAlteringState.value)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(TravelAlteringState.value, F.text, Registered())
|
||||
async def travel_change_entered(message: Message, state: FSMContext) -> None:
|
||||
if (
|
||||
message.text is None
|
||||
or message.from_user is None
|
||||
or message.bot is None
|
||||
):
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
column = data["column"]
|
||||
travel_id = data["travel_id"]
|
||||
value = message.text.strip()
|
||||
|
||||
if value == "/cancel":
|
||||
await message.answer(
|
||||
messages.CHANGE_CANCELED,
|
||||
)
|
||||
|
||||
await state.update_data(successfully=True)
|
||||
await message.delete()
|
||||
await delete_message_from_state(
|
||||
state,
|
||||
message.chat.id,
|
||||
message.bot,
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
return
|
||||
|
||||
if column == "title":
|
||||
try:
|
||||
validated_title = Travel().validate_title(
|
||||
key="title",
|
||||
value=value,
|
||||
)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await state.update_data(value=validated_title, successfully=True)
|
||||
elif column == "description":
|
||||
if value == "/skip":
|
||||
await state.update_data(value=None, successfully=True)
|
||||
await delete_message_from_state(
|
||||
state,
|
||||
message.chat.id,
|
||||
message.bot,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
validated_description = Travel().validate_description(
|
||||
key="description",
|
||||
value=value,
|
||||
)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await state.update_data(
|
||||
value=validated_description,
|
||||
successfully=True,
|
||||
)
|
||||
|
||||
await message.delete()
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
state_data = await state.get_data()
|
||||
|
||||
travel = Travel().get_travel_queryset_by_id(travel_id)
|
||||
|
||||
data = {state_data["column"]: state_data["value"]}
|
||||
travel.update(data)
|
||||
|
||||
session.commit()
|
||||
|
||||
travel = travel.first()
|
||||
session.refresh(travel)
|
||||
|
||||
try:
|
||||
await message.bot.edit_message_text(
|
||||
travel.get_travel_text(),
|
||||
message.chat.id,
|
||||
state_data["travel_message_id"],
|
||||
reply_markup=travel_get(travel_id),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
await message.answer(
|
||||
messages.TRAVEL_UPDATED,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
router = Router(name="location_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
@@ -525,8 +292,6 @@ async def location_date_end_entered(
|
||||
|
||||
return
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
await state.update_data(
|
||||
date_end=datetime.datetime.strftime(
|
||||
validated_date_end,
|
||||
@@ -536,6 +301,35 @@ async def location_date_end_entered(
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
overlapping_location = (
|
||||
session.query(Location)
|
||||
.filter(
|
||||
sa.and_(
|
||||
Location.travel_id == data["travel_id"],
|
||||
Location.date_start < data["date_end"],
|
||||
Location.date_end > data["date_start"],
|
||||
),
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if overlapping_location:
|
||||
await handle_validation_error(
|
||||
message,
|
||||
state,
|
||||
messages.OVERLAPPING_LOCATION,
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
await message.answer(
|
||||
messages.INPUT_TRAVEL_CALLBACK.format(
|
||||
key="end date",
|
||||
value=date_end,
|
||||
),
|
||||
)
|
||||
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
if "temp_location" in data:
|
||||
del data["temp_location"]
|
||||
|
||||
@@ -566,26 +360,241 @@ async def location_date_end_entered(
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_delete"),
|
||||
F.data.startswith("travel_locations_page"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def delete_travel_callback(
|
||||
async def travel_locations_page_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
travel_id = int(callback.data.replace("travel_delete_", ""))
|
||||
travel_id, page = map(
|
||||
int,
|
||||
callback.data.replace("travel_locations_page_", "").split("_"),
|
||||
)
|
||||
|
||||
travel = Travel.get_travel_queryset_by_id(travel_id)
|
||||
travel = Travel.get_travel_by_id(travel_id)
|
||||
|
||||
travel.delete()
|
||||
if not travel or travel == []:
|
||||
return
|
||||
|
||||
locations = Travel.get_sorted_locations(travel)
|
||||
|
||||
pages = (len(locations) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.LOCATIONS,
|
||||
reply_markup=locations_keyboard(
|
||||
locations,
|
||||
page,
|
||||
pages,
|
||||
travel_id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_location_detail"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_detail_location_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
location_id = int(callback.data.replace("travel_location_detail_", ""))
|
||||
|
||||
location = Location.get_location_by_id(location_id)
|
||||
|
||||
if not location or location == []:
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
location.get_location_text(),
|
||||
reply_markup=location_get(location.travel.id, location.id),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_locationdelete"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_locations_delete_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
location_id = int(callback.data.replace("travel_locationdelete_", ""))
|
||||
|
||||
location_queryset = Location.get_location_queryset_by_id(location_id)
|
||||
|
||||
if not location_queryset or location_queryset == []:
|
||||
return
|
||||
|
||||
travel = location_queryset.first().travel
|
||||
|
||||
location_queryset.delete()
|
||||
|
||||
session.commit()
|
||||
|
||||
await callback.message.answer(messages.DELETED_TRAVEL)
|
||||
locations = Travel.get_sorted_locations(travel)
|
||||
|
||||
await callback.message.delete()
|
||||
pages = (len(locations) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.LOCATIONS,
|
||||
reply_markup=locations_keyboard(
|
||||
locations,
|
||||
0,
|
||||
pages,
|
||||
travel.id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
await callback.message.answer(
|
||||
messages.LOCATION_DELETED,
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_locationsights"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_locationsights_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
location_id = int(callback.data.replace("travel_locationsights_", ""))
|
||||
|
||||
location = Location.get_location_by_id(location_id)
|
||||
|
||||
if not location or location == []:
|
||||
return
|
||||
|
||||
geocode = get_location_by_name(location.location)
|
||||
|
||||
sights = find_trips(geocode[1].raw.get("lat"), geocode[1].raw.get("lon"))
|
||||
|
||||
if sights is None or len(sights) == 0:
|
||||
await callback.message.answer(
|
||||
messages.NO_SIGHTS_FOUND.format(
|
||||
location=location.location,
|
||||
distance=Config.NEARBY_SIGHTS_RADIUS,
|
||||
),
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
messages.SIGHTS_HEADER
|
||||
+ messages.SIGHTS_FOOTER.format(
|
||||
location=location.location,
|
||||
sights_count=len(sights),
|
||||
distance=Config.NEARBY_SIGHTS_RADIUS,
|
||||
),
|
||||
reply_markup=sights_keyboard(sights[:20]),
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_sight_detail"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_sight_detail_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
sight_xid = callback.data.replace("travel_sight_detail_", "")
|
||||
|
||||
await get_info_by_xid(callback, sight_xid)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_locationweather"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_locationweather_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
location_id = int(callback.data.replace("travel_locationweather_", ""))
|
||||
|
||||
location = Location.get_location_by_id(location_id)
|
||||
|
||||
if not location or location == []:
|
||||
return
|
||||
|
||||
geocode = get_location_by_name(location.location)
|
||||
|
||||
weather = get_current_weather(
|
||||
geocode[1].raw.get("lat"),
|
||||
geocode[1].raw.get("lon"),
|
||||
)
|
||||
|
||||
await callback.message.answer(
|
||||
messages.LOCATION_WEATHER.format(
|
||||
location=location.location,
|
||||
weather_main=weather.get("weather")[0].get("main"),
|
||||
temp=weather.get("main").get("temp"),
|
||||
feels_like=weather.get("main").get("feels_like"),
|
||||
temp_min=weather.get("main").get("temp_min"),
|
||||
temp_max=weather.get("main").get("temp_max"),
|
||||
pressure=weather.get("main").get("pressure"),
|
||||
humidity=weather.get("main").get("humidity"),
|
||||
),
|
||||
reply_to_message_id=callback.message.message_id,
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
@@ -91,3 +91,16 @@ async def travels_callback(
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data == "menu_help",
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def help_callback(callback: CallbackQuery) -> None:
|
||||
if not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
await callback.message.answer(messages.HELP_MESSAGE)
|
||||
await callback.answer()
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from app import messages, session
|
||||
from app.config import Config
|
||||
from app.filters.user import RegisteredCallback
|
||||
from app.keyboards.builders import notes_keyboard
|
||||
from app.keyboards.note import get as notes_get
|
||||
from app.models.travel import Note, Travel
|
||||
from app.states.travel import (
|
||||
CreateNoteState,
|
||||
)
|
||||
|
||||
|
||||
router = Router(name="menu_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_add_note"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_add_note_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
travel_id = int(callback.data.replace("travel_add_note_", ""))
|
||||
|
||||
travel = Travel.get_travel_by_id(travel_id)
|
||||
|
||||
if not travel or travel == []:
|
||||
return
|
||||
|
||||
await state.set_state(CreateNoteState.file_id)
|
||||
await state.update_data(travel_id=travel_id)
|
||||
await callback.message.answer(
|
||||
messages.ADD_NOTE,
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(
|
||||
CreateNoteState.file_id,
|
||||
)
|
||||
async def create_note_file_id(message: Message, state: FSMContext):
|
||||
if message.from_user is None:
|
||||
return
|
||||
|
||||
if message.text == "/cancel":
|
||||
await message.answer(
|
||||
messages.ACTION_CANCELED,
|
||||
)
|
||||
|
||||
await state.update_data()
|
||||
await message.delete()
|
||||
await state.clear()
|
||||
|
||||
return
|
||||
|
||||
if message.photo is None and message.document is None:
|
||||
return
|
||||
|
||||
if message.photo is not None:
|
||||
await state.update_data(
|
||||
file_type="photo",
|
||||
file_id=message.photo[-1].file_id,
|
||||
file_name="photo",
|
||||
)
|
||||
|
||||
# await message.answer_photo(message.photo[-1].file_id)
|
||||
|
||||
elif message.document is not None:
|
||||
await state.update_data(
|
||||
file_type="document",
|
||||
file_id=message.document.file_id,
|
||||
file_name=message.document.file_name,
|
||||
)
|
||||
|
||||
# await message.answer_document(message.document.file_id)
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
data["author_id"] = message.from_user.id
|
||||
|
||||
session.add(Note(**data))
|
||||
|
||||
session.commit()
|
||||
|
||||
await message.answer(
|
||||
messages.NOTE_ADDED.format(file_name=data["file_name"]),
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_notes_page"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_notes_page_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
travel_id, page = map(
|
||||
int,
|
||||
callback.data.replace("travel_notes_page_", "").split("_"),
|
||||
)
|
||||
|
||||
travel = Travel.get_travel_queryset_by_id(travel_id)
|
||||
|
||||
if not travel or travel == []:
|
||||
return
|
||||
|
||||
travel = travel.first()
|
||||
|
||||
notes = Travel().get_notes(callback.from_user.id, travel, public=False)
|
||||
|
||||
pages = (len(notes) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.NOTES,
|
||||
reply_markup=notes_keyboard(notes, page, pages, travel.id),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_note_detail"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_note_detail_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
note_id = int(callback.data.replace("travel_note_detail_", ""))
|
||||
|
||||
note = Note.get_note_by_id(note_id)
|
||||
|
||||
if not note or note == []:
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
note.get_note_text(),
|
||||
reply_markup=notes_get(travel_id=note.travel.id, note=note),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_notesend"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_notesend_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
note_id = int(callback.data.replace("travel_notesend_", ""))
|
||||
|
||||
note = Note.get_note_by_id(note_id)
|
||||
|
||||
if not note or note == []:
|
||||
return
|
||||
|
||||
if note.file_type == "photo":
|
||||
await callback.message.answer_photo(
|
||||
note.file_id,
|
||||
reply_to_message_id=callback.message.message_id,
|
||||
)
|
||||
|
||||
elif note.file_type == "document":
|
||||
await callback.message.answer_document(
|
||||
note.file_id,
|
||||
reply_to_message_id=callback.message.message_id,
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_note_change_privacy"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_note_change_privacy_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
note_id = int(callback.data.replace("travel_note_change_privacy_", ""))
|
||||
|
||||
note = Note().get_note_by_id(note_id)
|
||||
|
||||
if not note or note == []:
|
||||
return
|
||||
|
||||
if note.public:
|
||||
note.public = False
|
||||
else:
|
||||
note.public = True
|
||||
|
||||
session.commit()
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
note.get_note_text(),
|
||||
reply_markup=notes_get(travel_id=note.travel.id, note=note),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_notedelete"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_notedelete_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if (
|
||||
callback.message is None
|
||||
or callback.data is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
note_id = int(callback.data.replace("travel_notedelete_", ""))
|
||||
|
||||
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.delete()
|
||||
|
||||
session.commit()
|
||||
|
||||
notes = Travel().get_notes(callback.from_user.id, travel, public=False)
|
||||
|
||||
pages = (len(notes) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.NOTES,
|
||||
reply_markup=notes_keyboard(notes, 0, pages, travel.id),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
await callback.message.answer(
|
||||
messages.NOTE_DELETED.format(file_name=file_name),
|
||||
)
|
||||
@@ -22,7 +22,7 @@ router = Router(name="profile_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("profile_change_"),
|
||||
F.data.startswith("profile_change"),
|
||||
StateFilter(None),
|
||||
RegisteredCallback(),
|
||||
)
|
||||
@@ -227,8 +227,12 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
||||
state_data = await state.get_data()
|
||||
|
||||
user = User.get_user_queryset_by_telegram_id(message.from_user.id)
|
||||
user_first = user.first()
|
||||
|
||||
if isinstance(state_data["value"], list):
|
||||
old_value = user_first.country + ", " + user_first.city
|
||||
new_value = state_data["value"][0] + ", " + state_data["value"][1]
|
||||
|
||||
user.update(
|
||||
{
|
||||
"country": state_data["value"][0],
|
||||
@@ -241,6 +245,9 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
else:
|
||||
old_value = getattr(user.first(), str(column))
|
||||
new_value = state_data["value"]
|
||||
|
||||
data = {state_data["column"]: state_data["value"]}
|
||||
user.update(data)
|
||||
|
||||
@@ -260,7 +267,11 @@ async def profile_change_entered(message: Message, state: FSMContext) -> None:
|
||||
pass
|
||||
|
||||
await message.answer(
|
||||
messages.PROFILE_UPDATED,
|
||||
messages.PROFILE_UPDATED.format(
|
||||
key=state_data["column"],
|
||||
old_value=old_value if old_value else messages.NOT_SET,
|
||||
new_value=(new_value if new_value else messages.NOT_SET),
|
||||
),
|
||||
reply_markup=ReplyKeyboardRemove(),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,303 @@
|
||||
__all__ = ("router",)
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.exceptions import TelegramBadRequest
|
||||
from aiogram.filters import StateFilter
|
||||
from aiogram.fsm.context import FSMContext
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
|
||||
from app import messages, session
|
||||
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.models.travel import Travel
|
||||
from app.models.user import User
|
||||
from app.states.travel import (
|
||||
TravelAlteringState,
|
||||
)
|
||||
from app.utils.states import delete_message_from_state, handle_validation_error
|
||||
|
||||
|
||||
router = Router(name="menu_callback")
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data == "travels",
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travels_index_callback(callback: CallbackQuery) -> None:
|
||||
page = 0
|
||||
|
||||
if callback.from_user is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
try:
|
||||
await callback.message.edit_text(messages.NO_TRAVELS)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(
|
||||
travels,
|
||||
page,
|
||||
pages,
|
||||
user.telegram_id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travels_page"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travels_callback(callback: CallbackQuery) -> None:
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
page = int(callback.data.replace("travels_page_", ""))
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
if not travels or travels == []:
|
||||
try:
|
||||
await callback.message.edit_text(messages.NO_TRAVELS)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
else:
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(
|
||||
travels,
|
||||
page,
|
||||
pages,
|
||||
user.telegram_id,
|
||||
),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_detail"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_detail_callback(callback: CallbackQuery) -> None:
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
travel_id = int(callback.data.replace("travel_detail_", ""))
|
||||
|
||||
travel = Travel().get_travel_by_id(travel_id)
|
||||
|
||||
if not travel:
|
||||
return
|
||||
|
||||
try:
|
||||
await callback.message.edit_text(
|
||||
travel.get_travel_text(),
|
||||
reply_markup=travel_get(travel),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_change"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def travel_change_callback(
|
||||
callback: CallbackQuery,
|
||||
state: FSMContext,
|
||||
) -> None:
|
||||
if (
|
||||
callback.data is None
|
||||
or callback.message is None
|
||||
or not isinstance(callback.message, Message)
|
||||
):
|
||||
return
|
||||
|
||||
travel_id, column = callback.data.replace("travel_change_", "").split("_")
|
||||
|
||||
travel = Travel().get_travel_by_id(travel_id)
|
||||
|
||||
if not travel:
|
||||
return
|
||||
|
||||
if column == "title":
|
||||
message = await callback.message.answer(
|
||||
f"{messages.INPUT_TRAVEL_TITLE}\n{messages.CANCEL_CHANGE}",
|
||||
)
|
||||
elif column == "description":
|
||||
message = await callback.message.answer(
|
||||
f"{messages.EDIT_TRAVEL_DESCRIPTION}\n{messages.CANCEL_CHANGE}",
|
||||
)
|
||||
|
||||
await state.update_data(
|
||||
column=column,
|
||||
travel_message_id=callback.message.message_id,
|
||||
input_message_id=message.message_id,
|
||||
travel_id=travel_id,
|
||||
)
|
||||
await state.set_state(TravelAlteringState.value)
|
||||
|
||||
await callback.answer()
|
||||
|
||||
|
||||
@router.message(TravelAlteringState.value, F.text, Registered())
|
||||
async def travel_change_entered(message: Message, state: FSMContext) -> None:
|
||||
if (
|
||||
message.text is None
|
||||
or message.from_user is None
|
||||
or message.bot is None
|
||||
):
|
||||
return
|
||||
|
||||
data = await state.get_data()
|
||||
|
||||
column = data["column"]
|
||||
travel_id = data["travel_id"]
|
||||
value = message.text.strip()
|
||||
|
||||
if value == "/cancel":
|
||||
await message.answer(
|
||||
messages.CHANGE_CANCELED,
|
||||
)
|
||||
|
||||
await state.update_data(successfully=True)
|
||||
await message.delete()
|
||||
await delete_message_from_state(
|
||||
state,
|
||||
message.chat.id,
|
||||
message.bot,
|
||||
)
|
||||
await state.clear()
|
||||
|
||||
return
|
||||
|
||||
if column == "title":
|
||||
try:
|
||||
validated_title = Travel().validate_title(
|
||||
key="title",
|
||||
value=value,
|
||||
)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await state.update_data(value=validated_title, successfully=True)
|
||||
elif column == "description":
|
||||
if value == "/skip":
|
||||
await state.update_data(value=None, successfully=True)
|
||||
await delete_message_from_state(
|
||||
state,
|
||||
message.chat.id,
|
||||
message.bot,
|
||||
)
|
||||
else:
|
||||
try:
|
||||
validated_description = Travel().validate_description(
|
||||
key="description",
|
||||
value=value,
|
||||
)
|
||||
except AssertionError as e:
|
||||
await handle_validation_error(message, state, e)
|
||||
|
||||
return
|
||||
|
||||
await state.update_data(
|
||||
value=validated_description,
|
||||
successfully=True,
|
||||
)
|
||||
|
||||
await message.delete()
|
||||
await delete_message_from_state(state, message.chat.id, message.bot)
|
||||
|
||||
state_data = await state.get_data()
|
||||
|
||||
travel = Travel().get_travel_queryset_by_id(travel_id)
|
||||
|
||||
data = {state_data["column"]: state_data["value"]}
|
||||
travel.update(data)
|
||||
|
||||
session.commit()
|
||||
|
||||
travel = travel.first()
|
||||
session.refresh(travel)
|
||||
|
||||
try:
|
||||
await message.bot.edit_message_text(
|
||||
travel.get_travel_text(),
|
||||
message.chat.id,
|
||||
state_data["travel_message_id"],
|
||||
reply_markup=travel_get(travel),
|
||||
)
|
||||
except TelegramBadRequest:
|
||||
pass
|
||||
|
||||
await message.answer(
|
||||
messages.TRAVEL_UPDATED,
|
||||
)
|
||||
|
||||
await state.clear()
|
||||
|
||||
|
||||
@router.callback_query(
|
||||
F.data.startswith("travel_delete"),
|
||||
RegisteredCallback(),
|
||||
StateFilter(None),
|
||||
)
|
||||
async def delete_travel_callback(
|
||||
callback: CallbackQuery,
|
||||
):
|
||||
if callback.data is None or not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
travel_id = int(callback.data.replace("travel_delete_", ""))
|
||||
|
||||
user = User().get_user_by_telegram_id(callback.from_user.id)
|
||||
|
||||
travel = Travel.get_travel_queryset_by_id(travel_id)
|
||||
|
||||
travel.delete()
|
||||
|
||||
session.commit()
|
||||
|
||||
travels = user.get_user_travels()
|
||||
|
||||
pages = (len(travels) + Config.PAGE_SIZE - 1) // Config.PAGE_SIZE
|
||||
|
||||
await callback.message.answer(messages.DELETED_TRAVEL)
|
||||
|
||||
await callback.message.edit_text(
|
||||
messages.TRAVELS,
|
||||
reply_markup=travels_keyboard(
|
||||
travels,
|
||||
0,
|
||||
pages,
|
||||
callback.from_user.id,
|
||||
),
|
||||
)
|
||||
|
||||
await callback.answer()
|
||||
@@ -17,4 +17,11 @@ class Config:
|
||||
"REDIS_URL",
|
||||
"redis://localhost:6379",
|
||||
)
|
||||
OPENTRIPMAP_API_KEY = os.getenv(
|
||||
"OPENTRIPMAP_API_KEY",
|
||||
)
|
||||
OPENWEATHERMAP_API_KEY = os.getenv(
|
||||
"OPENWEATHERMAP_API_KEY",
|
||||
)
|
||||
NEARBY_SIGHTS_RADIUS = 2000
|
||||
PAGE_SIZE = 6
|
||||
|
||||
+176
-1
@@ -58,9 +58,10 @@ def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
|
||||
),
|
||||
)
|
||||
|
||||
total_pages = 1 if pages == 0 else pages
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{page + 1}/{pages}",
|
||||
text=f"{page + 1}/{total_pages}",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
@@ -83,3 +84,177 @@ def travels_keyboard(travels: list, page: int, pages: int, user_id: int):
|
||||
builder.row(*navigation_row)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def locations_keyboard(locations: list, page: int, pages: int, travel_id: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
rows = []
|
||||
|
||||
start_index = page * Config.PAGE_SIZE
|
||||
end_index = min((page + 1) * Config.PAGE_SIZE, len(locations))
|
||||
|
||||
for location in locations[start_index:end_index]:
|
||||
button_text = location.location
|
||||
|
||||
rows.append(
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"travel_location_detail_{location.id}",
|
||||
),
|
||||
)
|
||||
|
||||
for _ in range(0, Config.PAGE_SIZE - len(rows)):
|
||||
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
|
||||
|
||||
builder.row(*rows, width=2)
|
||||
|
||||
navigation_row = []
|
||||
|
||||
if page > 0:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_locations_{travel_id}_{page - 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
total_pages = 1 if pages == 0 else pages
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{page + 1}/{total_pages}",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
if page < pages - 1:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="➡️",
|
||||
callback_data=f"travel_locations_{travel_id}_{page + 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(*navigation_row)
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_detail_{travel_id}",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def notes_keyboard(notes, page: int, pages: int, travel_id: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
rows = []
|
||||
|
||||
start_index = page * Config.PAGE_SIZE
|
||||
end_index = min((page + 1) * Config.PAGE_SIZE, len(notes))
|
||||
|
||||
for note in notes[start_index:end_index]:
|
||||
if note.file_type == "photo":
|
||||
button_text = f"Photo ID: {note.id}"
|
||||
else:
|
||||
button_text = note.file_name
|
||||
|
||||
rows.append(
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"travel_note_detail_{note.id}",
|
||||
),
|
||||
)
|
||||
|
||||
for _ in range(0, Config.PAGE_SIZE - len(rows)):
|
||||
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
|
||||
|
||||
builder.row(*rows, width=2)
|
||||
|
||||
navigation_row = []
|
||||
|
||||
if page > 0:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_notes_page_{travel_id}_{page - 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
total_pages = 1 if pages == 0 else pages
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=f"{page + 1}/{total_pages}",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
if page < pages - 1:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text="➡️",
|
||||
callback_data=f"travel_notes_page_{travel_id}_{page + 1}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
navigation_row.append(
|
||||
InlineKeyboardButton(
|
||||
text=" ",
|
||||
callback_data="pass",
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(*navigation_row)
|
||||
|
||||
builder.row(
|
||||
InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_detail_{travel_id}",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def sights_keyboard(sights: list):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
rows = []
|
||||
|
||||
for sight in sights:
|
||||
button_text = sight[0]
|
||||
|
||||
rows.append(
|
||||
InlineKeyboardButton(
|
||||
text=button_text,
|
||||
callback_data=f"travel_sight_detail_{sight[1]}",
|
||||
),
|
||||
)
|
||||
|
||||
for _ in range(0, 20 - len(rows)):
|
||||
rows.append(InlineKeyboardButton(text=" ", callback_data="pass"))
|
||||
|
||||
builder.row(*rows, width=2)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
__all__ = ("get",)
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
|
||||
def get(travel_id: int, location_id: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⏩ Get nearby sights",
|
||||
callback_data=f"travel_locationsights_{location_id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="☁️ Current weather",
|
||||
callback_data=f"travel_locationweather_{location_id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="❌ Delete location",
|
||||
callback_data=f"travel_locationdelete_{location_id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_locations_page_{travel_id}_0",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
def get_public(travel_id: int, location_id: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_locations_page_{travel_id}_0",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
@@ -23,8 +23,8 @@ def get():
|
||||
callback_data="menu_travels",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="🔵 Temp",
|
||||
callback_data="menu_temp",
|
||||
text="❓ Help",
|
||||
callback_data="menu_help",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
__all__ = ("get",)
|
||||
|
||||
from aiogram import types
|
||||
from aiogram.utils.keyboard import InlineKeyboardBuilder
|
||||
|
||||
|
||||
def get(travel_id: int, note):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⏩ View note",
|
||||
callback_data=f"travel_notesend_{note.id}",
|
||||
),
|
||||
)
|
||||
|
||||
if note.public:
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="🔒 Make private",
|
||||
callback_data=f"travel_note_change_privacy_{note.id}",
|
||||
),
|
||||
)
|
||||
else:
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="🔓 Make public",
|
||||
callback_data=f"travel_note_change_privacy_{note.id}",
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="❌ Delete note",
|
||||
callback_data=f"travel_notedelete_{note.id}",
|
||||
),
|
||||
)
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data=f"travel_detail_{travel_id}",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
+133
-10
@@ -3,54 +3,177 @@ __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")],
|
||||
)
|
||||
|
||||
def get(travel_id: int):
|
||||
builder = InlineKeyboardBuilder()
|
||||
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="📝 Change title",
|
||||
callback_data=f"travel_change_{travel_id}_title",
|
||||
callback_data=f"travel_change_{travel.id}_title",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="ℹ️ Change description",
|
||||
callback_data=f"travel_change_{travel_id}_description",
|
||||
callback_data=f"travel_change_{travel.id}_description",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="🗺️ Locations",
|
||||
callback_data=f"travel_locations_{travel_id}",
|
||||
callback_data=f"travel_locations_page_{travel.id}_0",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="➕ Add location",
|
||||
callback_data=f"travel_add_location_{travel_id}",
|
||||
callback_data=f"travel_add_location_{travel.id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="👤 Users",
|
||||
callback_data=f"travel_users_{travel_id}",
|
||||
callback_data=f"travel_users_page_{travel.id}_0",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="➕ Add user",
|
||||
callback_data=f"travel_add_user_{travel_id}",
|
||||
callback_data=f"travel_add_user_{travel.id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="📝 Notes",
|
||||
callback_data=f"travel_notes_{travel_id}",
|
||||
callback_data=f"travel_notes_page_{travel.id}_0",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="➕ Add note",
|
||||
callback_data=f"travel_add_note_{travel_id}",
|
||||
callback_data=f"travel_add_note_{travel.id}",
|
||||
),
|
||||
)
|
||||
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="❌ Delete travel",
|
||||
callback_data=f"travel_delete_{travel_id}",
|
||||
callback_data=f"travel_delete_{travel.id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="⬅️",
|
||||
callback_data="travels",
|
||||
),
|
||||
)
|
||||
|
||||
return builder.as_markup()
|
||||
|
||||
|
||||
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(
|
||||
types.InlineKeyboardButton(
|
||||
text="🗺️ Locations",
|
||||
callback_data=f"travel_locations_{travel.id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="👤 Users",
|
||||
callback_data=f"travel_users_{travel.id}",
|
||||
),
|
||||
)
|
||||
builder.row(
|
||||
types.InlineKeyboardButton(
|
||||
text="📝 Notes",
|
||||
callback_data=f"travel_notes_{travel.id}",
|
||||
),
|
||||
types.InlineKeyboardButton(
|
||||
text="➕ Add note",
|
||||
callback_data=f"travel_add_note_{travel.id}",
|
||||
),
|
||||
)
|
||||
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(
|
||||
|
||||
+34
-4
@@ -1,18 +1,48 @@
|
||||
# flake8: noqa
|
||||
|
||||
MENU = "<b>Menu:</b>"
|
||||
|
||||
NOTES = "📝 <b>Notes:</b>\n"
|
||||
NOTE_DETAIL = "📝 <b>Note detail:</b>\n\n\tFile name: <b>{file_name}</b>\n\tFile type: <b>{file_type}</b>\n\tPublic: <b>{public}</b>"
|
||||
NOTE_ADDED = "✅ Note <b>{file_name}</b> added successfully."
|
||||
NOTE_DELETED = "❌ Note <b>{file_name}</b> deleted."
|
||||
LOCATIONS = "🗺️ <b>Locations:</b>"
|
||||
LOCATION_DELETED = "❌ Location deleted."
|
||||
LOCATION_DETAIL = (
|
||||
"🗺️ <b>Location detail:</b>\n\n"
|
||||
"\t<b>Location:</b> {location}\n"
|
||||
"\t<b>Date start:</b> {date_start}\n"
|
||||
"\t<b>Date end:</b> {date_end}"
|
||||
)
|
||||
LOCATION_WEATHER = (
|
||||
"🌤️ <b>{location} weather:</b>\n\n"
|
||||
"\t<b>☁️ Weather:</b> {weather_main}\n"
|
||||
"\t<b>🌡️ Current tempurature:</b> {temp} °C\n"
|
||||
"\t<b>🤗 Feels like:</b> {feels_like} °C\n"
|
||||
"\t<b>❄️ Min. tempurature:</b> {temp_min} °C\n"
|
||||
"\t<b>🔥 Max. tempurature:</b> {temp_max} °C\n"
|
||||
"\t<b>⬇️ Pressure:</b> {pressure} hektopascals\n"
|
||||
"\t<b>💨 Humidity:</b> {humidity}%\n"
|
||||
)
|
||||
SIGHTS_HEADER = "🗺️ <b>Sights:</b>\n"
|
||||
SIGHTS_FOOTER = "Found {sights_count} sights within {distance} m from: <b>{location}</b>."
|
||||
NO_SIGHTS_FOUND = (
|
||||
"No sights found within {distance} m from: <b>{location}</b>."
|
||||
)
|
||||
SIGHT_DETAIL = "🗺️ <b>Sight detail:</b>\n\n"
|
||||
CREATE_LOCATION = "✈️ Lets create new location!"
|
||||
ENTER_LOCATION = "Enter location:"
|
||||
ENTER_LOCATION = "Enter location:\n<i>Format: country, city, ... etc</i>\n<i>Example: Kremlin, Moscow, Russia</i>\n<i>Enter /cancel to cancel creating.</i>"
|
||||
CONFIRM_LOCATION = "Is this location correct: <b>{location}</b>?"
|
||||
CONFIRMATION_REEJECTED = (
|
||||
"❌ 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:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>"
|
||||
ENTER_LOCATION_DATE_END = "Enter location end datetime(in UTC) in this format:\n<i>Format: YYYY-MM-DD HH:MM</i>\n<i>Example: 2022-01-01 00:00</i>"
|
||||
INVALID_DATE_END = "End date can't be earlier or equal to start date."
|
||||
LOCATION_ADDED = "✅ Location added"
|
||||
|
||||
ADD_NOTE = "✏️ Send me file or photo to add note.\n<i>Enter /cancel to cancel creating.</i>"
|
||||
|
||||
DELETED_TRAVEL = "✅ Travel deleted"
|
||||
TRAVELS = "📃 <b>Travels:</b>\n<i>👑 - owner</i>"
|
||||
NO_TRAVELS = "No travels yet. You can create one with /create_travel command."
|
||||
@@ -47,7 +77,7 @@ HELP_MESSAGE = (
|
||||
"/help - Show this message\n"
|
||||
"/menu - Show the main menu\n"
|
||||
"/profile - View and edit your profile\n"
|
||||
"/create_travel - Create new travel\n"
|
||||
"/create_travel - Create a new travel\n"
|
||||
"/travels - View and edit your travels\n"
|
||||
"/cancel - Cancel the current action\n\n"
|
||||
"❓ If you have any questions/issues, feel free to contact us via @itq_travel_agent_support_bot on Telegram."
|
||||
@@ -78,7 +108,7 @@ PROFILE = (
|
||||
NOT_SET = "<i>Not set</i>"
|
||||
EDIT_USERNAME = "Enter your username:\n<i>Allowed characters: a-z, A-Z, 0-9, _</i>\n<i>Length: 5-20 characters</i>"
|
||||
EDIT_BIO = "Enter your bio (enter /skip if you want to set it to None):\n<i>Maximum length: 100 characters</i>"
|
||||
PROFILE_UPDATED = "✅ Profile updated"
|
||||
PROFILE_UPDATED = "✅ Profile {key} updated\n\t<i>Old value: {old_value}</i>\n\t<i>New value: {new_value}</i>"
|
||||
CHANGE_CANCELED = "❌ Change canceled"
|
||||
|
||||
PROCCESSING = "⌛️ Processing..."
|
||||
|
||||
+9
-15
@@ -1,8 +1,8 @@
|
||||
"""Added travel models
|
||||
|
||||
Revision ID: 4dea8f302149
|
||||
Revision ID: 78ab1b779ca8
|
||||
Revises: 4914f00ae14a
|
||||
Create Date: 2024-03-24 17:56:20.975589
|
||||
Create Date: 2024-03-25 17:23:37.917899
|
||||
|
||||
"""
|
||||
|
||||
@@ -13,7 +13,7 @@ import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '4dea8f302149'
|
||||
revision: str = '78ab1b779ca8'
|
||||
down_revision: Union[str, None] = '4914f00ae14a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
@@ -28,8 +28,7 @@ def upgrade() -> None:
|
||||
sa.Column('description', sa.String(length=100), nullable=True),
|
||||
sa.Column('author_id', sa.BigInteger(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['author_id'],
|
||||
['users.telegram_id'],
|
||||
['author_id'], ['users.telegram_id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
sa.UniqueConstraint('title'),
|
||||
@@ -43,8 +42,7 @@ def upgrade() -> None:
|
||||
sa.Column('date_end', sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
['travel_id'], ['travels.id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
@@ -59,12 +57,10 @@ def upgrade() -> None:
|
||||
sa.Column('author_id', sa.BigInteger(), nullable=False),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
['author_id'],
|
||||
['users.telegram_id'],
|
||||
['author_id'], ['users.telegram_id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
['travel_id'], ['travels.id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
@@ -74,12 +70,10 @@ def upgrade() -> None:
|
||||
sa.Column('user_id', sa.BigInteger(), nullable=True),
|
||||
sa.Column('travel_id', sa.Integer(), nullable=True),
|
||||
sa.ForeignKeyConstraint(
|
||||
['travel_id'],
|
||||
['travels.id'],
|
||||
['travel_id'], ['travels.id'], ondelete='CASCADE'
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
['user_id'],
|
||||
['users.telegram_id'],
|
||||
['user_id'], ['users.telegram_id'], ondelete='CASCADE'
|
||||
),
|
||||
)
|
||||
# ### end Alembic commands ###
|
||||
+75
-10
@@ -14,8 +14,16 @@ from app.utils.geo import get_location_by_name
|
||||
association_table = sa.Table(
|
||||
"user_travel_association",
|
||||
Base.metadata,
|
||||
sa.Column("user_id", sa.BigInteger, sa.ForeignKey("users.telegram_id")),
|
||||
sa.Column("travel_id", sa.Integer, sa.ForeignKey("travels.id")),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.BigInteger,
|
||||
sa.ForeignKey("users.telegram_id", ondelete="CASCADE"),
|
||||
),
|
||||
sa.Column(
|
||||
"travel_id",
|
||||
sa.Integer,
|
||||
sa.ForeignKey("travels.id", ondelete="CASCADE"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -42,7 +50,7 @@ class Travel(Base):
|
||||
|
||||
author_id = sa.Column(
|
||||
sa.BigInteger,
|
||||
sa.ForeignKey(User.telegram_id),
|
||||
sa.ForeignKey(User.telegram_id, ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@@ -83,6 +91,18 @@ class Travel(Base):
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_notes(cls, author_id, travel, public=True):
|
||||
return (
|
||||
session.query(Note)
|
||||
.filter(
|
||||
Note.author_id == author_id,
|
||||
Note.travel_id == travel.id,
|
||||
Note.public == public,
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_travel_by_id(cls, travel_id):
|
||||
return session.query(Travel).filter(Travel.id == travel_id).first()
|
||||
@@ -91,6 +111,14 @@ class Travel(Base):
|
||||
def get_travel_queryset_by_id(cls, travel_id):
|
||||
return session.query(Travel).filter(Travel.id == travel_id)
|
||||
|
||||
@classmethod
|
||||
def get_sorted_locations(cls, travel, asc=True):
|
||||
return sorted(
|
||||
travel.locations,
|
||||
key=lambda location: location.date_end,
|
||||
reverse=asc,
|
||||
)
|
||||
|
||||
|
||||
class Location(Base):
|
||||
__tablename__ = "locations"
|
||||
@@ -114,17 +142,16 @@ class Location(Base):
|
||||
|
||||
travel_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey("travels.id"),
|
||||
sa.ForeignKey("travels.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
@validates("location")
|
||||
def validate_location(self, key, value):
|
||||
geocoder = get_location_by_name(value)
|
||||
geocode = get_location_by_name(value)
|
||||
|
||||
assert geocoder[0], "Invalid location."
|
||||
assert geocode[0], "Invalid location."
|
||||
|
||||
return geocoder[1].raw["display_name"]
|
||||
return geocode[1].raw["display_name"]
|
||||
|
||||
def validate_date_start(self, key, value):
|
||||
try:
|
||||
@@ -158,6 +185,29 @@ class Location(Base):
|
||||
|
||||
return value_datetime
|
||||
|
||||
def get_location_text(self):
|
||||
return messages.LOCATION_DETAIL.format(
|
||||
location=self.location,
|
||||
date_start=datetime.datetime.strftime(
|
||||
self.date_start,
|
||||
"%Y-%m-%d %H:%M",
|
||||
),
|
||||
date_end=datetime.datetime.strftime(
|
||||
self.date_end,
|
||||
"%Y-%m-%d %H:%M",
|
||||
),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_location_by_id(cls, location_id):
|
||||
return (
|
||||
session.query(Location).filter(Location.id == location_id).first()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_location_queryset_by_id(cls, location_id):
|
||||
return session.query(Location).filter(Location.id == location_id)
|
||||
|
||||
|
||||
class Note(Base):
|
||||
__tablename__ = "notes"
|
||||
@@ -181,11 +231,26 @@ class Note(Base):
|
||||
|
||||
author_id = sa.Column(
|
||||
sa.BigInteger,
|
||||
sa.ForeignKey(User.telegram_id),
|
||||
sa.ForeignKey(User.telegram_id, ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
travel_id = sa.Column(
|
||||
sa.Integer,
|
||||
sa.ForeignKey("travels.id"),
|
||||
sa.ForeignKey("travels.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
def get_note_text(self):
|
||||
return messages.NOTE_DETAIL.format(
|
||||
file_name=self.file_name,
|
||||
file_type=self.file_type,
|
||||
public="Yes" if self.public else "No",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_note_by_id(cls, note_id):
|
||||
return session.query(Note).filter(Note.id == note_id).first()
|
||||
|
||||
@classmethod
|
||||
def get_note_queryset_by_id(cls, note_id):
|
||||
return session.query(Note).filter(Note.id == note_id)
|
||||
|
||||
@@ -28,3 +28,10 @@ class CreateLocationState(StatesGroup):
|
||||
location = State()
|
||||
date_start = State()
|
||||
date_end = State()
|
||||
|
||||
|
||||
class CreateNoteState(StatesGroup):
|
||||
travel_id = State()
|
||||
file_id = State()
|
||||
file_type = State()
|
||||
file_name = State()
|
||||
|
||||
+1
-2
@@ -1,5 +1,5 @@
|
||||
# type: ignore
|
||||
__all__ = ("validate_country", "validate_city", "get_location_by_name")
|
||||
__all__ = ("get_location_by_name", "validate_country", "validate_city")
|
||||
|
||||
from geopy.exc import GeocoderTimedOut
|
||||
from geopy.geocoders import Nominatim
|
||||
@@ -81,7 +81,6 @@ def get_location_by_name(location: str) -> None:
|
||||
try:
|
||||
geocode = geolocator.geocode(
|
||||
location,
|
||||
featuretype="city",
|
||||
)
|
||||
break
|
||||
except GeocoderTimedOut:
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
__all__ = ("get_url_map",)
|
||||
|
||||
|
||||
def get_url_map(coordinats: list, profile: str):
|
||||
result_url = "https://graphhopper.com/maps/?"
|
||||
|
||||
for coordinat in coordinats:
|
||||
result_url += f"point={coordinat[0]}, {coordinat[1]}&"
|
||||
|
||||
result_url += f"profile={profile}&layer=OpenStreetMap"
|
||||
|
||||
return result_url
|
||||
@@ -0,0 +1,108 @@
|
||||
__all__ = ("find_trips", "get_info_by_xid")
|
||||
|
||||
from aiogram.types import CallbackQuery, Message
|
||||
import requests
|
||||
|
||||
from app import messages
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def find_trips(lat, lon, type_of_trip="unclassified_objects"):
|
||||
api_key = Config.OPENTRIPMAP_API_KEY
|
||||
radius = Config.NEARBY_SIGHTS_RADIUS
|
||||
|
||||
result_url = (
|
||||
"https://api.opentripmap.com/0.1/ru/places/radius"
|
||||
f"?radius={radius}&"
|
||||
f"kinds={type_of_trip}&"
|
||||
f"lon={lon}&"
|
||||
f"lat={lat}&"
|
||||
f"limit=20&"
|
||||
f"apikey={api_key}"
|
||||
)
|
||||
|
||||
data = requests.get(result_url).json()
|
||||
|
||||
if data["features"]:
|
||||
sights = []
|
||||
|
||||
for feature in data["features"]:
|
||||
button_text = (
|
||||
feature["properties"]["name"]
|
||||
+ " ("
|
||||
+ str(round(feature["properties"]["dist"]))
|
||||
+ "m)"
|
||||
)
|
||||
|
||||
sights.append((button_text, feature["properties"]["xid"]))
|
||||
|
||||
return sights
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def get_info_by_xid(callback: CallbackQuery, xid):
|
||||
if not isinstance(callback.message, Message):
|
||||
return
|
||||
|
||||
api_key = Config.OPENTRIPMAP_API_KEY
|
||||
|
||||
result_url = (
|
||||
f"https://api.opentripmap.com/0.1/ru/places/xid/{xid}?apikey={api_key}"
|
||||
)
|
||||
|
||||
data = requests.get(result_url).json()
|
||||
|
||||
text = messages.SIGHT_DETAIL
|
||||
|
||||
if data.get("name", ""):
|
||||
text += (
|
||||
"\n\t<b>📝 Name:</b> " + data.get("name", "<i>Missing</i>") + "\n"
|
||||
)
|
||||
|
||||
if data.get("address", ""):
|
||||
address_string = ""
|
||||
key_order = [
|
||||
"country",
|
||||
"state",
|
||||
"city",
|
||||
"city_district",
|
||||
"suburb",
|
||||
"road",
|
||||
"house_number",
|
||||
]
|
||||
address = data["address"]
|
||||
|
||||
for key in key_order:
|
||||
if address.get(key, ""):
|
||||
address_string += f" {address.get(key)}, "
|
||||
|
||||
text += "\t<b>📫 Address:</b> " + address_string + "\n"
|
||||
|
||||
if "wikipedia_extracts" in data:
|
||||
wikipedia_extracts = data["wikipedia_extracts"]
|
||||
wikipedia_title = wikipedia_extracts.get("title", "<i>Missing</i>")
|
||||
wikipedia_description = wikipedia_extracts.get(
|
||||
"text",
|
||||
"<i>Missing</i>",
|
||||
)
|
||||
|
||||
text += f"\n\t<b>📝 Wikipedia title:</b> {wikipedia_title}\n"
|
||||
text += f"\t<b>ℹ️ Wikipedia description:</b> {wikipedia_description}\n"
|
||||
|
||||
if "wikipedia" in data:
|
||||
wikipedia_link = data["wikipedia"]
|
||||
|
||||
text += f"\t<b>🔗 Wikipedia link:</b> {wikipedia_link}\n"
|
||||
|
||||
if "image" in data:
|
||||
await callback.message.answer_photo(
|
||||
data["image"],
|
||||
text,
|
||||
reply_to_message_id=callback.message.message_id,
|
||||
)
|
||||
else:
|
||||
await callback.message.answer(
|
||||
text,
|
||||
reply_to_message_id=callback.message.message_id,
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
__all__ = "get_weather"
|
||||
|
||||
import requests
|
||||
|
||||
from app.config import Config
|
||||
|
||||
|
||||
def get_current_weather(lat, lot):
|
||||
api_key = Config.OPENWEATHERMAP_API_KEY
|
||||
result_url = f"https://api.openweathermap.org/data/2.5/weather?lat={lat}&lon={lot}&appid={api_key}&lang=en&units=metric" # noqa
|
||||
|
||||
return requests.get(result_url).json()
|
||||
@@ -41,6 +41,8 @@ services:
|
||||
BOT_TOKEN: ${BOT_TOKEN:-6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk}
|
||||
REDIS_URL: redis://redis:${REDIS_PORT:-6379}/
|
||||
SQLALCHEMY_DATABASE_URI: postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres}
|
||||
OPENTRIPMAP_API_KEY: ${OPENTRIPMAP_API_KEY:-5ae2e3f221c38a28845f05b65004e18feef4a0f1a06c9554424e9f14}
|
||||
OPENWEATHERMAP_API_KEY: ${OPENWEATHERMAP_API_KEY:-3673b2ff64c097b3f79fc93e429e7ed1}
|
||||
entrypoint: ["bash", "-c"]
|
||||
command: ["alembic -c app/alembic.ini upgrade head && python -m app"]
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
black
|
||||
mypy
|
||||
sort-requirements
|
||||
types-requests==2.31
|
||||
|
||||
-r prod.txt
|
||||
-r test.txt
|
||||
|
||||
@@ -5,4 +5,5 @@ geopy==2.4.1
|
||||
psycopg2-binary==2.9.9
|
||||
python-dotenv==1.0.1
|
||||
redis==5.0.3
|
||||
requests==2.31.0
|
||||
sqlalchemy==2.0.28
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
BOT_TOKEN = <your_bot_token> # default: 6943803094:AAFxMjuiaqLlQbITUOVPlKx6SKIofKrThwk
|
||||
SQLALCHEMY_DATABASE_URI = <database_uri> # no need to specify if docker is used
|
||||
REDIS_URL = <redis_url> # no need to specify if docker is used
|
||||
OPENTRIPMAP_API_KEY = <api_key> # get it from https://dev.opentripmap.org/
|
||||
OPENWEATHERMAP_API_KEY = <api_key> # get it from https://openweathermap.org/
|
||||
|
||||
# For docker(remove if you want to keep defaults)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user