Language-labelled commands are accepted only for selected language. /father uses selected language for commands

This commit is contained in:
Davte 2020-05-14 18:49:40 +02:00
parent e6fdacd2f4
commit cf6a2e1baa
3 changed files with 115 additions and 73 deletions

View File

@ -3,7 +3,7 @@
Usage: Usage:
``` ```
import davtelepot import davtelepot
my_bot = davtelepot.Bot.get('my_token', 'my_database.db') my_bot = davtelepot.bot.Bot(token='my_token', database_url='my_database.db')
davtelepot.admin_tools.init(my_bot) davtelepot.admin_tools.init(my_bot)
``` ```
""" """
@ -1070,7 +1070,11 @@ def get_current_commands(bot: Bot, language: str = None) -> List[dict]:
return sorted( return sorted(
[ [
{ {
'command': name, 'command': bot.get_message(
messages=information['language_labelled_commands'],
default_message=name,
language=language
),
'description': bot.get_message( 'description': bot.get_message(
messages=information['description'], messages=information['description'],
language=language language=language

View File

@ -778,6 +778,19 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
).group(0) # Get the first group of characters matching pattern ).group(0) # Get the first group of characters matching pattern
if command in self.commands: if command in self.commands:
replier = self.commands[command]['handler'] replier = self.commands[command]['handler']
elif command in [
description['language_labelled_commands'][language]
for c, description in self.commands.items()
if 'language_labelled_commands' in description
and language in description['language_labelled_commands']
]:
replier = [
description['handler']
for c, description in self.commands.items()
if 'language_labelled_commands' in description
and language in description['language_labelled_commands']
and command == description['language_labelled_commands'][language]
][0]
elif 'chat' in update and update['chat']['id'] > 0: elif 'chat' in update and update['chat']['id'] > 0:
reply = dict(text=self.unknown_command_message) reply = dict(text=self.unknown_command_message)
else: # Handle command aliases and text parsers else: # Handle command aliases and text parsers
@ -2153,6 +2166,10 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
""" """
if language_labelled_commands is None: if language_labelled_commands is None:
language_labelled_commands = dict() language_labelled_commands = dict()
language_labelled_commands = {
key: val.strip('/').lower()
for key, val in language_labelled_commands.items()
}
# Handle language-labelled commands: # Handle language-labelled commands:
# choose one main command and add others to `aliases` # choose one main command and add others to `aliases`
if isinstance(command, dict) and len(command) > 0: if isinstance(command, dict) and len(command) > 0:
@ -2166,18 +2183,12 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
break break
if aliases is None: if aliases is None:
aliases = [] aliases = []
aliases += [
alias
for alias in language_labelled_commands.values()
if alias != command
]
if not isinstance(command, str): if not isinstance(command, str):
raise TypeError(f'Command `{command}` is not a string') raise TypeError(f'Command `{command}` is not a string')
if isinstance(reply_keyboard_button, dict): if isinstance(reply_keyboard_button, dict):
for button in reply_keyboard_button.values(): for button in reply_keyboard_button.values():
if button not in aliases: if button not in aliases:
aliases.append(button) aliases.append(button)
if aliases:
if not isinstance(aliases, list): if not isinstance(aliases, list):
raise TypeError(f'Aliases is not a list: `{aliases}`') raise TypeError(f'Aliases is not a list: `{aliases}`')
if not all( if not all(
@ -2187,7 +2198,7 @@ class Bot(TelegramBot, ObjectWithDatabase, MultiLanguageObject):
] ]
): ):
raise TypeError( raise TypeError(
f'Aliases {aliases} is not a list of strings string' f'Aliases {aliases} is not a list of strings'
) )
if isinstance(help_section, dict): if isinstance(help_section, dict):
if 'authorization_level' not in help_section: if 'authorization_level' not in help_section:

View File

@ -98,7 +98,7 @@ def extract(text, starter=None, ender=None):
def make_button(text=None, callback_data='', def make_button(text=None, callback_data='',
prefix='', delimiter='|', data=[]): prefix='', delimiter='|', data=None):
"""Return a Telegram bot API-compliant button. """Return a Telegram bot API-compliant button.
callback_data can be either a ready-to-use string or a callback_data can be either a ready-to-use string or a
@ -107,6 +107,8 @@ def make_button(text=None, callback_data='',
it gets truncated at the last delimiter before that limit. it gets truncated at the last delimiter before that limit.
If absent, text is the same as callback_data. If absent, text is the same as callback_data.
""" """
if data is None:
data = []
if len(data): if len(data):
callback_data += delimiter.join(map(str, data)) callback_data += delimiter.join(map(str, data))
callback_data = "{p}{c}".format( callback_data = "{p}{c}".format(
@ -170,7 +172,7 @@ async def async_get(url, mode='json', **kwargs):
del kwargs['mode'] del kwargs['mode']
return await async_request( return await async_request(
url, url,
type='get', method='get',
mode=mode, mode=mode,
**kwargs **kwargs
) )
@ -188,13 +190,13 @@ async def async_post(url, mode='html', **kwargs):
""" """
return await async_request( return await async_request(
url, url,
type='post', method='post',
mode=mode, mode=mode,
**kwargs **kwargs
) )
async def async_request(url, type='get', mode='json', encoding=None, errors='strict', async def async_request(url, method='get', mode='json', encoding=None, errors='strict',
**kwargs): **kwargs):
"""Make an async html request. """Make an async html request.
@ -214,7 +216,7 @@ async def async_request(url, type='get', mode='json', encoding=None, errors='str
async with aiohttp.ClientSession() as s: async with aiohttp.ClientSession() as s:
async with ( async with (
s.get(url, timeout=30) s.get(url, timeout=30)
if type == 'get' if method == 'get'
else s.post(url, timeout=30, data=kwargs) else s.post(url, timeout=30, data=kwargs)
) as r: ) as r:
if mode in ['html', 'json', 'string']: if mode in ['html', 'json', 'string']:
@ -246,12 +248,14 @@ async def async_request(url, type='get', mode='json', encoding=None, errors='str
return result return result
def json_read(file_, default={}, encoding='utf-8', **kwargs): def json_read(file_, default=None, encoding='utf-8', **kwargs):
"""Return json parsing of `file_`, or `default` if file does not exist. """Return json parsing of `file_`, or `default` if file does not exist.
`encoding` refers to how the file should be read. `encoding` refers to how the file should be read.
`kwargs` will be passed to json.load() `kwargs` will be passed to json.load()
""" """
if default is None:
default = {}
if not os.path.isfile(file_): if not os.path.isfile(file_):
return default return default
with open(file_, "r", encoding=encoding) as f: with open(file_, "r", encoding=encoding) as f:
@ -268,7 +272,7 @@ def json_write(what, file_, encoding='utf-8', **kwargs):
return json.dump(what, f, indent=4, **kwargs) return json.dump(what, f, indent=4, **kwargs)
def csv_read(file_, default=[], encoding='utf-8', def csv_read(file_, default=None, encoding='utf-8',
delimiter=',', quotechar='"', **kwargs): delimiter=',', quotechar='"', **kwargs):
"""Return csv parsing of `file_`, or `default` if file does not exist. """Return csv parsing of `file_`, or `default` if file does not exist.
@ -277,6 +281,8 @@ def csv_read(file_, default=[], encoding='utf-8',
`quotechar` is the string delimiter. `quotechar` is the string delimiter.
`kwargs` will be passed to csv.reader() `kwargs` will be passed to csv.reader()
""" """
if default is None:
default = []
if not os.path.isfile(file_): if not os.path.isfile(file_):
return default return default
result = [] result = []
@ -299,7 +305,7 @@ def csv_read(file_, default=[], encoding='utf-8',
return result return result
def csv_write(info=[], file_='output.csv', encoding='utf-8', def csv_write(info=None, file_='output.csv', encoding='utf-8',
delimiter=',', quotechar='"', **kwargs): delimiter=',', quotechar='"', **kwargs):
"""Store `info` in CSV `file_`. """Store `info` in CSV `file_`.
@ -309,6 +315,8 @@ def csv_write(info=[], file_='output.csv', encoding='utf-8',
`encoding` refers to how the file should be written. `encoding` refers to how the file should be written.
`kwargs` will be passed to csv.writer() `kwargs` will be passed to csv.writer()
""" """
if info is None:
info = []
assert ( assert (
type(info) is list type(info) is list
and len(info) > 0 and len(info) > 0
@ -403,19 +411,19 @@ class MyOD(collections.OrderedDict):
return None return None
def line_drawing_unordered_list(l): def line_drawing_unordered_list(list_):
"""Draw an old-fashioned unordered list. """Draw an old-fashioned unordered list.
Unorderd list example Unordered list example
An element An element
Another element Another element
Last element Last element
""" """
result = "" result = ""
if l: if list_:
for x in l[:-1]: for x in list_[:-1]:
result += "{}\n".format(x) result += "{}\n".format(x)
result += "{}".format(l[-1]) result += "{}".format(list_[-1])
return result return result
@ -444,7 +452,7 @@ def datetime_to_str(d):
return '{:%Y-%m-%d %H:%M:%S.%f}'.format(d) return '{:%Y-%m-%d %H:%M:%S.%f}'.format(d)
class MyCounter(): class MyCounter:
"""Counter object, with a `lvl` method incrementing `n` property.""" """Counter object, with a `lvl` method incrementing `n` property."""
def __init__(self): def __init__(self):
@ -523,7 +531,7 @@ def forwarded(by=None):
Decorator: such decorated functions have effect only if update Decorator: such decorated functions have effect only if update
is forwarded from someone (you can specify `by` whom). is forwarded from someone (you can specify `by` whom).
""" """
def is_forwarded_by(update, by): def is_forwarded_by(update):
if 'forward_from' not in update: if 'forward_from' not in update:
return False return False
if by and update['forward_from']['id'] != by: if by and update['forward_from']['id'] != by:
@ -533,11 +541,11 @@ def forwarded(by=None):
def decorator(view_func): def decorator(view_func):
if asyncio.iscoroutinefunction(view_func): if asyncio.iscoroutinefunction(view_func):
async def decorated(update): async def decorated(update):
if is_forwarded_by(update, by): if is_forwarded_by(update):
return await view_func(update) return await view_func(update)
else: else:
def decorated(update): def decorated(update):
if is_forwarded_by(update, by): if is_forwarded_by(update):
return view_func(update) return view_func(update)
return decorated return decorated
return decorator return decorator
@ -549,7 +557,7 @@ def chat_selective(chat_id=None):
Such decorated functions have effect only if update comes from Such decorated functions have effect only if update comes from
a specific (if `chat_id` is given) or generic chat. a specific (if `chat_id` is given) or generic chat.
""" """
def check_function(update, chat_id): def check_function(update):
if 'chat' not in update: if 'chat' not in update:
return False return False
if chat_id: if chat_id:
@ -560,17 +568,17 @@ def chat_selective(chat_id=None):
def decorator(view_func): def decorator(view_func):
if asyncio.iscoroutinefunction(view_func): if asyncio.iscoroutinefunction(view_func):
async def decorated(update): async def decorated(update):
if check_function(update, chat_id): if check_function(update):
return await view_func(update) return await view_func(update)
else: else:
def decorated(update): def decorated(update):
if check_function(update, chat_id): if check_function(update):
return view_func(update) return view_func(update)
return decorated return decorated
return decorator return decorator
async def sleep_until(when): async def sleep_until(when: Union[datetime.datetime, datetime.timedelta]):
"""Sleep until now > `when`. """Sleep until now > `when`.
`when` could be a datetime.datetime or a datetime.timedelta instance. `when` could be a datetime.datetime or a datetime.timedelta instance.
@ -587,6 +595,8 @@ async def sleep_until(when):
delta = when - datetime.datetime.now() delta = when - datetime.datetime.now()
elif isinstance(when, datetime.timedelta): elif isinstance(when, datetime.timedelta):
delta = when delta = when
else:
delta = datetime.timedelta(seconds=1)
if delta.days >= 0: if delta.days >= 0:
await asyncio.sleep( await asyncio.sleep(
delta.seconds delta.seconds
@ -672,7 +682,7 @@ ARTICOLI[4] = {
} }
class Gettable(): class Gettable:
"""Gettable objects can be retrieved from memory without being duplicated. """Gettable objects can be retrieved from memory without being duplicated.
Key is the primary key. Key is the primary key.
@ -683,19 +693,29 @@ class Gettable():
instances = {} instances = {}
def __init__(self, *args, key=None, **kwargs):
if key is None:
key = args[0]
if key not in self.__class__.instances:
self.__class__.instances[key] = self
@classmethod @classmethod
def get(cls, key, *args, **kwargs): def get(cls, *args, key=None, **kwargs):
"""Instantiate and/or retrieve Gettable object. """Instantiate and/or retrieve Gettable object.
SubClass.instances is searched if exists. SubClass.instances is searched if exists.
Gettable.instances is searched otherwise. Gettable.instances is searched otherwise.
""" """
if key is None:
key = args[0]
else:
kwargs['key'] = key
if key not in cls.instances: if key not in cls.instances:
cls.instances[key] = cls(key, *args, **kwargs) cls.instances[key] = cls(*args, **kwargs)
return cls.instances[key] return cls.instances[key]
class Confirmable(): class Confirmable:
"""Confirmable objects are provided with a confirm instance method. """Confirmable objects are provided with a confirm instance method.
It evaluates True if it was called within self._confirm_timedelta, It evaluates True if it was called within self._confirm_timedelta,
@ -715,6 +735,7 @@ class Confirmable():
confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA confirm_timedelta = self.__class__.CONFIRM_TIMEDELTA
elif type(confirm_timedelta) is int: elif type(confirm_timedelta) is int:
confirm_timedelta = datetime.timedelta(seconds=confirm_timedelta) confirm_timedelta = datetime.timedelta(seconds=confirm_timedelta)
self._confirm_timedelta = None
self.set_confirm_timedelta(confirm_timedelta) self.set_confirm_timedelta(confirm_timedelta)
self._confirm_datetimes = {} self._confirm_datetimes = {}
@ -756,18 +777,18 @@ class Confirmable():
return True return True
class HasBot(): class HasBot:
"""Objects having a Bot subclass object as `.bot` attribute. """Objects having a Bot subclass object as `.bot` attribute.
HasBot objects have a .bot and .db properties for faster access. HasBot objects have a .bot and .db properties for faster access.
""" """
bot = None _bot = None
@property @property
def bot(self): def bot(self):
"""Class bot.""" """Class bot."""
return self.__class__.bot return self.__class__._bot
@property @property
def db(self): def db(self):
@ -777,7 +798,7 @@ class HasBot():
@classmethod @classmethod
def set_bot(cls, bot): def set_bot(cls, bot):
"""Change class bot.""" """Change class bot."""
cls.bot = bot cls._bot = bot
class CachedPage(Gettable): class CachedPage(Gettable):
@ -815,6 +836,7 @@ class CachedPage(Gettable):
self._page = None self._page = None
self._last_update = datetime.datetime.now() - self.cache_time self._last_update = datetime.datetime.now() - self.cache_time
self._async_get_kwargs = async_get_kwargs self._async_get_kwargs = async_get_kwargs
super().__init__(key=url)
@property @property
def url(self): def url(self):
@ -861,7 +883,6 @@ class CachedPage(Gettable):
exc_info=False exc_info=False
) # Set exc_info=True to debug ) # Set exc_info=True to debug
return 1 return 1
return 1
async def get_page(self): async def get_page(self):
"""Refresh if necessary and return web page.""" """Refresh if necessary and return web page."""
@ -878,14 +899,17 @@ class Confirmator(Gettable, Confirmable):
def __init__(self, key, *args, confirm_timedelta=None): def __init__(self, key, *args, confirm_timedelta=None):
"""Call Confirmable.__init__ passing `confirm_timedelta`.""" """Call Confirmable.__init__ passing `confirm_timedelta`."""
Confirmable.__init__(self, confirm_timedelta) Confirmable.__init__(self, confirm_timedelta)
Gettable.__init__(self, key=key, *args)
def get_cleaned_text(update, bot=None, replace=[], strip='/ @'): def get_cleaned_text(update, bot=None, replace=None, strip='/ @'):
"""Clean `update`['text'] and return it. """Clean `update`['text'] and return it.
Strip `bot`.name and items to be `replace`d from the beginning of text. Strip `bot`.name and items to be `replace`d from the beginning of text.
Strip `strip` characters from both ends. Strip `strip` characters from both ends.
""" """
if replace is None:
replace = []
if bot is not None: if bot is not None:
replace.append( replace.append(
'@{.name}'.format( '@{.name}'.format(
@ -1120,9 +1144,6 @@ WEEKDAY_NAMES_ENG = ["Monday", "Tuesday", "Wednesday", "Thursday",
def _period_parser(text, result): def _period_parser(text, result):
succeeded = False
if text in ('every', 'ogni',):
succeeded = True
if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG: if text.title() in WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG:
day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title()) day_code = (WEEKDAY_NAMES_ITA + WEEKDAY_NAMES_ENG).index(text.title())
if day_code > 6: if day_code > 6:
@ -1196,7 +1217,8 @@ def parse_datetime_interval_string(text):
parsers = [] parsers = []
result_text, result_datetime, result_timedelta = [], None, None result_text, result_datetime, result_timedelta = [], None, None
is_quoted_text = False is_quoted_text = False
text = re.sub('\s\s+', ' ', text) # Replace multiple spaces with single space character # Replace multiple spaces with single space character
text = re.sub(r'\s\s+', ' ', text)
for word in text.split(' '): for word in text.split(' '):
if word.count('"') % 2: if word.count('"') % 2:
is_quoted_text = not is_quoted_text is_quoted_text = not is_quoted_text
@ -1247,7 +1269,7 @@ def parse_datetime_interval_string(text):
recurring_event = True recurring_event = True
type_ = parser['type_'] type_ = parser['type_']
for result in parser['result']: for result in parser['result']:
if not result['ok']: if not isinstance(result, dict) or not result['ok']:
continue continue
if recurring_event and 'weekly' in result and result['weekly']: if recurring_event and 'weekly' in result and result['weekly']:
weekly = True weekly = True
@ -1363,11 +1385,11 @@ def beautydt(dt):
now = datetime.datetime.now() now = datetime.datetime.now()
gap = dt - now gap = dt - now
gap_days = (dt.date() - now.date()).days gap_days = (dt.date() - now.date()).days
result = "{dt:alle %H:%M}".format( result = "alle {dt:%H:%M}".format(
dt=dt dt=dt
) )
if abs(gap) < datetime.timedelta(minutes=30): if abs(gap) < datetime.timedelta(minutes=30):
result += "{dt::%S}".format(dt=dt) result += ":{dt:%S}".format(dt=dt)
if -2 <= gap_days <= 2: if -2 <= gap_days <= 2:
result += " di {dg}".format( result += " di {dg}".format(
dg=DAY_GAPS[gap_days] dg=DAY_GAPS[gap_days]
@ -1493,16 +1515,16 @@ def get_line_by_content(text, key):
return return
def str_to_int(string): def str_to_int(string_):
"""Cast str to int, ignoring non-numeric characters.""" """Cast str to int, ignoring non-numeric characters."""
string = ''.join( string_ = ''.join(
char char
for char in string for char in string_
if char.isnumeric() if char.isnumeric()
) )
if len(string) == 0: if len(string_) == 0:
string = '0' string_ = '0'
return int(string) return int(string_)
def starting_with_or_similar_to(a, b): def starting_with_or_similar_to(a, b):
@ -1581,18 +1603,21 @@ def make_inline_query_answer(answer):
return answer return answer
# noinspection PyUnusedLocal
async def dummy_coroutine(*args, **kwargs): async def dummy_coroutine(*args, **kwargs):
"""Accept everthing as argument and do nothing.""" """Accept everything as argument and do nothing."""
return return
async def send_csv_file(bot, chat_id, query, caption=None, async def send_csv_file(bot, chat_id, query, caption=None,
file_name='File.csv', user_record=None, update=dict()): file_name='File.csv', user_record=None, update=None):
"""Run a query on `bot` database and send result as CSV file to `chat_id`. """Run a query on `bot` database and send result as CSV file to `chat_id`.
Optional parameters `caption` and `file_name` may be passed to this Optional parameters `caption` and `file_name` may be passed to this
function. function.
""" """
if update is None:
update = dict()
try: try:
with bot.db as db: with bot.db as db:
record = db.query( record = db.query(
@ -1627,7 +1652,7 @@ async def send_csv_file(bot, chat_id, query, caption=None,
async def send_part_of_text_file(bot, chat_id, file_path, caption=None, async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
file_name='File.txt', user_record=None, file_name='File.txt', user_record=None,
update=dict(), update=None,
reversed_=True, reversed_=True,
limit=None): limit=None):
"""Send `lines` lines of text file via `bot` in `chat_id`. """Send `lines` lines of text file via `bot` in `chat_id`.
@ -1637,10 +1662,12 @@ async def send_part_of_text_file(bot, chat_id, file_path, caption=None,
way to allow `reversed` files, but it is inefficient and requires a lot way to allow `reversed` files, but it is inefficient and requires a lot
of memory. of memory.
""" """
if update is None:
update = dict()
try: try:
with open(file_path, 'r') as log_file: with open(file_path, 'r') as log_file:
lines = log_file.readlines() lines = log_file.readlines()
if reversed: if reversed_:
lines = lines[::-1] lines = lines[::-1]
if limit: if limit:
lines = lines[:limit] lines = lines[:limit]