Database changes, new translations #32
20
classes/importer/abstract.py
Normal file
20
classes/importer/abstract.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
class Importer(ABC):
|
||||||
|
"""
|
||||||
|
The Importer class represents the object with
|
||||||
|
functionality to import/export garbage collection
|
||||||
|
records and convert them to other object types.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def import_data(self, data: Any) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def export_data(self, data: Any) -> None:
|
||||||
|
pass
|
64
classes/importer/csv.py
Normal file
64
classes/importer/csv.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from codecs import decode
|
||||||
|
from csv import DictReader
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
|
||||||
|
from classes.importer.abstract import Importer
|
||||||
|
from modules.database_api import col_entries
|
||||||
|
|
||||||
|
|
||||||
|
class ImporterCSV(Importer):
|
||||||
|
"""
|
||||||
|
The ImporterCSV class represents the object with
|
||||||
|
functionality to import/export garbage collection
|
||||||
|
records and convert them to other object types
|
||||||
|
from CSV files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Importer, self).__init__()
|
||||||
|
|
||||||
|
async def import_data(self, data: bytes) -> List[ObjectId]:
|
||||||
|
entries: List[Dict[str, Any]] = list(
|
||||||
|
DictReader(decode(data).split("\n"), delimiter=";")
|
||||||
|
)
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
entry["locations"] = (
|
||||||
|
[int(entry["locations"])]
|
||||||
|
if "," not in entry["locations"]
|
||||||
|
else [int(id) for id in entry["locations"].split(",")]
|
||||||
|
)
|
||||||
|
entry["garbage_type"] = int(entry["garbage_type"])
|
||||||
|
|
||||||
|
for key in ("locations", "garbage_type", "date"):
|
||||||
|
if (
|
||||||
|
key not in entry
|
||||||
|
or (key == "garbage_type" and not isinstance(entry[key], int))
|
||||||
|
or (key == "locations" and not isinstance(entry[key], list))
|
||||||
|
):
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if key == "date":
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(str(entry[key]))
|
||||||
|
except (ValueError, TypeError) as exc:
|
||||||
|
raise ValueError from exc
|
||||||
|
|
||||||
|
entries_clean: List[Dict[str, Union[str, int, datetime]]] = [
|
||||||
|
{
|
||||||
|
"locations": entry["locations"],
|
||||||
|
"garbage_type": entry["garbage_type"],
|
||||||
|
"date": datetime.fromisoformat(str(entry["date"])),
|
||||||
|
}
|
||||||
|
for entry in entries
|
||||||
|
]
|
||||||
|
|
||||||
|
inserted = await col_entries.insert_many(entries_clean)
|
||||||
|
|
||||||
|
return [] if inserted is None else inserted.inserted_ids
|
||||||
|
|
||||||
|
async def export_data(self, data: Any) -> Any:
|
||||||
|
return None
|
56
classes/importer/json.py
Normal file
56
classes/importer/json.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Union
|
||||||
|
|
||||||
|
from bson import ObjectId
|
||||||
|
from ujson import loads
|
||||||
|
|
||||||
|
from classes.importer.abstract import Importer
|
||||||
|
from modules.database_api import col_entries
|
||||||
|
|
||||||
|
|
||||||
|
class ImporterJSON(Importer):
|
||||||
|
"""
|
||||||
|
The ImporterJSON class represents the object with
|
||||||
|
functionality to import/export garbage collection
|
||||||
|
records and convert them to other object types
|
||||||
|
from JSON files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super(Importer, self).__init__()
|
||||||
|
|
||||||
|
async def import_data(self, data: bytes) -> List[ObjectId]:
|
||||||
|
entries: List[Dict[str, Any]] = loads(data)
|
||||||
|
|
||||||
|
for entry in entries:
|
||||||
|
for key in ("locations", "garbage_type", "date"):
|
||||||
|
if (
|
||||||
|
key not in entry
|
||||||
|
or (key == "garbage_type" and not isinstance(entry[key], int))
|
||||||
|
or (key == "locations" and not isinstance(entry[key], list))
|
||||||
|
):
|
||||||
|
print("keys", entry)
|
||||||
|
raise ValueError
|
||||||
|
|
||||||
|
if key == "date":
|
||||||
|
try:
|
||||||
|
datetime.fromisoformat(str(entry[key]))
|
||||||
|
except (ValueError, TypeError) as exc:
|
||||||
|
print("date", entry)
|
||||||
|
raise ValueError from exc
|
||||||
|
|
||||||
|
entries_clean: List[Dict[str, Union[str, int, datetime]]] = [
|
||||||
|
{
|
||||||
|
"locations": entry["locations"],
|
||||||
|
"garbage_type": entry["garbage_type"],
|
||||||
|
"date": datetime.fromisoformat(str(entry["date"])),
|
||||||
|
}
|
||||||
|
for entry in entries
|
||||||
|
]
|
||||||
|
|
||||||
|
inserted = await col_entries.insert_many(entries_clean)
|
||||||
|
|
||||||
|
return [] if inserted is None else inserted.inserted_ids
|
||||||
|
|
||||||
|
async def export_data(self, data: Any) -> Any:
|
||||||
|
return None
|
@ -1,9 +1,12 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
from pytz import timezone as pytz_timezone
|
||||||
|
from pytz.tzinfo import BaseTzInfo, DstTzInfo
|
||||||
|
|
||||||
from classes.point import Point
|
from classes.point import Point
|
||||||
from modules.database import col_locations
|
from modules.database_api import col_locations
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -22,7 +25,7 @@ class Location:
|
|||||||
name: str
|
name: str
|
||||||
location: Point
|
location: Point
|
||||||
country: int
|
country: int
|
||||||
timezone: str
|
timezone: Union[BaseTzInfo, DstTzInfo]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get(cls, id: int):
|
async def get(cls, id: int):
|
||||||
@ -32,6 +35,7 @@ class Location:
|
|||||||
raise ValueError(f"No location with ID {id} found.")
|
raise ValueError(f"No location with ID {id} found.")
|
||||||
|
|
||||||
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
||||||
|
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
|
||||||
|
|
||||||
return cls(**db_entry)
|
return cls(**db_entry)
|
||||||
|
|
||||||
@ -43,6 +47,7 @@ class Location:
|
|||||||
raise ValueError(f"No location with name {name} found.")
|
raise ValueError(f"No location with name {name} found.")
|
||||||
|
|
||||||
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
||||||
|
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
|
||||||
|
|
||||||
return cls(**db_entry)
|
return cls(**db_entry)
|
||||||
|
|
||||||
@ -54,5 +59,6 @@ class Location:
|
|||||||
raise ValueError(f"No location near {lat}, {lon} found.")
|
raise ValueError(f"No location near {lat}, {lon} found.")
|
||||||
|
|
||||||
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
db_entry["location"] = Point(*db_entry["location"]) # type: ignore
|
||||||
|
db_entry["timezone"] = pytz_timezone(db_entry["timezone"]) # type: ignore
|
||||||
|
|
||||||
return cls(**db_entry)
|
return cls(**db_entry)
|
||||||
|
@ -7,7 +7,7 @@ from pyrogram.types import User
|
|||||||
|
|
||||||
from classes.location import Location
|
from classes.location import Location
|
||||||
from classes.pyrouser import PyroUser
|
from classes.pyrouser import PyroUser
|
||||||
from modules.database import col_locations
|
from modules.database_api import col_locations
|
||||||
from modules.reminder import remind
|
from modules.reminder import remind
|
||||||
|
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ class PyroClient(LibPyroClient):
|
|||||||
self.scheduler.add_job(
|
self.scheduler.add_job(
|
||||||
remind, CronTrigger.from_crontab("* * * * *"), args=(self,)
|
remind, CronTrigger.from_crontab("* * * * *"), args=(self,)
|
||||||
)
|
)
|
||||||
|
self.contexts = []
|
||||||
|
|
||||||
async def start(self, **kwargs):
|
async def start(self, **kwargs):
|
||||||
await col_locations.create_index(
|
await col_locations.create_index(
|
||||||
|
@ -71,6 +71,15 @@ class PyroUser:
|
|||||||
|
|
||||||
return cls(**db_entry)
|
return cls(**db_entry)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def from_dict(cls, **kwargs):
|
||||||
|
if "location" in kwargs:
|
||||||
|
try:
|
||||||
|
kwargs["location"] = await Location.get(kwargs["location"]) # type: ignore
|
||||||
|
except ValueError:
|
||||||
|
kwargs["location"] = None # type: ignore
|
||||||
|
return cls(**kwargs)
|
||||||
|
|
||||||
async def update_locale(self, locale: Union[str, None]) -> None:
|
async def update_locale(self, locale: Union[str, None]) -> None:
|
||||||
"""Change user's locale stored in the database.
|
"""Change user's locale stored in the database.
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"locale": "en",
|
"locale": "en",
|
||||||
|
"debug": false,
|
||||||
"bot": {
|
"bot": {
|
||||||
"owner": 0,
|
"owner": 0,
|
||||||
"api_id": 0,
|
"api_id": 0,
|
||||||
@ -13,7 +14,14 @@
|
|||||||
"password": null,
|
"password": null,
|
||||||
"host": "127.0.0.1",
|
"host": "127.0.0.1",
|
||||||
"port": 27017,
|
"port": 27017,
|
||||||
"name": "garbagebot"
|
"name": "garbage_bot"
|
||||||
|
},
|
||||||
|
"database_api": {
|
||||||
|
"user": null,
|
||||||
|
"password": null,
|
||||||
|
"host": "127.0.0.1",
|
||||||
|
"port": 27017,
|
||||||
|
"name": "garbage_reminder"
|
||||||
},
|
},
|
||||||
"search": {
|
"search": {
|
||||||
"radius": 0.1
|
"radius": 0.1
|
||||||
|
98
locale/de.json
Normal file
98
locale/de.json
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"flag": "🇩🇪",
|
||||||
|
"name": "Deutsch",
|
||||||
|
"codes": [
|
||||||
|
"de",
|
||||||
|
"de-DE"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"formats": {
|
||||||
|
"date": "%d.%m.%Y",
|
||||||
|
"time": "%H:%M"
|
||||||
|
},
|
||||||
|
"garbage_types": {
|
||||||
|
"0": "🟤 Biotonne",
|
||||||
|
"1": "🟡 Gelber Sack",
|
||||||
|
"2": "🔵 Papiertonne",
|
||||||
|
"3": "⚫️ Restmüll",
|
||||||
|
"4": "🟢 Altglas",
|
||||||
|
"5": "❓ Unbestimmt"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"help": "Hilfemenü anzeigen",
|
||||||
|
"setup": "Standort auswählen",
|
||||||
|
"toggle": "Benachrichtigungen aktivieren/deaktivieren",
|
||||||
|
"set_time": "Benachrichtigungszeit einstellen",
|
||||||
|
"set_offset": "Benachrichtigungstage-Offset festlegen",
|
||||||
|
"upcoming": "Sammlung für die nächsten 30 Tage",
|
||||||
|
"language": "Sprache der Botnachrichten ändern",
|
||||||
|
"checkout": "Benutzerdaten exportieren oder löschen",
|
||||||
|
"import": "Hochladen von JSON in die Datenbank",
|
||||||
|
"shutdown": "Bot ausschalten",
|
||||||
|
"remove_commands": "Alle Kommandos abmelden"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"cancel": "Verwenden Sie /cancel, wenn Sie diesen Vorgang abbrechen möchten.",
|
||||||
|
"cancelled": "Die Operation wurde abgebrochen.",
|
||||||
|
"checkout_deleted": "🗑️ Ihre Daten wurden gelöscht. Wenn Sie diesen Bot wieder benutzen wollen, verwenden Sie bitte den Befehl /setup. Andernfalls löschen/blockieren Sie den Bot und interagieren Sie nicht mehr mit ihm.",
|
||||||
|
"checkout_deletion": "Prima. Bitte bestätigen Sie, dass Sie Ihre Daten aus dem Bot löschen möchten.\n\nDie folgenden Daten werden gelöscht:\n• Gewählter Sammlungsort\n• Bevorzugte Sprache für alle Nachrichten\n• Zeit der Benachrichtigungen\n• Offset der Benachrichtigungen\n\nVerwenden Sie die Tastatur, um den Vorgang zu bestätigen und fortzusetzen oder /cancel, um ihn abzubrechen.",
|
||||||
|
"checkout": "Das sind alle Daten, die der Bot hat. Bitte verwenden Sie die Tastatur, um zu wählen, ob Sie Ihre Daten aus dem Bot löschen möchten.",
|
||||||
|
"commands_removed": "✅ Alle derzeit registrierten Kommandos wurden unregistriert. Die Kommandos werden beim Start des Bots erneut registriert.",
|
||||||
|
"help": "🔔 Dieser Bot sendet Ihnen Benachrichtigungen über die Müllabfuhr nach Ihrem lokalen Zeitplan.\n\n**Verfügbare Kommandos**\n/help - Diese Menü anzeigen\n/setup - Standort wählen\n/toggle - Deaktivieren/Aktivieren der Benachrichtigungen\n/set_time - Einstellen der Erinnerungszeit\n/set_offset - Offset zwischen Erinnerung und Abholung einstellen\n/upcoming - Zeigt die nächste Abholung an\n/language - Die Sprache des Bots wählen\n/checkout - Daten exportieren oder löschen\n\n💭 Sie können auch vorschlagen, Ihre Stadt/Ihren Bezirk in den Bot aufzunehmen, indem Sie die Administratoren über [diesen Link]({url_contact}) kontaktieren und Ihren Zeitplan angeben.\n\n⚙️ Möchten Sie diesen Bot selbst hosten oder Änderungen vornehmen? Er ist Open-Source, Sie können ihn also forken. Werfen Sie einen Blick auf das [Bot-Repository]({url_repo}) für Details.\n\nViel Spaß beim Benutzen! 🤗",
|
||||||
|
"import_finished": "Sie haben erfolgreich {count} Einträge eingefügt.",
|
||||||
|
"import_invalid_date": "Die Einträge enthalten ungültige Datumsformate. Verwenden Sie das Datumsformat **ISO 8601**.",
|
||||||
|
"import_invalid_filetype": "Ungültige Eingabe. Bitte senden Sie mir eine JSON- oder CSV-Datei mit Einträgen. {cancel_notice}",
|
||||||
|
"import_invalid": "Dies ist keine gültige Abfalltermine-Datei.",
|
||||||
|
"import": "Okay. Senden Sie mir eine gültige Datei. Sie kann im JSON- oder CSV-Format sein. Lesen Sie mehr über die unterstützten Formate in der Dokumentation",
|
||||||
|
"locale_choice": "Prima. Bitte wählen Sie die Sprache mit der Tastatur unten.",
|
||||||
|
"location_empty": "Sie haben keinen Standort festgelegt. Verwenden Sie /setup, um Ihren Standort auszuwählen.",
|
||||||
|
"location_name_empty": "Es konnten keine Orte mit diesem Namen gefunden werden. Versuchen Sie, ihn umzuformulieren, oder stellen Sie sicher, dass Sie dieselbe Sprache und denselben Namen verwenden, wie er von Ihren örtlichen Behörden im Müllabfuhrplan angegeben ist.\n\n{cancel_notice}",
|
||||||
|
"location_name_invalid": "Bitte senden Sie den Namen des Ortes als Text. {cancel_notice}",
|
||||||
|
"location_name": "Bitte senden Sie mir einen Standortnamen. Es sollte der Name sein, der im Müllabfuhrplan Ihrer örtlichen Behörde verwendet wird. In der Regel ist dies der Name des Bezirks oder sogar der Stadt selbst.",
|
||||||
|
"location_select": "Wählen Sie den Ort über die Tastatur aus.",
|
||||||
|
"reminder": "**Müllabfuhr**\n\nTyp: {Typ}\nDatum: {Datum}\n\nVergessen Sie nicht, Ihre Tonne für die Abholung vorzubereiten!",
|
||||||
|
"search_nearby_empty": "Es konnten keine Orte in der Nähe gefunden werden. Versuchen wir es mit der Namenssuche.",
|
||||||
|
"selection_invalid": "Bitte wählen Sie eine gültige Option über die Tastatur aus. {cancel_notice}",
|
||||||
|
"set_offset_finished": "🔔 Benachrichtigungen Offset wurde aktualisiert! Sie erhalten nun eine Benachrichtigung über die Abholung **{offset} T.** vor der Abholung um {time}. {toggle_notice}",
|
||||||
|
"set_offset_invalid": "Bitte geben Sie eine gültige ganzzahlige Anzahl von Tagen im Bereich von 0 bis 7 (einschließlich) an. {cancel_notice}",
|
||||||
|
"set_offset": "Okay. Bitte geben Sie an, wie viele Tage im Voraus Sie eine Benachrichtigung über die Abholung erhalten möchten.",
|
||||||
|
"set_time_finished": "🔔 Die Benachrichtigungszeit wurde aktualisiert! Sie erhalten nun eine Benachrichtigung über die Abholung {offset} T. vor der Abholung um **{time}**. {toggle_notice}",
|
||||||
|
"set_time_invalid": "Bitte geben Sie eine gültige Uhrzeit im Format SS:MM an. {cancel_notice}",
|
||||||
|
"set_time": "Okay. Bitte senden Sie die gewünschte Zeit im Format SS:MM.",
|
||||||
|
"setup_finished": "✅ Fertig! Ihr Standort ist jetzt **{name}**. Sie werden die Benachrichtigungen über die Müllabfuhr {offset} T. im Voraus um {time} erhalten.",
|
||||||
|
"setup_retry": "ℹ️ Wenn Sie versuchen möchten, den Speicherort erneut auszuwählen, verwenden Sie den Kommando /setup.",
|
||||||
|
"setup": "⚙️ Beginnen wir die Konfiguration mit der Suche nach Ihrem Standort.\n\nBitte wählen Sie aus, ob Sie unter den Standorten in Ihrer Nähe suchen möchten oder direkt zur Suche nach dem Standortnamen übergehen wollen.\n\nBeachten Sie, dass der von Ihnen gesendete Standort **NICHT** irgendwo gespeichert wird und nur für die Standortsuche in der Datenbank verwendet wird.",
|
||||||
|
"start_code_invalid": "🚫 Sie haben den Bot über den Link gestartet, der einen Ort enthält, aber es scheint kein gültiger zu sein. Bitte verwenden Sie den Kommando /setup, um den Standort manuell zu konfigurieren.",
|
||||||
|
"start_code": "ℹ️ Sie haben den Bot über den Link gestartet, der einen Ort **{name}** enthält.\n\nBitte bestätigen Sie, ob Sie diesen als Ihren Standort verwenden möchten.",
|
||||||
|
"start_configure": "📍 Lassen Sie uns Ihren Standort konfigurieren. Drücken Sie die Taste auf der Pop-up-Tastatur, um den Vorgang zu starten.",
|
||||||
|
"start_selection_no": "Gut, Sie sind jetzt auf sich allein gestellt. Bitte verwenden Sie den Kommando /setup, um Ihren Standort zu konfigurieren und Erinnerungen zu erhalten.",
|
||||||
|
"start_selection_yes": "✅ Fertig! Ihr Standort ist jetzt **{name}**. Sie erhalten Erinnerungen an die Müllabfuhr {offset} T. im Voraus um {time}.\n\nBitte besuchen Sie /help Menü, wenn Sie wissen möchten, wie Sie die Zeit der Benachrichtigungen ändern oder sie deaktivieren können.",
|
||||||
|
"start": "👋 Herzlich willkommen!\n\nDieser kleine Open-Source-Bot soll Ihnen das Leben etwas erleichtern, indem er Sie über die nächste Müllabfuhr in Ihrer Nähe informiert.\n\nDurch die Nutzung dieses Bots akzeptieren Sie die [Datenschutzbestimmungen]({privacy_policy}), andernfalls blockieren und entfernen Sie diesen Bot bitte vor weiterer Interaktion.\n\nNun ist der offizielle Teil vorbei und Sie können sich mit dem Bot beschäftigen.",
|
||||||
|
"toggle_disabled": "🔕 Die Benachrichtigungen wurden deaktiviert.",
|
||||||
|
"toggle_enabled_location": "🔔 Benachrichtigungen wurden aktiviert {offset} T. vor der Sammlung um {time} am **{name}**.",
|
||||||
|
"toggle_enabled": "🔔 Benachrichtigungen wurden aktiviert {offset} T. vor der Sammlung um {time}. Verwenden Sie /setup, um Ihren Standort auszuwählen.",
|
||||||
|
"toggle": "Führen Sie /toggle aus, um Benachrichtigungen zu aktivieren.",
|
||||||
|
"upcoming_empty": "Keine Müllabfuhr-Einträge für die nächsten 30 Tage bei **{name}** gefunden",
|
||||||
|
"upcoming": "Bevorstehende Müllabfuhr:\n\n{entries}"
|
||||||
|
},
|
||||||
|
"force_replies": {
|
||||||
|
"import": "JSON mit Abfalltermine",
|
||||||
|
"location_name": "Ortsname",
|
||||||
|
"set_offset": "Anzahl der Tage",
|
||||||
|
"set_time": "Uhrzeit als SS:MM"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"delete_confirm": "Ich stimme zu und möchte fortfahren",
|
||||||
|
"delete_no": "❌ Nein, ich möchte sie nicht löschen",
|
||||||
|
"delete_yes": "✅ Ja, ich möchte sie löschen",
|
||||||
|
"search_name": "🔎 Suche nach Ortsnamen",
|
||||||
|
"search_nearby": "📍 Suche in der Nähe",
|
||||||
|
"start_code_no": "❌ Nein, ich will es nicht benutzen",
|
||||||
|
"start_code_yes": "✅ Ja, ich möchte es benutzen",
|
||||||
|
"start_configure": "⚙️ Konfigurieren wir den Bot"
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"locale_set": "Ihre Sprache ist jetzt: {locale}"
|
||||||
|
}
|
||||||
|
}
|
@ -42,10 +42,11 @@
|
|||||||
"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! 🤗",
|
"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_finished": "You have successfully inserted {count} entries.",
|
||||||
"import_invalid_date": "Entries contain invalid date formats. Use **ISO 8601** date format.",
|
"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_filetype": "Invalid input. Please, send me a JSON or CSV file with entries. {cancel_notice}",
|
||||||
"import_invalid": "This is not a valid garbage collection JSON.",
|
"import_invalid": "This is not a valid garbage collection file.",
|
||||||
"import": "Alright. Send me a valid JSON.",
|
"import": "Alright. Send me a valid file. It can be in JSON or CSV format. Read more about supported formats in the documentation",
|
||||||
"locale_choice": "Alright. Please choose the language using keyboard below.",
|
"locale_choice": "Alright. Please choose the language using keyboard below.",
|
||||||
|
"location_empty": "You have no location set. Use /setup to select your location first.",
|
||||||
"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_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_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_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.",
|
||||||
@ -72,7 +73,6 @@
|
|||||||
"toggle_enabled_location": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time} at the **{name}**.",
|
"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_enabled": "🔔 Notifications have been enabled {offset} d. before garbage collection at {time}. Use /setup to select your location.",
|
||||||
"toggle": "Execute /toggle to enable notifications.",
|
"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_empty": "No garbage collection entries found for the next 30 days at **{name}**",
|
||||||
"upcoming": "Upcoming garbage collection:\n\n{entries}"
|
"upcoming": "Upcoming garbage collection:\n\n{entries}"
|
||||||
},
|
},
|
||||||
|
98
locale/ru.json
Normal file
98
locale/ru.json
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
{
|
||||||
|
"metadata": {
|
||||||
|
"codes": [
|
||||||
|
"ru",
|
||||||
|
"ru-RU"
|
||||||
|
],
|
||||||
|
"flag": "🇺🇦",
|
||||||
|
"name": "Російська"
|
||||||
|
},
|
||||||
|
"messages": {
|
||||||
|
"cancelled": "Операцію скасовано.",
|
||||||
|
"setup": "⚙️ Почнемо налаштування з пошуку Вашого місцезнаходження.\n\nБудь ласка, виберіть, чи хочете Ви шукати серед найближчих до Вас локацій, чи одразу перейти до пошуку за назвою.\n\nЗверніть увагу, що надіслане Вами місцезнаходження **НЕ** зберігається ніде і використовується лише для пошуку місць поряд в базі даних.",
|
||||||
|
"cancel": "Якщо Ви хочете скасувати цю операцію, використовуйте /cancel.",
|
||||||
|
"checkout_deleted": "🗑️ Ваші дані було видалено. Якщо Ви хочете знову почати користуватися цим ботом, скористайтеся командою /setup. В іншому випадку видаліть/заблокуйте бота і більше не взаємодійте з ним.",
|
||||||
|
"checkout_deletion": "Гаразд. Будь ласка, підтвердіть, що Ви хочете видалити свої дані з бота.\n\nНаступні дані будуть видалені:\n• Вибране місце\n• Бажана мова всіх повідомлень\n• Час сповіщень\n• Зсув сповіщень\n\nВикористовуйте клавіатуру, щоб підтвердити й продовжити або /cancel, щоб перервати цю операцію.",
|
||||||
|
"checkout": "Це фактично всі дані, які має бот. Будь ласка, використовуйте ці кнопки, щоб вибрати, чи хочете Ви видалити свої дані з бота.",
|
||||||
|
"commands_removed": "✅ Всі зареєстровані на цю мить команди було видалено. Команди будуть зареєстровані знову при запуску бота.",
|
||||||
|
"help": "🔔 Цей бот надсилає сповіщення про вивезення сміття згідно з Вашим місцевим графіком.\n\n**Доступні команди**\n/help - Показати це повідомлення\n/setup - Вибрати місце розташування\n/toggle - Увімкнути/вимкнути нагадування\n/set_time - Встановити час нагадувань\n/set_offset - Встановити зсув між нагадуваннями та вивозом\n/upcoming - Показати майбутні вивезення\n/language - Обрати мову бота\n/checkout - Експортувати або видалити дані\n\nВи також можете запропонувати додати своє місто/район до бота, зв'язавшись з адміністраторами за [цим посиланням]({url_contact}) та вказавши свій розклад.\n\n⚙️ Бажаєте розмістити цього бота самостійно або внести деякі зміни? Бот має відкритий вихідний код, тож ви можете форкнути його. Ознайомтесь із [репозиторієм бота]({url_repo}) щоб дізнатись деталі.\n\nПриємного користування! 🤗",
|
||||||
|
"import_finished": "Ви успішно вставили {count} записів.",
|
||||||
|
"import_invalid_date": "Записи містять невірні формати дат. Використовуйте формат дати **ISO 8601**.",
|
||||||
|
"import_invalid_filetype": "Неправильний ввід. Будь ласка, надішліть мені JSON або CSV файл із записами. {cancel_notice}",
|
||||||
|
"import_invalid": "Це недійсний файл з даними про збір сміття.",
|
||||||
|
"import": "Гаразд. Надішліть мені правильний файл. Він може бути у форматі JSON або CSV. Дізнайтеся більше про підтримувані формати в документації",
|
||||||
|
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
|
||||||
|
"location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
|
||||||
|
"location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}",
|
||||||
|
"location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}",
|
||||||
|
"location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.",
|
||||||
|
"location_select": "Виберіть місце за допомогою показаної клавіатури.",
|
||||||
|
"reminder": "**Вивіз сміття**\n\nТип: {type}\nДата: {date}\n\nНе забудьте підготувати свій контейнер до збору!",
|
||||||
|
"search_nearby_empty": "Не вдалося знайти жодної локації поблизу. Спробуємо скористатися пошуком за назвою.",
|
||||||
|
"selection_invalid": "Будь ласка, виберіть правильний варіант за допомогою клавіатури. {cancel_notice}",
|
||||||
|
"set_offset_finished": "🔔 Час сповіщень було оновлено! Тепер Ви будете отримувати сповіщення про вивіз сміття за **{offset} д.** до вивозу о {time}. {toggle_notice}",
|
||||||
|
"set_offset_invalid": "Будь ласка, вкажіть дійсну цілу кількість днів у діапазоні від 0 до 7 (включно). {cancel_notice}",
|
||||||
|
"set_offset": "Гаразд. Будь ласка, напишіть, за скільки днів до збору Ви хочете отримати сповіщення про збір.",
|
||||||
|
"set_time_finished": "🔔 Час сповіщень було оновлено! Тепер Ви будете отримувати сповіщення про вивіз сміття за {offset} д. до вивозу о **{time}**. {toggle_notice}",
|
||||||
|
"set_time_invalid": "Будь ласка, вкажіть дійсний час у форматі ГГ:ХХ. {cancel_notice}",
|
||||||
|
"set_time": "Гаразд. Будь ласка, надішліть бажаний час у форматі ГГ:ХХ.",
|
||||||
|
"setup_finished": "✅ Готово! Ваше місцезнаходження тепер **{name}**. Ви будете отримувати сповіщення про вивезення сміття за {offset} д. заздалегідь о {time}.",
|
||||||
|
"setup_retry": "ℹ️ Якщо Ви захочете вибрати місце розташування, скористайтеся командою /setup.",
|
||||||
|
"start_code_invalid": "🚫 Ви запустили бота за посиланням, що містить локацію, але, схоже, вона не є дійсною. Будь ласка, скористайтеся командою /setup, щоб налаштувати локацію вручну.",
|
||||||
|
"start_code": "ℹ️ Ви запустили бота за посиланням, що містить локацію **{name}**.\n\nБудь ласка, підтвердіть, чи хочете Ви використовувати її як свою локацію для сповіщень.",
|
||||||
|
"start_configure": "📍 Налаштуймо Вашу локацію. Натисніть кнопку на показаній клавіатурі, щоб почати процес.",
|
||||||
|
"start_selection_no": "Гаразд, тепер Ви самі по собі. Будь ласка, скористайтеся командою /setup, щоб налаштувати своє місцезнаходження і почати отримувати нагадування.",
|
||||||
|
"start_selection_yes": "✅ Готово! Ваша локація тепер **{name}**. Ви будете отримувати нагадування про вивезення сміття за {offset} д. заздалегідь о {time}.\n\nБудь ласка, скористайтесь /help, якщо Ви хочете дізнатися, як змінити час сповіщень або вимкнути їх.",
|
||||||
|
"start": "👋 Вітання!\n\nЦей невеличкий бот з відкритим вихідним кодом створений для того, щоб трохи спростити Вам життя, надсилаючи сповіщення про вивіз сміття у вашому регіоні.\n\nКористуючись цим ботом, Ви приймаєте [Політику конфіденційності]({privacy_policy}), в іншому випадку, будь ласка, заблокуйте та видаліть цього бота перед подальшою взаємодією.\n\nТепер офіційна частина закінчена, тож Ви можете зануритися в бота.",
|
||||||
|
"toggle_disabled": "🔕 Сповіщення було вимкнено.",
|
||||||
|
"toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.",
|
||||||
|
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
|
||||||
|
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
|
||||||
|
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
|
||||||
|
"upcoming": "Найближчі вивози сміття:\n\n{entries}"
|
||||||
|
},
|
||||||
|
"formats": {
|
||||||
|
"date": "%d.%m.%Y",
|
||||||
|
"time": "%H:%M"
|
||||||
|
},
|
||||||
|
"garbage_types": {
|
||||||
|
"0": "🟤 Біо",
|
||||||
|
"1": "🟡 Пластик",
|
||||||
|
"2": "🔵 Папір",
|
||||||
|
"3": "⚫️ Загальне",
|
||||||
|
"4": "🟢 Скло",
|
||||||
|
"5": "❓Невизначене"
|
||||||
|
},
|
||||||
|
"commands": {
|
||||||
|
"help": "Показати меню допомоги",
|
||||||
|
"setup": "Обрати місце розташування",
|
||||||
|
"toggle": "Увімкнути/вимкнути сповіщення",
|
||||||
|
"set_time": "Встановити час сповіщень",
|
||||||
|
"set_offset": "Встановити зміщення сповіщень",
|
||||||
|
"upcoming": "Дати збору на наступні 30 днів",
|
||||||
|
"language": "Змінити мову повідомлень бота",
|
||||||
|
"checkout": "Експортувати або видалити дані",
|
||||||
|
"import": "Завантажити з JSON до бази даних",
|
||||||
|
"shutdown": "Вимкнути бота",
|
||||||
|
"remove_commands": "Видалити всі команди"
|
||||||
|
},
|
||||||
|
"buttons": {
|
||||||
|
"delete_confirm": "Я погоджуюсь і хочу продовжити",
|
||||||
|
"delete_no": "❌ Ні, я не хочу видаляти їх",
|
||||||
|
"delete_yes": "✅ Так, я хочу видалити їх",
|
||||||
|
"search_name": "🔎 Пошук за назвою місця",
|
||||||
|
"search_nearby": "📍 Пошук найближчих локацій",
|
||||||
|
"start_code_no": "❌ Ні, я не хочу її використовувати",
|
||||||
|
"start_code_yes": "✅ Так, я хочу її використовувати",
|
||||||
|
"start_configure": "⚙️ Налаштуймо бота"
|
||||||
|
},
|
||||||
|
"callbacks": {
|
||||||
|
"locale_set": "Встановлено мову: {locale}"
|
||||||
|
},
|
||||||
|
"force_replies": {
|
||||||
|
"import": "JSON із записами вивозу сміття",
|
||||||
|
"location_name": "Назва локації",
|
||||||
|
"set_offset": "Кількість днів",
|
||||||
|
"set_time": "Час у вигляді ГГ:ХХ"
|
||||||
|
}
|
||||||
|
}
|
@ -21,27 +21,78 @@
|
|||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
"help": "Показати меню допомоги",
|
"help": "Показати меню допомоги",
|
||||||
"setup": "Обрати місто та район",
|
"setup": "Обрати місце розташування",
|
||||||
"toggle": "Вимкнути/вимкнути повідомлення",
|
"toggle": "Увімкнути/вимкнути сповіщення",
|
||||||
"set_time": "Встановити час повідомлень",
|
"set_time": "Встановити час сповіщень",
|
||||||
"set_offset": "Встановити дні випередження",
|
"set_offset": "Встановити зміщення сповіщень",
|
||||||
"upcoming": "Дати збирання на наступні 30 днів",
|
"upcoming": "Дати збору на наступні 30 днів",
|
||||||
"language": "Змінити мову повідомлень бота",
|
"language": "Змінити мову повідомлень бота",
|
||||||
"checkout": "Експортувати чи видалити дані",
|
"checkout": "Експортувати або видалити дані",
|
||||||
"import": "Завантажити з JSON до бази даних",
|
"import": "Завантажити з JSON до бази даних",
|
||||||
"shutdown": "Вимкнути бота",
|
"shutdown": "Вимкнути бота",
|
||||||
"remove_commands": "Видалити всі команди"
|
"remove_commands": "Видалити всі команди"
|
||||||
},
|
},
|
||||||
"messages": {
|
"messages": {
|
||||||
"help": "Привіт! Я твій бот!",
|
"cancel": "Якщо Ви хочете скасувати цю операцію, використовуйте /cancel.",
|
||||||
|
"cancelled": "Операцію скасовано.",
|
||||||
|
"checkout_deleted": "🗑️ Ваші дані було видалено. Якщо Ви хочете знову почати користуватися цим ботом, скористайтеся командою /setup. В іншому випадку видаліть/заблокуйте бота і більше не взаємодійте з ним.",
|
||||||
|
"checkout_deletion": "Гаразд. Будь ласка, підтвердіть, що Ви хочете видалити свої дані з бота.\n\nНаступні дані будуть видалені:\n• Вибране місце\n• Бажана мова всіх повідомлень\n• Час сповіщень\n• Зсув сповіщень\n\nВикористовуйте клавіатуру, щоб підтвердити й продовжити або /cancel, щоб перервати цю операцію.",
|
||||||
|
"checkout": "Це фактично всі дані, які має бот. Будь ласка, використовуйте ці кнопки, щоб вибрати, чи хочете Ви видалити свої дані з бота.",
|
||||||
|
"commands_removed": "✅ Всі зареєстровані на цю мить команди було видалено. Команди будуть зареєстровані знову при запуску бота.",
|
||||||
|
"help": "🔔 Цей бот надсилає сповіщення про вивезення сміття згідно з Вашим місцевим графіком.\n\n**Доступні команди**\n/help - Показати це повідомлення\n/setup - Вибрати місце розташування\n/toggle - Увімкнути/вимкнути нагадування\n/set_time - Встановити час нагадувань\n/set_offset - Встановити зсув між нагадуваннями та вивозом\n/upcoming - Показати майбутні вивезення\n/language - Обрати мову бота\n/checkout - Експортувати або видалити дані\n\nВи також можете запропонувати додати своє місто/район до бота, зв'язавшись з адміністраторами за [цим посиланням]({url_contact}) та вказавши свій розклад.\n\n⚙️ Бажаєте розмістити цього бота самостійно або внести деякі зміни? Бот має відкритий вихідний код, тож ви можете форкнути його. Ознайомтесь із [репозиторієм бота]({url_repo}) щоб дізнатись деталі.\n\nПриємного користування! 🤗",
|
||||||
|
"import_finished": "Ви успішно вставили {count} записів.",
|
||||||
|
"import_invalid_date": "Записи містять невірні формати дат. Використовуйте формат дати **ISO 8601**.",
|
||||||
|
"import_invalid_filetype": "Неправильний ввід. Будь ласка, надішліть мені JSON або CSV файл із записами. {cancel_notice}",
|
||||||
|
"import_invalid": "Це недійсний файл з даними про збір сміття.",
|
||||||
|
"import": "Гаразд. Надішліть мені правильний файл. Він може бути у форматі JSON або CSV. Дізнайтеся більше про підтримувані формати в документації",
|
||||||
|
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче.",
|
||||||
|
"location_empty": "У Вас не встановлено локацію вивозу. Оберіть свою локацію за допомогою /setup.",
|
||||||
|
"location_name_empty": "Не вдалося знайти жодного населеного пункту з такою назвою. Спробуйте перефразувати назву або переконайтеся, що Ви використовуєте ту саму мову та назву, що й місцева влада у графіку вивезення сміття.\n\n{cancel_notice}",
|
||||||
|
"location_name_invalid": "Будь ласка, надішліть назву місця у вигляді тексту. {cancel_notice}",
|
||||||
|
"location_name": "Будь ласка, надішліть мені назву населеного пункту. Це має бути назва, яка використовується у графіку вивезення сміття Вашою місцевою владою. Зазвичай це назва району або міста.",
|
||||||
|
"location_select": "Виберіть місце за допомогою показаної клавіатури.",
|
||||||
|
"reminder": "**Вивіз сміття**\n\nТип: {type}\nДата: {date}\n\nНе забудьте підготувати свій контейнер до збору!",
|
||||||
|
"search_nearby_empty": "Не вдалося знайти жодної локації поблизу. Спробуємо скористатися пошуком за назвою.",
|
||||||
|
"selection_invalid": "Будь ласка, виберіть правильний варіант за допомогою клавіатури. {cancel_notice}",
|
||||||
|
"set_offset_finished": "🔔 Час сповіщень було оновлено! Тепер Ви будете отримувати сповіщення про вивіз сміття за **{offset} д.** до вивозу о {time}. {toggle_notice}",
|
||||||
|
"set_offset_invalid": "Будь ласка, вкажіть дійсну цілу кількість днів у діапазоні від 0 до 7 (включно). {cancel_notice}",
|
||||||
|
"set_offset": "Гаразд. Будь ласка, напишіть, за скільки днів до збору Ви хочете отримати сповіщення про збір.",
|
||||||
|
"set_time_finished": "🔔 Час сповіщень було оновлено! Тепер Ви будете отримувати сповіщення про вивіз сміття за {offset} д. до вивозу о **{time}**. {toggle_notice}",
|
||||||
|
"set_time_invalid": "Будь ласка, вкажіть дійсний час у форматі ГГ:ХХ. {cancel_notice}",
|
||||||
|
"set_time": "Гаразд. Будь ласка, надішліть бажаний час у форматі ГГ:ХХ.",
|
||||||
|
"setup_finished": "✅ Готово! Ваше місцезнаходження тепер **{name}**. Ви будете отримувати сповіщення про вивезення сміття за {offset} д. заздалегідь о {time}.",
|
||||||
|
"setup_retry": "ℹ️ Якщо Ви захочете вибрати місце розташування, скористайтеся командою /setup.",
|
||||||
|
"setup": "⚙️ Почнемо налаштування з пошуку Вашого місцезнаходження.\n\nБудь ласка, виберіть, чи хочете Ви шукати серед найближчих до Вас локацій, чи одразу перейти до пошуку за назвою.\n\nЗверніть увагу, що надіслане Вами місцезнаходження **НЕ** зберігається ніде і використовується лише для пошуку місць поряд в базі даних.",
|
||||||
|
"start_code_invalid": "🚫 Ви запустили бота за посиланням, що містить локацію, але, схоже, вона не є дійсною. Будь ласка, скористайтеся командою /setup, щоб налаштувати локацію вручну.",
|
||||||
|
"start_code": "ℹ️ Ви запустили бота за посиланням, що містить локацію **{name}**.\n\nБудь ласка, підтвердіть, чи хочете Ви використовувати її як свою локацію для сповіщень.",
|
||||||
|
"start_configure": "📍 Налаштуймо Вашу локацію. Натисніть кнопку на показаній клавіатурі, щоб почати процес.",
|
||||||
|
"start_selection_no": "Гаразд, тепер Ви самі по собі. Будь ласка, скористайтеся командою /setup, щоб налаштувати своє місцезнаходження і почати отримувати нагадування.",
|
||||||
|
"start_selection_yes": "✅ Готово! Ваша локація тепер **{name}**. Ви будете отримувати нагадування про вивезення сміття за {offset} д. заздалегідь о {time}.\n\nБудь ласка, скористайтесь /help, якщо Ви хочете дізнатися, як змінити час сповіщень або вимкнути їх.",
|
||||||
"start": "👋 Вітання!\n\nЦей невеличкий бот з відкритим вихідним кодом створений для того, щоб трохи спростити Вам життя, надсилаючи сповіщення про вивіз сміття у вашому регіоні.\n\nКористуючись цим ботом, Ви приймаєте [Політику конфіденційності]({privacy_policy}), в іншому випадку, будь ласка, заблокуйте та видаліть цього бота перед подальшою взаємодією.\n\nТепер офіційна частина закінчена, тож Ви можете зануритися в бота.",
|
"start": "👋 Вітання!\n\nЦей невеличкий бот з відкритим вихідним кодом створений для того, щоб трохи спростити Вам життя, надсилаючи сповіщення про вивіз сміття у вашому регіоні.\n\nКористуючись цим ботом, Ви приймаєте [Політику конфіденційності]({privacy_policy}), в іншому випадку, будь ласка, заблокуйте та видаліть цього бота перед подальшою взаємодією.\n\nТепер офіційна частина закінчена, тож Ви можете зануритися в бота.",
|
||||||
"locale_choice": "Гаразд. Будь ласка, оберіть мову за допомогою клавіатури нижче."
|
"toggle_disabled": "🔕 Сповіщення було вимкнено.",
|
||||||
|
"toggle_enabled_location": "🔔 Сповіщення увімкнено за {offset} д. до вивезення сміття о {time} для локації **{name}**.",
|
||||||
|
"toggle_enabled": "🔔 Сповіщення було увімкнено за {offset} д. до вивезення сміття о {time}. Оберіть своє розташування за допомогою /setup.",
|
||||||
|
"toggle": "Використовуйте /toggle, щоб увімкнути сповіщення.",
|
||||||
|
"upcoming_empty": "Не знайдено записів про вивезення сміття на найближчі 30 днів для **{name}**",
|
||||||
|
"upcoming": "Найближчі вивози сміття:\n\n{entries}"
|
||||||
},
|
},
|
||||||
"buttons": {
|
"buttons": {
|
||||||
"configure": "Давайте налаштуємо бота"
|
"delete_confirm": "Я погоджуюсь і хочу продовжити",
|
||||||
|
"delete_no": "❌ Ні, я не хочу видаляти їх",
|
||||||
|
"delete_yes": "✅ Так, я хочу видалити їх",
|
||||||
|
"search_name": "🔎 Пошук за назвою місця",
|
||||||
|
"search_nearby": "📍 Пошук найближчих локацій",
|
||||||
|
"start_code_no": "❌ Ні, я не хочу її використовувати",
|
||||||
|
"start_code_yes": "✅ Так, я хочу її використовувати",
|
||||||
|
"start_configure": "⚙️ Налаштуймо бота"
|
||||||
},
|
},
|
||||||
"callbacks": {
|
"callbacks": {
|
||||||
"locale_set": "Встановлено мову: {locale}"
|
"locale_set": "Встановлено мову: {locale}"
|
||||||
},
|
},
|
||||||
"force_replies": {}
|
"force_replies": {
|
||||||
}
|
"import": "JSON із записами вивозу сміття",
|
||||||
|
"location_name": "Назва локації",
|
||||||
|
"set_offset": "Кількість днів",
|
||||||
|
"set_time": "Час у вигляді ГГ:ХХ"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2
main.py
2
main.py
@ -12,7 +12,7 @@ from modules.migrator import migrate_database
|
|||||||
from modules.scheduler import scheduler
|
from modules.scheduler import scheduler
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.DEBUG if sync.config_get("debug") else logging.INFO,
|
||||||
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
|
format="%(name)s.%(funcName)s | %(levelname)s | %(message)s",
|
||||||
datefmt="[%X]",
|
datefmt="[%X]",
|
||||||
)
|
)
|
||||||
|
@ -10,4 +10,9 @@ async def _owner_func(_, __: PyroClient, message: Message):
|
|||||||
return False if message.from_user is None else __.owner == message.from_user.id
|
return False if message.from_user is None else __.owner == message.from_user.id
|
||||||
|
|
||||||
|
|
||||||
|
async def _context_func(_, __: PyroClient, message: Message):
|
||||||
|
return message.from_user.id in __.contexts
|
||||||
|
|
||||||
|
|
||||||
owner = filters.create(_owner_func)
|
owner = filters.create(_owner_func)
|
||||||
|
context = filters.create(_context_func)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
"""Module that provides all database collections"""
|
"""Module that provides bot's database collections."""
|
||||||
|
|
||||||
from typing import Any, Mapping
|
from typing import Any, Mapping
|
||||||
|
|
||||||
@ -24,5 +24,3 @@ db_client = AsyncClient(con_string)
|
|||||||
db: AsyncDatabase = db_client.get_database(name=db_config["name"])
|
db: AsyncDatabase = db_client.get_database(name=db_config["name"])
|
||||||
|
|
||||||
col_users: AsyncCollection = db.get_collection("users")
|
col_users: AsyncCollection = db.get_collection("users")
|
||||||
col_entries: AsyncCollection = db.get_collection("entries")
|
|
||||||
col_locations: AsyncCollection = db.get_collection("locations")
|
|
||||||
|
29
modules/database_api.py
Normal file
29
modules/database_api.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
"""Module that provides API database collections.
|
||||||
|
It's possible to use REST API client instead, but
|
||||||
|
using MongoDB directly is MUCH faster this way."""
|
||||||
|
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from async_pymongo import AsyncClient, AsyncCollection, AsyncDatabase
|
||||||
|
from libbot.sync import config_get
|
||||||
|
|
||||||
|
db_config: Mapping[str, Any] = config_get("database_api")
|
||||||
|
|
||||||
|
if db_config["user"] is not None and db_config["password"] is not None:
|
||||||
|
con_string = "mongodb://{0}:{1}@{2}:{3}/{4}".format(
|
||||||
|
db_config["user"],
|
||||||
|
db_config["password"],
|
||||||
|
db_config["host"],
|
||||||
|
db_config["port"],
|
||||||
|
db_config["name"],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
con_string = "mongodb://{0}:{1}/{2}".format(
|
||||||
|
db_config["host"], db_config["port"], db_config["name"]
|
||||||
|
)
|
||||||
|
|
||||||
|
db_client = AsyncClient(con_string)
|
||||||
|
db: AsyncDatabase = db_client.get_database(name=db_config["name"])
|
||||||
|
|
||||||
|
col_entries: AsyncCollection = db.get_collection("entries")
|
||||||
|
col_locations: AsyncCollection = db.get_collection("locations")
|
@ -1,26 +1,38 @@
|
|||||||
import logging
|
import logging
|
||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
from bson import json_util
|
||||||
from libbot.pyrogram.classes import PyroClient
|
from libbot.pyrogram.classes import PyroClient
|
||||||
from pytz import timezone as pytz_timezone
|
|
||||||
|
|
||||||
from classes.enums import GarbageType
|
from classes.enums import GarbageType
|
||||||
from classes.location import Location
|
from classes.location import Location
|
||||||
from classes.pyrouser import PyroUser
|
from classes.pyrouser import PyroUser
|
||||||
from modules.database import col_entries, col_users
|
from modules.database import col_users
|
||||||
|
from modules.database_api import col_entries
|
||||||
|
from modules.utils import from_utc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def remind(app: PyroClient) -> None:
|
async def remind(app: PyroClient) -> None:
|
||||||
now = datetime.now()
|
utcnow = datetime.utcnow()
|
||||||
|
|
||||||
|
logger.debug("Performing reminder lookup for %s (UTCNOW)", utcnow)
|
||||||
|
|
||||||
users = await col_users.find(
|
users = await col_users.find(
|
||||||
{"time_hour": now.hour, "time_minute": now.minute}
|
{"time_hour": utcnow.hour, "time_minute": utcnow.minute}
|
||||||
).to_list()
|
).to_list()
|
||||||
|
|
||||||
|
logger.debug(
|
||||||
|
"Found following reminders for %s (UTC NOW): %s",
|
||||||
|
utcnow,
|
||||||
|
json_util.dumps(users, indent=None),
|
||||||
|
)
|
||||||
|
|
||||||
for user_db in users:
|
for user_db in users:
|
||||||
user = PyroUser(**user_db)
|
logger.debug("Processing user %s...", json_util.dumps(user_db, indent=None))
|
||||||
|
|
||||||
|
user = await PyroUser.from_dict(**user_db)
|
||||||
|
|
||||||
if not user.enabled or user.location is None:
|
if not user.enabled or user.location is None:
|
||||||
continue
|
continue
|
||||||
@ -30,23 +42,27 @@ async def remind(app: PyroClient) -> None:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
user_date = (
|
user_date = from_utc(
|
||||||
datetime.now(pytz_timezone(location.timezone)).replace(
|
datetime.utcnow() + timedelta(days=user.offset),
|
||||||
second=0, microsecond=0
|
user.location.timezone.zone,
|
||||||
)
|
).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||||
+ timedelta(days=user.offset)
|
|
||||||
).replace(tzinfo=timezone.utc)
|
|
||||||
|
|
||||||
entries = await col_entries.find(
|
entries = await col_entries.find(
|
||||||
{
|
{
|
||||||
"location": {"$in": location.id},
|
"locations": location.id,
|
||||||
"date": user_date.replace(hour=0, minute=0),
|
"date": user_date,
|
||||||
}
|
}
|
||||||
).to_list()
|
).to_list()
|
||||||
|
|
||||||
logger.info("Entries of %s for %s: %s", user.id, user_date, entries)
|
logger.info("Entries of %s for %s: %s", user.id, user_date, entries)
|
||||||
|
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
|
logger.debug(
|
||||||
|
"Sending %s notification about %s",
|
||||||
|
user.id,
|
||||||
|
json_util.dumps(entry, indent=None),
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
garbage_type = app._(
|
garbage_type = app._(
|
||||||
str(GarbageType(entry["garbage_type"]).value),
|
str(GarbageType(entry["garbage_type"]).value),
|
||||||
|
@ -6,7 +6,7 @@ from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
|||||||
|
|
||||||
from classes.location import Location
|
from classes.location import Location
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
from modules.database import col_locations
|
from modules.database_api import col_locations
|
||||||
|
|
||||||
|
|
||||||
async def search_name(app: PyroClient, message: Message) -> Union[Location, None]:
|
async def search_name(app: PyroClient, message: Message) -> Union[Location, None]:
|
||||||
@ -15,14 +15,16 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
|
|||||||
location: Union[Location, None] = None
|
location: Union[Location, None] = None
|
||||||
|
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("location_request_name", "messages", locale=user.locale),
|
app._("location_name", "messages", locale=user.locale),
|
||||||
reply_markup=ForceReply(
|
reply_markup=ForceReply(
|
||||||
placeholder=app._("location_name", "force_replies", locale=user.locale)
|
placeholder=app._("location_name", "force_replies", locale=user.locale)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
while location is None:
|
while location is None:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
@ -48,7 +50,7 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
|
|||||||
|
|
||||||
locations = await col_locations.find(query).limit(6).to_list()
|
locations = await col_locations.find(query).limit(6).to_list()
|
||||||
|
|
||||||
if len(locations) == 0:
|
if len(locations) == 0 or locations is None:
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("location_name_empty", "messages", locale=user.locale).format(
|
app._("location_name_empty", "messages", locale=user.locale).format(
|
||||||
cancel_notice=app._("cancel", "messages", locale=user.locale)
|
cancel_notice=app._("cancel", "messages", locale=user.locale)
|
||||||
@ -61,6 +63,8 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
|
|||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
locations.reverse()
|
||||||
|
|
||||||
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
|
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
|
||||||
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
|
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
|
||||||
|
|
||||||
@ -70,7 +74,9 @@ async def search_name(app: PyroClient, message: Message) -> Union[Location, None
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
|
@ -6,7 +6,7 @@ from pyrogram.types import Message, ReplyKeyboardRemove
|
|||||||
|
|
||||||
from classes.location import Location
|
from classes.location import Location
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
from modules.database import col_locations
|
from modules.database_api import col_locations
|
||||||
from modules.search_name import search_name
|
from modules.search_name import search_name
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +32,8 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
|
|||||||
)
|
)
|
||||||
return await search_name(app, message)
|
return await search_name(app, message)
|
||||||
|
|
||||||
|
locations.reverse()
|
||||||
|
|
||||||
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
|
keyboard = ReplyKeyboard(resize_keyboard=True, row_width=2)
|
||||||
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
|
keyboard.add(*[ReplyButton(db_record["name"]) for db_record in locations])
|
||||||
|
|
||||||
@ -41,7 +43,10 @@ async def search_nearby(app: PyroClient, message: Message) -> Union[Location, No
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
location: Union[Location, None] = None
|
location: Union[Location, None] = None
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
from pytz import UTC
|
||||||
|
from pytz import timezone as pytz_timezone
|
||||||
|
|
||||||
|
|
||||||
|
def to_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
|
||||||
|
"""Move timezone unaware datetime object to UTC timezone and return it.
|
||||||
|
|
||||||
|
### Args:
|
||||||
|
* date (`datetime`): Datetime to be converted.
|
||||||
|
* timezone (`Union[str, None] = None`): Timezone name (must be pytz-compatible). Defaults to `None` (UTC).
|
||||||
|
|
||||||
|
### Returns:
|
||||||
|
* `datetime`: Timezone unaware datetime in UTC with timezone's offset applied to it.
|
||||||
|
"""
|
||||||
|
timezone = "UTC" if timezone is None else timezone
|
||||||
|
return pytz_timezone(timezone).localize(date).astimezone(UTC).replace(tzinfo=None)
|
||||||
|
|
||||||
|
|
||||||
|
def from_utc(date: datetime, timezone: Union[str, None] = None) -> datetime:
|
||||||
|
"""Move timezone unaware datetime object to the timezone specified and return it.
|
||||||
|
|
||||||
|
### Args:
|
||||||
|
* date (`datetime`): Datetime to be converted.
|
||||||
|
* timezone (`Union[str, None] = None`): Timezone name (must be pytz-compatible). Defaults to `None` (UTC).
|
||||||
|
|
||||||
|
### Returns:
|
||||||
|
* `datetime`: Timezone unaware datetime in timezone provided with offset from UTC applied to it.
|
||||||
|
"""
|
||||||
|
timezone = "UTC" if timezone is None else timezone
|
||||||
|
return (
|
||||||
|
pytz_timezone("UTC")
|
||||||
|
.localize(date)
|
||||||
|
.astimezone(pytz_timezone(timezone))
|
||||||
|
.replace(tzinfo=None)
|
||||||
|
)
|
@ -8,12 +8,13 @@ from pyrogram.types import Message, ReplyKeyboardRemove
|
|||||||
from ujson import dumps
|
from ujson import dumps
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["checkout"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["checkout"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_checkout(app: PyroClient, message: Message):
|
async def command_checkout(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
@ -42,7 +43,9 @@ async def command_checkout(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer_delete = await listen_message(app, message.chat.id, 300)
|
answer_delete = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
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(
|
||||||
@ -84,7 +87,9 @@ async def command_checkout(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer_confirm = await listen_message(app, message.chat.id, 300)
|
answer_confirm = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
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(
|
||||||
|
@ -2,10 +2,11 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["help"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["help"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
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)
|
||||||
|
@ -1,14 +1,11 @@
|
|||||||
from datetime import datetime
|
|
||||||
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 ForceReply, Message, ReplyKeyboardRemove
|
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
||||||
from ujson import loads
|
|
||||||
|
|
||||||
|
from classes.importer.csv import ImporterCSV
|
||||||
|
from classes.importer.json import ImporterJSON
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
from modules import custom_filters
|
from modules import custom_filters
|
||||||
from modules.database import col_entries
|
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
@ -32,7 +29,10 @@ async def command_import(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if answer.document is None or answer.document.mime_type != "application/json":
|
if answer.document is None or answer.document.mime_type not in [
|
||||||
|
"application/json",
|
||||||
|
"text/csv",
|
||||||
|
]:
|
||||||
await answer.reply_text(
|
await answer.reply_text(
|
||||||
app._("import_invalid_filetype", "messages", locale=user.locale).format(
|
app._("import_invalid_filetype", "messages", locale=user.locale).format(
|
||||||
cancel_notice=app._("cancel", "messages", locale=user.locale)
|
cancel_notice=app._("cancel", "messages", locale=user.locale)
|
||||||
@ -44,51 +44,36 @@ async def command_import(app: PyroClient, message: Message):
|
|||||||
|
|
||||||
file = await app.download_media(answer, in_memory=True)
|
file = await app.download_media(answer, in_memory=True)
|
||||||
|
|
||||||
entries: List[Mapping[str, Union[str, int]]] = loads(bytes(file.getbuffer())) # type: ignore
|
data: bytes = bytes(file.getbuffer()) # type: ignore
|
||||||
|
|
||||||
for entry in entries:
|
# I'd like to replace it with switch-case, but 3.9 compatibility
|
||||||
if not isinstance(entries, list):
|
# is still more important to be there. Although refactor may be
|
||||||
await answer.reply_text(
|
# done in the near future as Python 3.9 EOL gets nearer.
|
||||||
app._("import_invalid", "messages", locale=user.locale),
|
if answer.document.mime_type == "application/json":
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
importer = ImporterJSON()
|
||||||
)
|
elif answer.document.mime_type == "text/csv":
|
||||||
return
|
importer = ImporterCSV()
|
||||||
|
else:
|
||||||
|
await answer.reply_text(
|
||||||
|
app._("import_invalid_filetype", "messages", locale=user.locale).format(
|
||||||
|
cancel_notice=""
|
||||||
|
),
|
||||||
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
for key in ("locations", "garbage_type", "date"):
|
try:
|
||||||
if (
|
import_result = await importer.import_data(data)
|
||||||
key not in entry
|
except ValueError:
|
||||||
or (key == "garbage_type" and not isinstance(entry[key], int))
|
await answer.reply_text(
|
||||||
or (key == "locations" and not isinstance(entry[key], list))
|
app._("import_invalid", "messages", locale=user.locale),
|
||||||
):
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
await answer.reply_text(
|
)
|
||||||
app._("import_invalid", "messages", locale=user.locale),
|
return
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
if key == "date":
|
|
||||||
try:
|
|
||||||
datetime.fromisoformat(str(entry[key]))
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
await answer.reply_text(
|
|
||||||
app._("import_invalid_date", "messages", locale=user.locale),
|
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
entries_clean: List[Mapping[str, Union[str, int, datetime]]] = [
|
|
||||||
{
|
|
||||||
"locations": entry["locations"],
|
|
||||||
"garbage_type": entry["garbage_type"],
|
|
||||||
"date": datetime.fromisoformat(str(entry["date"])),
|
|
||||||
}
|
|
||||||
for entry in entries
|
|
||||||
]
|
|
||||||
|
|
||||||
await col_entries.insert_many(entries_clean)
|
|
||||||
|
|
||||||
await answer.reply_text(
|
await answer.reply_text(
|
||||||
app._("import_finished", "messages", locale=user.locale).format(
|
app._("import_finished", "messages", locale=user.locale).format(
|
||||||
count=len(entries_clean)
|
count=len(import_result)
|
||||||
),
|
),
|
||||||
reply_markup=ReplyKeyboardRemove(),
|
reply_markup=ReplyKeyboardRemove(),
|
||||||
)
|
)
|
||||||
|
@ -2,10 +2,11 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["remove_commands"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_remove_commands(app: PyroClient, message: Message):
|
async def command_remove_commands(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
|
@ -6,16 +6,24 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
from modules.utils import from_utc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["set_offset"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["set_offset"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_set_offset(app: PyroClient, message: Message):
|
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)
|
||||||
|
|
||||||
|
if user.location is None:
|
||||||
|
await message.reply_text(
|
||||||
|
app._("location_empty", "messages", locale=user.locale)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("set_offset", "messages", locale=user.locale),
|
app._("set_offset", "messages", locale=user.locale),
|
||||||
reply_markup=ForceReply(
|
reply_markup=ForceReply(
|
||||||
@ -24,7 +32,9 @@ async def command_set_offset(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
@ -55,8 +65,9 @@ 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)
|
||||||
|
|
||||||
garbage_time = datetime(
|
garbage_time = from_utc(
|
||||||
1970, 1, 1, hour=user.time_hour, minute=user.time_minute
|
datetime(1970, 1, 1, user.time_hour, user.time_minute),
|
||||||
|
None if user.location is None else user.location.timezone.zone,
|
||||||
).strftime(app._("time", "formats"))
|
).strftime(app._("time", "formats"))
|
||||||
|
|
||||||
await answer.reply_text(
|
await answer.reply_text(
|
||||||
|
@ -6,16 +6,24 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
from pyrogram.types import ForceReply, Message, ReplyKeyboardRemove
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
from modules.utils import to_utc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["set_time"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["set_time"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_set_time(app: PyroClient, message: Message):
|
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)
|
||||||
|
|
||||||
|
if user.location is None:
|
||||||
|
await message.reply_text(
|
||||||
|
app._("location_empty", "messages", locale=user.locale)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("set_time", "messages", locale=user.locale),
|
app._("set_time", "messages", locale=user.locale),
|
||||||
reply_markup=ForceReply(
|
reply_markup=ForceReply(
|
||||||
@ -24,7 +32,9 @@ async def command_set_time(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
@ -45,7 +55,13 @@ async def command_set_time(app: PyroClient, message: Message):
|
|||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
user_time = datetime.strptime(answer.text, "%H:%M")
|
now = datetime.now()
|
||||||
|
|
||||||
|
parsed_time = datetime.strptime(answer.text, "%H:%M").replace(
|
||||||
|
year=now.year, month=now.month, day=now.day, second=0, microsecond=0
|
||||||
|
)
|
||||||
|
|
||||||
|
user_time = to_utc(parsed_time, user.location.timezone.zone)
|
||||||
|
|
||||||
await user.update_time(hour=user_time.hour, minute=user_time.minute)
|
await user.update_time(hour=user_time.hour, minute=user_time.minute)
|
||||||
|
|
||||||
@ -55,7 +71,7 @@ async def command_set_time(app: PyroClient, message: Message):
|
|||||||
user_time.strftime("%H:%M"),
|
user_time.strftime("%H:%M"),
|
||||||
)
|
)
|
||||||
|
|
||||||
garbage_time = user_time.strftime(app._("time", "formats"))
|
garbage_time = parsed_time.strftime(app._("time", "formats"))
|
||||||
|
|
||||||
await answer.reply_text(
|
await answer.reply_text(
|
||||||
app._("set_time_finished", "messages", locale=user.locale).format(
|
app._("set_time_finished", "messages", locale=user.locale).format(
|
||||||
|
@ -8,14 +8,16 @@ 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 import custom_filters
|
||||||
from modules.search_name import search_name
|
from modules.search_name import search_name
|
||||||
from modules.search_nearby import search_nearby
|
from modules.search_nearby import search_nearby
|
||||||
|
from modules.utils import from_utc
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["setup"] + i18n.sync.in_all_locales("configure", "buttons"), prefixes=["/", ""]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["setup"] + i18n.sync.in_all_locales("start_configure", "buttons"), prefixes=["/", ""]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
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)
|
||||||
@ -34,7 +36,9 @@ async def command_setup(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer_type = await listen_message(app, message.chat.id, 300)
|
answer_type = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer_type is None or answer_type.text == "/cancel":
|
if answer_type is None or answer_type.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
@ -70,9 +74,10 @@ async def command_setup(app: PyroClient, message: Message):
|
|||||||
|
|
||||||
await user.update_location(location.id)
|
await user.update_location(location.id)
|
||||||
|
|
||||||
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime(
|
user_time = from_utc(
|
||||||
app._("time", "formats", locale=user.locale)
|
datetime(1970, 1, 1, user.time_hour, user.time_minute),
|
||||||
)
|
None if user.location is None else user.location.timezone.zone,
|
||||||
|
).strftime(app._("time", "formats", locale=user.locale))
|
||||||
|
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("setup_finished", "messages", locale=user.locale).format(
|
app._("setup_finished", "messages", locale=user.locale).format(
|
||||||
|
@ -4,12 +4,14 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled
|
~filters.scheduled
|
||||||
& filters.private
|
& filters.private
|
||||||
& filters.command(["shutdown", "reboot", "restart"], prefixes=["/"]) # type: ignore
|
& filters.command(["shutdown", "reboot", "restart"], prefixes=["/"])
|
||||||
|
& ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_shutdown(app: PyroClient, msg: Message):
|
async def command_shutdown(app: PyroClient, msg: Message):
|
||||||
if msg.from_user.id == app.owner:
|
if msg.from_user.id == app.owner:
|
||||||
|
@ -10,10 +10,12 @@ from pyrogram.types import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
from modules.utils import from_utc
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["start"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_start(app: PyroClient, message: Message):
|
async def command_start(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
@ -52,7 +54,9 @@ async def command_start(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
app.contexts.append(message.from_user.id)
|
||||||
answer = await listen_message(app, message.chat.id, 300)
|
answer = await listen_message(app, message.chat.id, 300)
|
||||||
|
app.contexts.remove(message.from_user.id)
|
||||||
|
|
||||||
if answer is None or answer.text == "/cancel":
|
if answer is None or answer.text == "/cancel":
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
@ -82,9 +86,10 @@ async def command_start(app: PyroClient, message: Message):
|
|||||||
|
|
||||||
await user.update_location(location.id)
|
await user.update_location(location.id)
|
||||||
|
|
||||||
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime(
|
user_time = from_utc(
|
||||||
app._("time", "formats", locale=user.locale)
|
datetime(1970, 1, 1, user.time_hour, user.time_minute),
|
||||||
)
|
None if user.location is None else user.location.timezone.zone,
|
||||||
|
).strftime(app._("time", "formats", locale=user.locale))
|
||||||
await answer.reply_text(
|
await answer.reply_text(
|
||||||
app._("start_selection_yes", "messages", locale=user.locale).format(
|
app._("start_selection_yes", "messages", locale=user.locale).format(
|
||||||
name=location.name, offset=user.offset, time=user_time
|
name=location.name, offset=user.offset, time=user_time
|
||||||
|
@ -4,10 +4,12 @@ from pyrogram import filters
|
|||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
|
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
from modules.utils import from_utc
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["toggle"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["toggle"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_toggle(app: PyroClient, message: Message):
|
async def command_toggle(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
@ -20,7 +22,10 @@ async def command_toggle(app: PyroClient, message: Message):
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
user_time = datetime(1970, 1, 1, user.time_hour, user.time_minute).strftime("%H:%M")
|
user_time = from_utc(
|
||||||
|
datetime(1970, 1, 1, user.time_hour, user.time_minute),
|
||||||
|
None if user.location is None else user.location.timezone.zone,
|
||||||
|
).strftime(app._("time", "formats"))
|
||||||
|
|
||||||
if user.location is None:
|
if user.location is None:
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
|
@ -2,34 +2,30 @@ from datetime import datetime, timedelta, timezone
|
|||||||
|
|
||||||
from pyrogram import filters
|
from pyrogram import filters
|
||||||
from pyrogram.types import Message
|
from pyrogram.types import Message
|
||||||
from pytz import timezone as pytz_timezone
|
|
||||||
|
|
||||||
from classes.garbage_entry import GarbageEntry
|
from classes.garbage_entry import GarbageEntry
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
from modules.database import col_entries
|
from modules import custom_filters
|
||||||
|
from modules.database_api import col_entries
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["upcoming"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["upcoming"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_upcoming(app: PyroClient, message: Message):
|
async def command_upcoming(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
|
|
||||||
if user.location is None:
|
if user.location is None:
|
||||||
await message.reply_text(
|
await message.reply_text(
|
||||||
app._("upcoming_empty_location", "messages", locale=user.locale)
|
app._("location_empty", "messages", locale=user.locale)
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
date_min = (
|
date_min = (
|
||||||
datetime.now(pytz_timezone(user.location.timezone)).replace(
|
datetime.now(user.location.timezone).replace(second=0, microsecond=0)
|
||||||
second=0, microsecond=0
|
|
||||||
)
|
|
||||||
).replace(tzinfo=timezone.utc)
|
).replace(tzinfo=timezone.utc)
|
||||||
date_max = (
|
date_max = (
|
||||||
datetime.now(pytz_timezone(user.location.timezone)).replace(
|
datetime.now(user.location.timezone).replace(second=0, microsecond=0)
|
||||||
second=0, microsecond=0
|
|
||||||
)
|
|
||||||
+ timedelta(days=30)
|
+ timedelta(days=30)
|
||||||
).replace(tzinfo=timezone.utc)
|
).replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
@ -37,7 +33,7 @@ async def command_upcoming(app: PyroClient, message: Message):
|
|||||||
await GarbageEntry.from_record(entry)
|
await GarbageEntry.from_record(entry)
|
||||||
async for entry in col_entries.find(
|
async for entry in col_entries.find(
|
||||||
{
|
{
|
||||||
"location": {"$in": user.location.id},
|
"locations": user.location.id,
|
||||||
"date": {"$gte": date_min, "$lte": date_max},
|
"date": {"$gte": date_min, "$lte": date_max},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -45,7 +41,7 @@ async def command_upcoming(app: PyroClient, message: Message):
|
|||||||
|
|
||||||
entries_text = "\n\n".join(
|
entries_text = "\n\n".join(
|
||||||
[
|
[
|
||||||
f"**{entry.date.strftime(app._('date', 'formats', locale=user.locale))}**:\n{app._(str(entry.garbage_type.value), 'garbage_types')}"
|
f"**{entry.date.strftime(app._('date', 'formats', locale=user.locale))}**:\n{app._(str(entry.garbage_type.value), 'garbage_types', locale=user.locale)}"
|
||||||
for entry in entries
|
for entry in entries
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -6,10 +6,11 @@ from pyrogram.types import CallbackQuery, Message
|
|||||||
|
|
||||||
from classes.callbacks import CallbackLanguage
|
from classes.callbacks import CallbackLanguage
|
||||||
from classes.pyroclient import PyroClient
|
from classes.pyroclient import PyroClient
|
||||||
|
from modules import custom_filters
|
||||||
|
|
||||||
|
|
||||||
@PyroClient.on_message(
|
@PyroClient.on_message(
|
||||||
~filters.scheduled & filters.private & filters.command(["language"], prefixes=["/"]) # type: ignore
|
~filters.scheduled & filters.private & filters.command(["language"], prefixes=["/"]) & ~custom_filters.context # type: ignore
|
||||||
)
|
)
|
||||||
async def command_language(app: PyroClient, message: Message):
|
async def command_language(app: PyroClient, message: Message):
|
||||||
user = await app.find_user(message.from_user)
|
user = await app.find_user(message.from_user)
|
||||||
|
@ -5,7 +5,7 @@ mongodb-migrations==1.3.0
|
|||||||
pykeyboard==0.1.5
|
pykeyboard==0.1.5
|
||||||
tgcrypto==1.2.5
|
tgcrypto==1.2.5
|
||||||
ujson>=5.0.0
|
ujson>=5.0.0
|
||||||
uvloop==0.17.0
|
uvloop==0.19.0
|
||||||
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
|
--extra-index-url https://git.end-play.xyz/api/packages/profitroll/pypi/simple
|
||||||
async_pymongo==0.1.4
|
async_pymongo==0.1.4
|
||||||
libbot[speed,pyrogram]==2.0.1
|
libbot[speed,pyrogram]==2.0.1
|
Reference in New Issue
Block a user