diff --git a/requirements.txt b/requirements.txt index d9726c0e..9bdb3618 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ pytest>=4.3.1 psycopg2-binary>=2.8 aiohttp>=3.5.4 sqlalchemy>=1.3.2 +Markdown>=3.1 +dateparser>=0.7.1 diff --git a/royalgames.py b/royalgames.py index 8589584e..14a079a7 100644 --- a/royalgames.py +++ b/royalgames.py @@ -1,9 +1,8 @@ import os import asyncio from royalnet.bots import TelegramBot -from royalnet.commands import PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, SyncCommand, DiarioCommand, RageCommand +from royalnet.commands import * from royalnet.commands.debug_create import DebugCreateCommand -from royalnet.commands.debug_author import DebugAuthorCommand from royalnet.commands.error_handler import ErrorHandlerCommand from royalnet.network import RoyalnetServer from royalnet.database.tables import Royal, Telegram @@ -11,7 +10,7 @@ from royalnet.database.tables import Royal, Telegram loop = asyncio.get_event_loop() commands = [PingCommand, ShipCommand, SmecdsCommand, ColorCommand, CiaoruoziCommand, DebugCreateCommand, SyncCommand, - DebugAuthorCommand, DiarioCommand, RageCommand] + AuthorCommand, DiarioCommand, RageCommand, DateparserCommand, ReminderCommand] master = RoyalnetServer("localhost", 1234, "sas") tg_bot = TelegramBot(os.environ["TG_AK"], "localhost:1234", "sas", commands, os.environ["DB_PATH"], Royal, Telegram, "tg_id", error_command=ErrorHandlerCommand) diff --git a/royalnet/bots/telegram.py b/royalnet/bots/telegram.py index 181650fc..c6e683b0 100644 --- a/royalnet/bots/telegram.py +++ b/royalnet/bots/telegram.py @@ -4,7 +4,7 @@ import typing import logging as _logging import sys from ..commands import NullCommand -from ..utils import asyncify, Call, Command +from ..utils import asyncify, Call, Command, UnregisteredError from ..network import RoyalnetLink, Message from ..database import Alchemy, relationshiplinkchain @@ -71,16 +71,21 @@ class TelegramBot: response = await self.network.request(message, destination) return response - async def get_author(call): + async def get_author(call, error_if_none=False): update: telegram.Update = call.kwargs["update"] user: telegram.User = update.effective_user if user is None: + if error_if_none: + raise UnregisteredError("Author is not registered!") return None query = call.session.query(self.master_table) for link in self.identity_chain: query = query.join(link.mapper.class_) query = query.filter(self.identity_column == user.id) - return await asyncify(query.one_or_none) + result = await asyncify(query.one_or_none) + if result is None and error_if_none: + raise UnregisteredError("Author is not registered!") + return result self.TelegramCall = TelegramCall diff --git a/royalnet/commands/__init__.py b/royalnet/commands/__init__.py index 78f7657f..d9670135 100644 --- a/royalnet/commands/__init__.py +++ b/royalnet/commands/__init__.py @@ -7,7 +7,10 @@ from .color import ColorCommand from .sync import SyncCommand from .diario import DiarioCommand from .rage import RageCommand +from .dateparser import DateparserCommand +from .author import AuthorCommand +from .reminder import ReminderCommand __all__ = ["NullCommand", "PingCommand", "ShipCommand", "SmecdsCommand", "CiaoruoziCommand", "ColorCommand", - "SyncCommand", "DiarioCommand", "RageCommand"] + "SyncCommand", "DiarioCommand", "RageCommand", "DateparserCommand", "AuthorCommand", "ReminderCommand"] diff --git a/royalnet/commands/debug_author.py b/royalnet/commands/author.py similarity index 80% rename from royalnet/commands/debug_author.py rename to royalnet/commands/author.py index 7400ee85..188ec55f 100644 --- a/royalnet/commands/debug_author.py +++ b/royalnet/commands/author.py @@ -1,10 +1,10 @@ -from ..utils import Command, CommandArgs, Call +from ..utils import Command, Call from ..database.tables import Royal, Telegram -class DebugAuthorCommand(Command): +class AuthorCommand(Command): - command_name = "debug_author" + command_name = "author" command_description = "Ottieni informazioni sull'autore di questa chiamata." command_syntax = "" diff --git a/royalnet/commands/color.py b/royalnet/commands/color.py index 4c920486..ff9a458f 100644 --- a/royalnet/commands/color.py +++ b/royalnet/commands/color.py @@ -1,4 +1,4 @@ -from ..utils import Command, CommandArgs, Call +from ..utils import Command, Call class ColorCommand(Command): diff --git a/royalnet/commands/dateparser.py b/royalnet/commands/dateparser.py new file mode 100644 index 00000000..c2dc401f --- /dev/null +++ b/royalnet/commands/dateparser.py @@ -0,0 +1,21 @@ +import datetime +import dateparser +from ..utils import Command, Call, InvalidInputError + + +class DateparserCommand(Command): + + command_name = "dateparser" + command_description = "Legge e comprende la data inserita." + command_syntax = "(data)" + + @classmethod + async def common(cls, call: Call): + if len(call.args) == 0: + raise InvalidInputError("Missing arg") + text = " ".join(call.args) + date: datetime.datetime = dateparser.parse(text) + if date is None: + await call.reply("🕕 La data inserita non è valida.") + return + await call.reply(f"🕐 La data inserita è {date.isoformat()}") diff --git a/royalnet/commands/debug_create.py b/royalnet/commands/debug_create.py index faaedfb2..3b06e7c1 100644 --- a/royalnet/commands/debug_create.py +++ b/royalnet/commands/debug_create.py @@ -1,4 +1,4 @@ -from ..utils import Command, CommandArgs, Call, asyncify +from ..utils import Command, Call, asyncify from ..database.tables import Royal, Alias diff --git a/royalnet/commands/diario.py b/royalnet/commands/diario.py index 7159bddf..c078cffc 100644 --- a/royalnet/commands/diario.py +++ b/royalnet/commands/diario.py @@ -45,10 +45,7 @@ class DiarioCommand(Command): @classmethod async def common(cls, call: Call): # Find the creator of the quotes - creator = await call.get_author() - if creator is None: - await call.reply("⚠️ Devi essere registrato a Royalnet per usare questo comando!") - return + creator = await call.get_author(error_if_none=True) # Recreate the full sentence raw_text = " ".join(call.args) # Pass the sentence through the diario regex diff --git a/royalnet/commands/error_handler.py b/royalnet/commands/error_handler.py index 13f2a299..70c6a521 100644 --- a/royalnet/commands/error_handler.py +++ b/royalnet/commands/error_handler.py @@ -1,6 +1,6 @@ import traceback from logging import Logger -from ..utils import Command, CommandArgs, Call, InvalidInputError, UnsupportedError +from ..utils import Command, CommandArgs, Call, InvalidInputError, UnsupportedError, UnregisteredError class ErrorHandlerCommand(Command): @@ -22,7 +22,10 @@ class ErrorHandlerCommand(Command): return if e_type == InvalidInputError: command = call.kwargs["previous_command"] - await call.reply(f"⚠️ Sintassi non valida.\nSintassi corretta:[c]/{command.command_name} {command.command_syntax}[/c]") + await call.reply(f"⚠️ Sintassi non valida.\nSintassi corretta: [c]/{command.command_name} {command.command_syntax}[/c]") + return + if e_type == UnregisteredError: + await call.reply("⚠️ Devi essere registrato a Royalnet per usare questo comando!") return await call.reply(f"❌ Eccezione non gestita durante l'esecuzione del comando:\n[b]{e_type.__name__}[/b]\n{e_value}") formatted_tb: str = '\n'.join(traceback.format_tb(e_tb)) diff --git a/royalnet/commands/null.py b/royalnet/commands/null.py index 441d090b..f30db9fb 100644 --- a/royalnet/commands/null.py +++ b/royalnet/commands/null.py @@ -1,4 +1,4 @@ -from ..utils import Command, CommandArgs, Call +from ..utils import Command, Call class NullCommand(Command): diff --git a/royalnet/commands/ping.py b/royalnet/commands/ping.py index 04ea42bd..df280dd7 100644 --- a/royalnet/commands/ping.py +++ b/royalnet/commands/ping.py @@ -1,5 +1,5 @@ import asyncio -from ..utils import Command, CommandArgs, Call, InvalidInputError +from ..utils import Command, Call, InvalidInputError class PingCommand(Command): diff --git a/royalnet/commands/reminder.py b/royalnet/commands/reminder.py new file mode 100644 index 00000000..cae785f8 --- /dev/null +++ b/royalnet/commands/reminder.py @@ -0,0 +1,28 @@ +import datetime +import dateparser +import typing +from ..utils import Command, Call, sleep_until + + +class ReminderCommand(Command): + + command_name = "reminder" + command_description = "Ripete quello che gli avevi chiesto dopo un po' di tempo." + command_syntax = "[ (data) ] (testo)" + + @classmethod + async def common(cls, call: Call): + match = call.args.match(r"\[ *(.+?) *] *(.+?) *$") + date_str = match.group(1) + reminder_text = match.group(2) + date: typing.Optional[datetime.datetime] + try: + date = dateparser.parse(date_str) + except OverflowError: + date = None + if date is None: + await call.reply("⚠️ La data che hai inserito non è valida.") + return + await call.reply(f"✅ Promemoria impostato per [b]{date.strftime('%Y-%m-%d %H:%M:%S')}[/b]") + await sleep_until(date) + await call.reply(f"❗️ Promemoria: [b]{reminder_text}[/b]") diff --git a/royalnet/commands/ship.py b/royalnet/commands/ship.py index c0b014af..8ec4e661 100644 --- a/royalnet/commands/ship.py +++ b/royalnet/commands/ship.py @@ -1,5 +1,5 @@ import re -from ..utils import Command, CommandArgs, Call, safeformat +from ..utils import Command, Call, safeformat SHIP_RESULT = "💕 {one} + {two} = [b]{result}[/b]" diff --git a/royalnet/commands/smecds.py b/royalnet/commands/smecds.py index 7e8bc21c..6d5616d4 100644 --- a/royalnet/commands/smecds.py +++ b/royalnet/commands/smecds.py @@ -1,5 +1,5 @@ import random -from ..utils import Command, CommandArgs, Call, safeformat +from ..utils import Command, Call, safeformat DS_LIST = ["della secca", "del seccatore", "del secchiello", "del secchio", "del secchione", "del secondino", diff --git a/royalnet/commands/sync.py b/royalnet/commands/sync.py index 2dc7dadd..288ce3c1 100644 --- a/royalnet/commands/sync.py +++ b/royalnet/commands/sync.py @@ -1,6 +1,6 @@ import typing from telegram import Update, User -from ..utils import Command, CommandArgs, Call, asyncify, UnsupportedError +from ..utils import Command, Call, asyncify, UnsupportedError from ..database.tables import Royal, Telegram diff --git a/royalnet/database/tables/aliases.py b/royalnet/database/tables/aliases.py index 7f86ffa6..f88f79d7 100644 --- a/royalnet/database/tables/aliases.py +++ b/royalnet/database/tables/aliases.py @@ -1,8 +1,6 @@ from sqlalchemy import Column, \ Integer, \ String, \ - BigInteger, \ - LargeBinary, \ ForeignKey from sqlalchemy.orm import relationship from .royals import Royal diff --git a/royalnet/utils/__init__.py b/royalnet/utils/__init__.py index c75aaa79..7060c955 100644 --- a/royalnet/utils/__init__.py +++ b/royalnet/utils/__init__.py @@ -1,8 +1,9 @@ from .asyncify import asyncify -from .call import Call +from .call import Call, UnregisteredError from .command import Command, CommandArgs, InvalidInputError, UnsupportedError, InvalidConfigError, ExternalError from .safeformat import safeformat from .classdictjanitor import cdj +from .sleepuntil import sleep_until __all__ = ["asyncify", "Call", "Command", "safeformat", "InvalidInputError", "UnsupportedError", "CommandArgs", - "cdj", "InvalidConfigError", "ExternalError"] + "cdj", "InvalidConfigError", "ExternalError", "sleep_until", "UnregisteredError"] diff --git a/royalnet/utils/call.py b/royalnet/utils/call.py index ab31c6bc..9d0afba2 100644 --- a/royalnet/utils/call.py +++ b/royalnet/utils/call.py @@ -10,6 +10,10 @@ if typing.TYPE_CHECKING: loop = asyncio.get_event_loop() +class UnregisteredError(Exception): + pass + + class Call: """A command call. Still an abstract class, subbots should create a new call from this.""" @@ -27,9 +31,10 @@ class Call: The data must be pickleable.""" raise NotImplementedError() - async def get_author(self): + async def get_author(self, error_if_none=False): """Try to find the universal identifier of the user that sent the message. - That probably means, the database row identifying the user.""" + That probably means, the database row identifying the user. + Raise a UnregisteredError if error_if_none is set to True and no author is found.""" raise NotImplementedError() # These parameters / methods should be left alone diff --git a/royalnet/utils/command.py b/royalnet/utils/command.py index 098f78d0..84a26f8e 100644 --- a/royalnet/utils/command.py +++ b/royalnet/utils/command.py @@ -1,3 +1,4 @@ +import re import typing if typing.TYPE_CHECKING: from .call import Call @@ -35,6 +36,18 @@ class CommandArgs(list): raise InvalidInputError(f'Tried to get invalid [{item}] slice from CommandArgs') raise ValueError(f"Invalid type passed to CommandArgs.__getattr__: {type(item)}") + def match(self, pattern: typing.Pattern) -> typing.Match: + text = " ".join(self) + match = re.match(pattern, text) + if match is None: + raise InvalidInputError("Pattern didn't match") + return match + + def optional(self, index: int) -> typing.Optional: + try: + return self[index] + except IndexError: + return None class Command: """A generic command, called from any source."""