Database changes, new translations #32

Merged
profitroll merged 50 commits from dev into main 2023-11-05 15:37:22 +02:00
18 changed files with 438 additions and 162 deletions
Showing only changes of commit 0c25fdb377 - Show all commits

View File

@ -2,7 +2,7 @@ from typing import List, Union
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from libbot.pyrogram.classes import PyroClient as LibPyroClient from libbot.pyrogram.classes import PyroClient as LibPyroClient
from pymongo import ASCENDING, GEO2D from pymongo import ASCENDING, GEOSPHERE, TEXT
from pyrogram.types import User from pyrogram.types import User
from classes.location import Location from classes.location import Location
@ -24,9 +24,10 @@ class PyroClient(LibPyroClient):
[("id", ASCENDING)], name="location_id", unique=True [("id", ASCENDING)], name="location_id", unique=True
) )
await col_locations.create_index( await col_locations.create_index(
[("location", GEO2D)], [("location", GEOSPHERE)],
name="location_location", name="location_location",
) )
await col_locations.create_index([("name", TEXT)], name="location_name")
return await super().start(**kwargs) return await super().start(**kwargs)
async def find_user(self, user: Union[int, User]) -> PyroUser: async def find_user(self, user: Union[int, User]) -> PyroUser:

View File

@ -15,8 +15,15 @@
"port": 27017, "port": 27017,
"name": "garbagebot" "name": "garbagebot"
}, },
"search": {
"radius": 0.1
},
"reports": { "reports": {
"chat_id": "owner" "chat_id": "owner"
}, },
"disabled_plugins": [] "disabled_plugins": [],
"strings": {
"url_repo": "https://git.end-play.xyz/GarbageReminder/TelegramBot",
"url_contact": "https://git.end-play.xyz/GarbageReminder/TelegramBot/issues"
}
} }

View File

@ -33,14 +33,66 @@
"remove_commands": "Unregister all commands" "remove_commands": "Unregister all commands"
}, },
"messages": { "messages": {
"help": "Help message here, lol.", "cancel": "Use /cancel if you want to cancel this operation.",
"cancelled": "Operation has been cancelled.",
"checkout_deleted": "🗑️ Your data has been deleted. If you want to start using this bot again, please use /setup command. Otherwise delete/block the bot and do not interact with it anymore.",
"checkout_deletion": "Alright. Please, confirm that you want to delete your data from the bot.\n\nFollowing data will be deleted:\n• Selected location\n• Preferred language of all messages\n• Time of the notifications\n• Offset of the notifications\n\nUse keyboard provided to confirm and continue or /cancel to abort this operation.",
"checkout": "Here's pretty much all the data bot has. Please, use these buttons to choose whether you want to delete your data from the bot.",
"commands_removed": "✅ All currently registered commands have been unregistered. Commands will be registered again on bot's start.",
"help": "🔔 This bot sends you notifications about garbage collection according to your local schedule.\n\n**Available commands**\n/help - Show this message\n/setup - Select the location\n/toggle - Disable/enable the reminders\n/set_time - Set the reminders' time\n/set_offset - Set offset between reminders and collection\n/upcoming - Show the upcoming collection\n/language - Select the bot's language\n/checkout - Export or remove your data\n\n💭 You can also suggest adding your town/district to the bot by contacting the admins using [this link]({url_contact}) and providing your schedule.\n\n⚙ Want to host this bot yourself or make some changes? It's open-source, so you can basically fork it. Take a look at [bot's repository]({url_repo}) for details.\n\nHappy using! 🤗",
"import_finished": "You have successfully inserted {count} entries.",
"import_invalid_date": "Entries contain invalid date formats. Use **ISO 8601** date format.",
"import_invalid_filetype": "Invalid input. Please, send me a JSON file with entries. {cancel_notice}",
"import_invalid": "This is not a valid garbage collection JSON.",
"import": "Alright. Send me a valid JSON.",
"locale_choice": "Alright. Please choose the language using keyboard below.",
"location_name_empty": "Could not find any locations by this name. Try rephrasing it or make sure you use the same location language and name itself as it in written by your local authorities in garbage collection schedule.\n\n{cancel_notice}",
"location_name_invalid": "Please, send the name of the location as a text. {cancel_notice}",
"location_name": "Please, send me a location name. It should be the name used in your local authorities' garbage collection schedule. This usually is a name of the district or even the town itself.",
"location_select": "Select the location using the keyboard provided.",
"reminder": "**Garbage Collection**\n\nType: {type}\nDate: {date}\n\nDon't forget to prepare your bin for collection!",
"search_nearby_empty": "Could not find any locations nearby. Let's try using the name search.",
"selection_invalid": "Please, select a valid option using the keyboard provided. {cancel_notice}",
"set_offset_finished": "🔔 Notifications offset has been updated! You will now receive notification about collection **{offset} d.** before the collection at {time}. {toggle_notice}",
"set_offset_invalid": "Please, provide a valid integer number of days in range 0 to 7 (inclusive). {cancel_notice}",
"set_offset": "Alright. Please, send how many days in advance do you want to get a notification about the collection.",
"set_time_finished": "🔔 Notifications time has been updated! You will now receive notification about collection {offset} d. before the collection at **{time}**. {toggle_notice}",
"set_time_invalid": "Please, provide a valid time in HH:MM format. {cancel_notice}",
"set_time": "Alright. Please, send your desired time in HH:MM format.",
"setup_finished": "✅ Finished! Your location is now **{name}**. You will receive the notifications about garbage collection {offset} d. in advance at {time}.",
"setup_retry": " If you want try selecting the location again, use the /setup command.",
"setup": "⚙️ Let's begin configuration with the search for your location.\n\nPlease, select whether you want to search among the locations near you or go straight to the search by location name.\n\nNote that the location you send will **NOT** be saved anywhere and is only used for location lookup in the database.",
"start_code_invalid": "🚫 You have started the bot by the link containing a location, but it does not seem to be a valid one. Please, use the command /setup to manually configure the location.",
"start_code": " You have started the bot by the link containing a location **{name}**.\n\nPlease, confirm whether you want to use it as your location.",
"start_configure": "📍 Let's configure your location. Press the button on pop-up keyboard to start the process.",
"start_selection_no": "Alright, you're on your own now. Please, use the command /setup to configure your location and start receiving reminders.",
"start_selection_yes": "✅ Finished! Your location is now **{name}**. You will receive reminders about garbage collection {offset} d. in advance at {time}.\n\nPlease, visit /help if you want to know how to change notifications time or disable them.",
"start": "👋 Welcome!\n\nThis small open-source bot is made to simplify your life a bit easier by sending you notifications about upcoming garbage collection in your location.\n\nBy using this bot you accept [Privacy Policy]({privacy_policy}), otherwise please block and remove this bot before further interaction.\n\nNow the official part is over so you can dive into the bot.", "start": "👋 Welcome!\n\nThis small open-source bot is made to simplify your life a bit easier by sending you notifications about upcoming garbage collection in your location.\n\nBy using this bot you accept [Privacy Policy]({privacy_policy}), otherwise please block and remove this bot before further interaction.\n\nNow the official part is over so you can dive into the bot.",
"locale_choice": "Alright. Please choose the language using keyboard below." "toggle_disabled": "🔕 Notifications have been disabled.",
"toggle_enabled_location": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time} at the **{name}**.",
"toggle_enabled": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time}. Use /setup to select your location.",
"toggle": "Execute /toggle to enable notifications.",
"upcoming_empty_location": "You have no location set. Use /setup to select your location.",
"upcoming_empty": "No garbage collection entries found for the next 30 days at **{name}**",
"upcoming": "Upcoming garbage collection:\n\n{entries}"
}, },
"buttons": { "buttons": {
"configure": "Let's configure the bot" "delete_confirm": "I agree and want to proceed",
"delete_no": "❌ No, I don't want to delete it",
"delete_yes": "✅ Yes, I want to delete it",
"search_name": "🔎 Search by location name",
"search_nearby": "📍 Search nearby locations",
"start_code_no": "❌ No, I don't want to use it",
"start_code_yes": "✅ Yes, I want to use it",
"start_configure": "⚙️ Let's configure the bot"
}, },
"callbacks": { "callbacks": {
"locale_set": "Your language now is: {locale}" "locale_set": "Your language now is: {locale}"
},
"force_replies": {
"import": "JSON with garbage entries",
"location_name": "Location name",
"set_offset": "Number of days",
"set_time": "Time as HH:MM"
} }
} }

View File

@ -34,7 +34,7 @@
}, },
"messages": { "messages": {
"help": "Привіт! Я твій бот!", "help": "Привіт! Я твій бот!",
"start": "Hi! By using this bot you accept **terms of service** and **privacy policy**, otherwise please block and remove this bot before further interaction.", "start": "👋 Вітання!\n\nЦей невеличкий бот з відкритим вихідним кодом створений для того, щоб трохи спростити Вам життя, надсилаючи сповіщення про вивіз сміття у вашому регіоні.\n\nКористуючись цим ботом, Ви приймаєте [Політику конфіденційності]({privacy_policy}), в іншому випадку, будь ласка, заблокуйте та видаліть цього бота перед подальшою взаємодією.\n\nТепер офіційна частина закінчена, тож Ви можете зануритися в бота.",
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче." "locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче."
}, },
"buttons": { "buttons": {
@ -42,5 +42,6 @@
}, },
"callbacks": { "callbacks": {
"locale_set": "Встановлено мову: {locale}" "locale_set": "Встановлено мову: {locale}"
} },
"force_replies": {}
} }

View File

@ -22,8 +22,6 @@ async def remind(app: PyroClient) -> None:
for user_db in users: for user_db in users:
user = PyroUser(**user_db) user = PyroUser(**user_db)
logger.info("Processing %s...", user.id)
if not user.enabled or user.location is None: if not user.enabled or user.location is None:
continue continue
@ -61,7 +59,7 @@ async def remind(app: PyroClient) -> None:
await app.send_message( await app.send_message(
user.id, user.id,
"**Garbage Collection**\n\nType: {type}\nDate: {date}\n\nDon't forget to prepare your bin for collection!".format( app._("reminder", "messages", locale=user.locale).format(
type=garbage_type, date=garbage_date type=garbage_type, date=garbage_date
), ),
) )

96
modules/search_name.py Normal file
View File

@ -0,0 +1,96 @@
from typing import Union
from convopyro import listen_message
from pykeyboard import ReplyButton, ReplyKeyboard
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from classes.location import Location
from classes.pyroclient import PyroClient
from modules.database import col_locations
async def search_name(app: PyroClient, message: Message) -> Union[Location, None]:
user = await app.find_user(message.from_user)
location: Union[Location, None] = None
await message.reply_text(
app._("location_request_name", "messages", locale=user.locale),
reply_markup=ForceReply(
placeholder=app._("location_name", "force_replies", locale=user.locale)
),
)
while location is None:
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
if answer.text is None:
await message.reply_text(
app._("location_name_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
),
reply_markup=ForceReply(
placeholder=app._(
"location_name", "force_replies", locale=user.locale
)
),
)
continue
query = {"$text": {"$search": answer.text}}
locations = await col_locations.find(query).limit(6).to_list()
if len(locations) == 0:
await message.reply_text(
app._("location_name_empty", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
),
reply_markup=ForceReply(
placeholder=app._(
"location_name", "force_replies", locale=user.locale
)
),
)
continue
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
await message.reply_text(
app._("location_select", "messages", locale=user.locale),
reply_markup=keyboard,
)
while True:
answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel":
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
for db_record in locations:
if answer.text == db_record["name"]:
location = Location(**db_record)
if answer.text is None or location is None:
await answer.reply_text(
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
break
return location

69
modules/search_nearby.py Normal file
View File

@ -0,0 +1,69 @@
from typing import Union
from convopyro import listen_message
from pykeyboard import ReplyButton, ReplyKeyboard
from pyrogram.types import Message, ReplyKeyboardRemove
from classes.location import Location
from classes.pyroclient import PyroClient
from modules.database import col_locations
from modules.search_name import search_name
async def search_nearby(app: PyroClient, message: Message) -> Union[Location, None]:
user = await app.find_user(message.from_user)
query = {
"location": {
"$within": {
"$center": [
[message.location.longitude, message.location.latitude],
app.config["search"]["radius"],
]
}
}
}
locations = await col_locations.find(query).limit(6).to_list()
if len(locations) == 0:
await message.reply_text(
app._("search_nearby_empty", "messages", locale=user.locale)
)
return await search_name(app, message)
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
await message.reply_text(
app._("location_select", "messages", locale=user.locale),
reply_markup=keyboard,
)
while True:
answer = await listen_message(app, message.chat.id, 300)
location: Union[Location, None] = None
if answer is None or answer.text == "/cancel":
await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
for db_record in locations:
if answer.text == db_record["name"]:
location = Location(**db_record)
break
if answer.text is None or location is None:
await answer.reply_text(
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
break
return location or await search_name(app, message)

View File

@ -1,11 +0,0 @@
from pyrogram import filters
from pyrogram.types import CallbackQuery
from classes.pyroclient import PyroClient
@PyroClient.on_callback_query(filters.regex("nothing")) # type: ignore
async def callback_nothing(app: PyroClient, callback: CallbackQuery):
await callback.answer(
text=app._("nothing", "callbacks", locale=callback.from_user.language_code)
)

View File

@ -32,12 +32,12 @@ async def command_checkout(app: PyroClient, message: Message):
row_width=1, resize_keyboard=True, one_time_keyboard=True row_width=1, resize_keyboard=True, one_time_keyboard=True
) )
keyboard_delete.add( keyboard_delete.add(
ReplyButton("Yes, I want to delete it"), ReplyButton(app._("delete_yes", "buttons", locale=user.locale)),
ReplyButton("No, I don't want to delete it"), ReplyButton(app._("delete_no", "buttons", locale=user.locale)),
) )
await message.reply_text( await message.reply_text(
"Here's pretty much all the data bot has. Please, use these buttons to choose whether you want to delete your data from the bot.", app._("checkout", "messages", locale=user.locale),
reply_markup=keyboard_delete, reply_markup=keyboard_delete,
) )
@ -46,25 +46,25 @@ async def command_checkout(app: PyroClient, message: Message):
if answer_delete is None or answer_delete.text == "/cancel": if answer_delete is None or answer_delete.text == "/cancel":
await message.reply_text( await message.reply_text(
"Cancelled.", app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )
return return
if answer_delete.text not in [ if answer_delete.text not in app._(
"Yes, I want to delete it", "delete_yes", "buttons", locale=user.locale
"No, I don't want to delete it", ) + app._("delete_no", "buttons", locale=user.locale):
]:
await answer_delete.reply_text( await answer_delete.reply_text(
"Invalid answer provided. Use /cancel if you want to cancel this operation." app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
if answer_delete.text in [ if answer_delete.text in app._("delete_no", "buttons", locale=user.locale):
"No, I don't want to delete it",
]:
await answer_delete.reply_text( await answer_delete.reply_text(
"Alright, cancelled.", reply_markup=ReplyKeyboardRemove() app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
) )
return return
@ -74,10 +74,12 @@ async def command_checkout(app: PyroClient, message: Message):
keyboard_confirm = ReplyKeyboard( keyboard_confirm = ReplyKeyboard(
row_width=1, resize_keyboard=True, one_time_keyboard=True row_width=1, resize_keyboard=True, one_time_keyboard=True
) )
keyboard_confirm.add(ReplyButton("I agree and want to proceed")) keyboard_confirm.add(
ReplyButton(app._("delete_confirm", "buttons", locale=user.locale))
)
await message.reply_text( await message.reply_text(
"Alright. Please, confirm that you want to delete your data from the bot.\n\nFollowing data will be deleted:\nSelected location, preferred language of the messages, notifications time and your notifications offset.", app._("checkout_deletion", "messages", locale=user.locale),
reply_markup=keyboard_confirm, reply_markup=keyboard_confirm,
) )
@ -86,14 +88,16 @@ async def command_checkout(app: PyroClient, message: Message):
if answer_confirm is None or answer_confirm.text == "/cancel": if answer_confirm is None or answer_confirm.text == "/cancel":
await message.reply_text( await message.reply_text(
"Cancelled.", app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )
return return
if answer_confirm.text not in ["I agree and want to proceed"]: if answer_confirm.text not in app.in_all_locales("delete_confirm", "buttons"):
await answer_confirm.reply_text( await answer_confirm.reply_text(
"Invalid answer provided. Use /cancel if you want to cancel this operation." app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
@ -101,6 +105,6 @@ async def command_checkout(app: PyroClient, message: Message):
await user.delete() await user.delete()
await answer_confirm.reply_text( await answer_confirm.reply_text(
"Your data has been deleted. If you want to start using this bot again, please use /setup command. Otherwise delete/block the bot and do not interact with it anymore.", app._("checkout_deleted", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )

View File

@ -10,4 +10,10 @@ from classes.pyroclient import PyroClient
async def command_help(app: PyroClient, message: Message): async def command_help(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) user = await app.find_user(message.from_user)
await message.reply_text(app._("help", "messages", locale=user.locale)) await message.reply_text(
app._("help", "messages", locale=user.locale).format(
url_contact=app.config["strings"]["url_contact"],
url_repo=app.config["strings"]["url_repo"],
),
disable_web_page_preview=True,
)

View File

@ -3,7 +3,7 @@ from typing import List, Mapping, Union
from convopyro import listen_message from convopyro import listen_message
from pyrogram import filters from pyrogram import filters
from pyrogram.types import Message from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
from ujson import loads from ujson import loads
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
@ -15,18 +15,28 @@ from modules.database import col_entries
~filters.scheduled & filters.private & custom_filters.owner & filters.command(["import"], prefixes=["/"]) # type: ignore ~filters.scheduled & filters.private & custom_filters.owner & filters.command(["import"], prefixes=["/"]) # type: ignore
) )
async def command_import(app: PyroClient, message: Message): async def command_import(app: PyroClient, message: Message):
await message.reply_text("Alright. Send me a valid JSON.") user = await app.find_user(message.from_user)
await message.reply_text(
app._("import", "messages", locale=user.locale),
reply_markup=ForceReply(placeholder=""),
)
while True: while True:
answer = await listen_message(app, message.chat.id, 300) answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel": if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.") await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return return
if answer.document is None or answer.document.mime_type != "application/json": if answer.document is None or answer.document.mime_type != "application/json":
await answer.reply_text( await answer.reply_text(
"Invalid input. Please, send me a JSON file with entries." app._("import_invalid_filetype", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
@ -38,7 +48,10 @@ async def command_import(app: PyroClient, message: Message):
for entry in entries: for entry in entries:
if not isinstance(entries, list): if not isinstance(entries, list):
await answer.reply_text("This is not a valid garbage collection JSON.") await answer.reply_text(
app._("import_invalid", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return return
for key in ("locations", "garbage_type", "date"): for key in ("locations", "garbage_type", "date"):
@ -47,14 +60,18 @@ async def command_import(app: PyroClient, message: Message):
or (key == "garbage_type" and not isinstance(entry[key], int)) or (key == "garbage_type" and not isinstance(entry[key], int))
or (key == "locations" and not isinstance(entry[key], list)) or (key == "locations" and not isinstance(entry[key], list))
): ):
await answer.reply_text("This is not a valid garbage collection JSON.") await answer.reply_text(
app._("import_invalid", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return return
if key == "date": if key == "date":
try: try:
datetime.fromisoformat(str(entry[key])) datetime.fromisoformat(str(entry[key]))
except (ValueError, TypeError): except (ValueError, TypeError):
await answer.reply_text( await answer.reply_text(
"Entries contain invalid date formats. Use **ISO 8601** date format." app._("import_invalid_date", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
) )
return return
@ -70,5 +87,8 @@ async def command_import(app: PyroClient, message: Message):
await col_entries.insert_many(entries_clean) await col_entries.insert_many(entries_clean)
await answer.reply_text( await answer.reply_text(
f"You have successfully inserted {len(entries_clean)} entries." app._("import_finished", "messages", locale=user.locale).format(
count=len(entries_clean)
),
reply_markup=ReplyKeyboardRemove(),
) )

View File

@ -8,5 +8,7 @@ from classes.pyroclient import PyroClient
~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore ~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore
) )
async def command_remove_commands(app: PyroClient, message: Message): async def command_remove_commands(app: PyroClient, message: Message):
await message.reply_text("Okay.") user = await app.find_user(message.from_user)
await message.reply_text(app._("commands_removed", "messages", locale=user.locale))
await app.remove_commands(command_sets=await app.collect_commands()) await app.remove_commands(command_sets=await app.collect_commands())

View File

@ -17,15 +17,20 @@ async def command_set_offset(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) user = await app.find_user(message.from_user)
await message.reply_text( await message.reply_text(
"Alright. Please, send how many days in advance do you want to get a notification about the collection.", app._("set_offset", "messages", locale=user.locale),
reply_markup=ForceReply(placeholder="Number of days"), reply_markup=ForceReply(
placeholder=app._("set_offset", "force_replies", locale=user.locale)
),
) )
while True: while True:
answer = await listen_message(app, message.chat.id, 300) answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel": if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove()) await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return return
try: try:
@ -36,7 +41,9 @@ async def command_set_offset(app: PyroClient, message: Message):
) )
except (ValueError, TypeError): except (ValueError, TypeError):
await answer.reply_text( await answer.reply_text(
"Please, provide a valid integer number of days in range 0 to 7 (inclusive). Use /cancel if you want to cancel this operation." app._("set_offset_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
@ -48,13 +55,17 @@ async def command_set_offset(app: PyroClient, message: Message):
logger.info("User %s has set offset to %s", user.id, offset) logger.info("User %s has set offset to %s", user.id, offset)
notice = "" if user.enabled else "Execute /toggle to enable notifications."
garbage_time = datetime( garbage_time = datetime(
1970, 1, 1, hour=user.time_hour, minute=user.time_minute 1970, 1, 1, hour=user.time_hour, minute=user.time_minute
).strftime(app._("time", "formats")) ).strftime(app._("time", "formats"))
await answer.reply_text( await answer.reply_text(
f"Notifications time has been updated! You will now receive notification about collection **{offset} d.** before the collection at {garbage_time}. {notice}", app._("set_offset_finished", "messages", locale=user.locale).format(
offset=offset,
time=garbage_time,
toggle_notice=""
if user.enabled
else app._("toggle", "messages", locale=user.locale),
),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )

View File

@ -17,22 +17,29 @@ async def command_set_time(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) user = await app.find_user(message.from_user)
await message.reply_text( await message.reply_text(
"Alright. Please, send your desired time in HH:MM format.", app._("set_time", "messages", locale=user.locale),
reply_markup=ForceReply(placeholder="Time as HH:MM"), reply_markup=ForceReply(
placeholder=app._("set_time", "force_replies", locale=user.locale)
),
) )
while True: while True:
answer = await listen_message(app, message.chat.id, 300) answer = await listen_message(app, message.chat.id, 300)
if answer is None or answer.text == "/cancel": if answer is None or answer.text == "/cancel":
await message.reply_text("Cancelled.", reply_markup=ReplyKeyboardRemove()) await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return return
try: try:
datetime.strptime(answer.text, "%H:%M") datetime.strptime(answer.text, "%H:%M")
except ValueError: except ValueError:
await answer.reply_text( await answer.reply_text(
"Please, provide a valid time in HH:MM format. Use /cancel if you want to cancel this operation." app._("set_time_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
@ -48,11 +55,15 @@ async def command_set_time(app: PyroClient, message: Message):
user_time.strftime("%H:%M"), user_time.strftime("%H:%M"),
) )
notice = "" if user.enabled else "Execute /toggle to enable notifications."
garbage_time = user_time.strftime(app._("time", "formats")) garbage_time = user_time.strftime(app._("time", "formats"))
await answer.reply_text( await answer.reply_text(
f"Notifications time has been updated! You will now receive notification about collection {user.offset} d. before the collection at **{garbage_time}**. {notice}", app._("set_time_finished", "messages", locale=user.locale).format(
offset=user.offset,
time=garbage_time,
toggle_notice=""
if user.enabled
else app._("toggle", "messages", locale=user.locale),
),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )

View File

@ -1,4 +1,5 @@
import logging import logging
from datetime import datetime
from convopyro import listen_message from convopyro import listen_message
from libbot import i18n from libbot import i18n
@ -7,6 +8,8 @@ from pyrogram import filters
from pyrogram.types import Message, ReplyKeyboardRemove from pyrogram.types import Message, ReplyKeyboardRemove
from classes.pyroclient import PyroClient from classes.pyroclient import PyroClient
from modules.search_name import search_name
from modules.search_nearby import search_nearby
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -17,88 +20,65 @@ logger = logging.getLogger(__name__)
async def command_setup(app: PyroClient, message: Message): async def command_setup(app: PyroClient, message: Message):
user = await app.find_user(message.from_user) user = await app.find_user(message.from_user)
await message.reply_text( keyboard_type = ReplyKeyboard(resize_keyboard=True, row_width=1)
"Holy... This one is still WIP...", reply_markup=ReplyKeyboardRemove() keyboard_type.add(
ReplyButton(
app._("search_nearby", "buttons", locale=user.locale), request_location=True
),
ReplyButton(app._("search_name", "buttons", locale=user.locale)),
) )
# # City selection await message.reply_text(
# city_names = [city_iter.name for city_iter in await app.get_cities()] app._("setup", "messages", locale=user.locale),
# keyboard_cities = ReplyKeyboard(resize_keyboard=True, row_width=2) reply_markup=keyboard_type,
# keyboard_cities.add(*[ReplyButton(name) for name in city_names]) )
# await message.reply_text( while True:
# "Alright. Please, use the keyboard provided to choose your town.", answer_type = await listen_message(app, message.chat.id, 300)
# reply_markup=keyboard_cities,
# )
# while True: if answer_type is None or answer_type.text == "/cancel":
# answer_city = await listen_message(app, message.chat.id, 300) await message.reply_text(
app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
# if answer_city is None or answer_city.text == "/cancel": if answer_type.location is None and answer_type.text not in app.in_all_locales(
# await message.reply_text("Cancelled.") "search_name", "buttons"
# return ):
await answer_type.reply_text(
app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
)
continue
# if answer_city.text not in city_names: break
# await answer_city.reply_text(
# "Please, select a valid town using keyboard provided. Use /cancel if you want to cancel this operation."
# )
# continue
# break location = (
await search_name(app, answer_type)
if answer_type.location is None
else await search_nearby(app, answer_type)
)
# # City recognition if location is None:
# city = await app.find_city(name=answer_city.text) await answer_type.reply_text(
app._("setup_retry", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
)
return
# # District selection await user.update_location(location.id)
# district_names = [district_iter.name for district_iter in city.districts]
# keyboard_districts = ReplyKeyboard(resize_keyboard=True, row_width=2)
# keyboard_districts.add(*[ReplyButton(name) for name in district_names])
# await message.reply_text( user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime(
# "Alright. Please, use the keyboard provided to choose your district.", app._("time", "formats", locale=user.locale)
# reply_markup=keyboard_districts, )
# )
# while True: await message.reply_text(
# answer_district = await listen_message(app, message.chat.id, 300) app._("setup_finished", "messages", locale=user.locale).format(
name=location.name,
# if answer_district is None or answer_district.text == "/cancel": offset=user.offset,
# await message.reply_text("Cancelled.") time=user_time,
# return ),
reply_markup=ReplyKeyboardRemove(),
# if answer_district.text not in district_names: )
# await answer_district.reply_text(
# "Please, select a valid district using keyboard provided. Use /cancel if you want to cancel this operation."
# )
# continue
# break
# # District recognition
# district_results = city.find_district(answer_district.text)
# if len(district_results) == 0:
# await answer_district.reply_text(
# "Something went wrong. Could not find this district in the database.",
# reply_markup=ReplyKeyboardRemove(),
# )
# return
# district = district_results[0]
# await user.update_city(city.id)
# await user.update_district(district.id)
# logger.info(
# "User %s has finished the location set up with city %s and district %s selected",
# user.id,
# city.id,
# district.id,
# )
# notice = "" if user.enabled else "Execute /toggle to enable notifications."
# await answer_district.reply_text(
# f"All set! You will now receive notification about garbage collection in district **{district.name}** of the town **{city.name}**. {notice}",
# reply_markup=ReplyKeyboardRemove(),
# )

View File

@ -27,21 +27,27 @@ async def command_start(app: PyroClient, message: Message):
location = await app.get_location(int(join_code)) location = await app.get_location(int(join_code))
except ValueError: except ValueError:
await message.reply_text( await message.reply_text(
"🚫 You have provided the location but it does not seem to be a valid one. Please, use the command /setup to manually configure the location." app._("start_code_invalid", "messages", locale=user.locale)
) )
return return
keyboard = ReplyKeyboardMarkup( keyboard = ReplyKeyboardMarkup(
[ [
[KeyboardButton("Yes, I want to use it")], [
[KeyboardButton("No, I don't want to use it")], KeyboardButton(
app._("start_code_yes", "buttons", locale=user.locale)
)
],
[KeyboardButton(app._("start_code_no", "buttons", locale=user.locale))],
], ],
resize_keyboard=True, resize_keyboard=True,
one_time_keyboard=True, one_time_keyboard=True,
) )
await message.reply_text( await message.reply_text(
f" You have started the bot by the link containing a location **{location.name}**.\n\nPlease, confirm whether you want to use it as your location.", app._("start_code", "messages", locale=user.locale).format(
name=location.name
),
reply_markup=keyboard, reply_markup=keyboard,
) )
@ -50,24 +56,24 @@ async def command_start(app: PyroClient, message: Message):
if answer is None or answer.text == "/cancel": if answer is None or answer.text == "/cancel":
await message.reply_text( await message.reply_text(
"Cancelled.", reply_markup=ReplyKeyboardRemove() app._("cancelled", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(),
) )
return return
if answer.text not in [ if answer.text not in app.in_all_locales(
"Yes, I want to use it", "start_code_yes", "buttons"
"No, I don't want to use it", ) + app.in_all_locales("start_code_no", "buttons"):
]:
await answer.reply_text( await answer.reply_text(
"Please, select a valid location using keyboard provided. Use /cancel if you want to cancel this operation." app._("selection_invalid", "messages", locale=user.locale).format(
cancel_notice=app._("cancel", "messages", locale=user.locale)
)
) )
continue continue
if answer.text in [ if answer.text in app.in_all_locales("start_code_no", "buttons"):
"No, I don't want to use it",
]:
await answer.reply_text( await answer.reply_text(
"Alright, you're on your own now. Please, use the command /setup to configure your location and start receiving reminders.", app._("start_selection_no", "messages", locale=user.locale),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )
return return
@ -80,16 +86,24 @@ async def command_start(app: PyroClient, message: Message):
app._("time", "formats", locale=user.locale) app._("time", "formats", locale=user.locale)
) )
await answer.reply_text( await answer.reply_text(
f"✅ Finished! Your location is now **{location.name}**. You will receive reminders about garbage collection {user.offset} d. in advance at {user_time}.\n\nPlease, visit /help if you want to know how to change notifications time or disable them.", app._("start_selection_yes", "messages", locale=user.locale).format(
name=location.name, offset=user.offset, time=user_time
),
reply_markup=ReplyKeyboardRemove(), reply_markup=ReplyKeyboardRemove(),
) )
return return
if user.location is None: if user.location is None:
await message.reply_text( await message.reply_text(
"📍 Let's configure your location. Press the button on pop-up keyboard to start the process.", app._("start_configure", "messages", locale=user.locale),
reply_markup=ReplyKeyboardMarkup( reply_markup=ReplyKeyboardMarkup(
[[KeyboardButton(app._("configure", "buttons", locale=user.locale))]], [
[
KeyboardButton(
app._("start_configure", "buttons", locale=user.locale)
)
]
],
resize_keyboard=True, resize_keyboard=True,
one_time_keyboard=True, one_time_keyboard=True,
), ),

View File

@ -15,15 +15,26 @@ async def command_toggle(app: PyroClient, message: Message):
await user.update_state(not user.enabled) await user.update_state(not user.enabled)
if user.enabled: if user.enabled:
await message.reply_text("Notifications have been disabled.") await message.reply_text(
app._("toggle_disabled", "messages", locale=user.locale)
)
return return
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime("%H:%M")
if user.location is None: if user.location is None:
await message.reply_text( await message.reply_text(
f"Notifications have been enabled {user.offset} d. before garbage collection at {datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime('%H:%M')}. Use /setup to select your location." app._("toggle_enabled", "messages", locale=user.locale).format(
offset=user.offset,
time=user_time,
),
) )
return return
await message.reply_text( await message.reply_text(
f"Notifications have been enabled {user.offset} d. before garbage collection at {datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime('%H:%M')} at the **{user.location.name}**." app._("toggle_enabled_location", "messages", locale=user.locale).format(
offset=user.offset,
time=user_time,
name=user.location.name,
)
) )

View File

@ -17,7 +17,7 @@ async def command_upcoming(app: PyroClient, message: Message):
if user.location is None: if user.location is None:
await message.reply_text( await message.reply_text(
"You have no location set. Use /setup to select your location." app._("upcoming_empty_location", "messages", locale=user.locale)
) )
return return
@ -52,8 +52,12 @@ async def command_upcoming(app: PyroClient, message: Message):
if not entries: if not entries:
await message.reply_text( await message.reply_text(
f"No garbage collection entries found for the next 30 days at **{user.location.name}**" app._("upcoming_empty", "messages", locale=user.locale).format(
name=user.location.name
)
) )
return return
await message.reply_text(f"Upcoming garbage collection:\n\n{entries_text}") await message.reply_text(
app._("upcoming", "messages", locale=user.locale).format(entries=entries_text)
)