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,30 +682,40 @@ 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.
Use classmethod get to instantiate (or retrieve) Gettable objects. Use class method get to instantiate (or retrieve) Gettable objects.
Assign SubClass.instances = {}, otherwise Gettable.instances will Assign SubClass.instances = {}, otherwise Gettable.instances will
contain SubClass objects. contain SubClass objects.
""" """
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,11 +798,11 @@ 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):
"""Cache a webpage and return it during CACHE_TIME, otherwise refresh. """Cache a web page and return it during CACHE_TIME, otherwise refresh.
Usage: Usage:
cached_page = CachedPage.get( cached_page = CachedPage.get(
@ -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):
@ -847,7 +869,7 @@ class CachedPage(Gettable):
return datetime.datetime.now() > self.last_update + self.cache_time return datetime.datetime.now() > self.last_update + self.cache_time
async def refresh(self): async def refresh(self):
"""Update cached webpage.""" """Update cached web page."""
try: try:
self._page = await async_get(self.url, **self.async_get_kwargs) self._page = await async_get(self.url, **self.async_get_kwargs)
self._last_update = datetime.datetime.now() self._last_update = datetime.datetime.now()
@ -861,10 +883,9 @@ 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 webpage.""" """Refresh if necessary and return web page."""
if self.is_old: if self.is_old:
await self.refresh() await self.refresh()
return self.page return self.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]