Queer European MD passionate about IT

languages.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328
  1. """Bot support for multiple languages."""
  2. # Standard library modules
  3. import asyncio
  4. from collections import OrderedDict
  5. import logging
  6. # Project modules
  7. from .messages import default_language_messages
  8. from .utilities import extract, make_button, make_inline_keyboard
  9. class MultiLanguageObject(object):
  10. """Make bot inherit from this class to make it support multiple languages.
  11. Call MultiLanguageObject().get_message(
  12. field1, field2, ...,
  13. update, user_record, language,
  14. format_kwarg1, format_kwarg2, ...
  15. ) to get the corresponding message in the selected language.
  16. """
  17. def __init__(self, *args,
  18. messages=None,
  19. default_language='en',
  20. missing_message="Invalid message!",
  21. supported_languages=None,
  22. **kwargs):
  23. """Instantiate MultiLanguageObject, setting its attributes."""
  24. if messages is None:
  25. messages = dict()
  26. self.messages = messages
  27. self._default_language = default_language
  28. self._missing_message = missing_message
  29. if supported_languages is None:
  30. supported_languages = OrderedDict(
  31. {
  32. self.default_language: OrderedDict(
  33. name=self.default_language,
  34. flag=''
  35. )
  36. }
  37. )
  38. self._supported_languages = supported_languages
  39. @property
  40. def default_language(self):
  41. """Return default language."""
  42. return self._default_language
  43. def set_default_language(self, language):
  44. """Set default language."""
  45. self._default_language = language
  46. @property
  47. def missing_message(self):
  48. """Return this message when a proper message can not be found."""
  49. return self._missing_message
  50. def set_missing_message(self, message):
  51. """Set message to be returned where a proper one can not be found."""
  52. self._missing_message = message
  53. @property
  54. def supported_languages(self):
  55. """Return dict of supported languages.
  56. If it is not set, return default language only without flag.
  57. """
  58. return self._supported_languages
  59. def add_supported_languages(self, languages):
  60. """Add some `languages` to supported languages.
  61. Example
  62. ```python
  63. languages = {
  64. 'en': {
  65. 'flag': '🇬🇧',
  66. 'name': 'English'
  67. },
  68. 'it': {
  69. 'flag': '🇮🇹',
  70. 'name': 'Italiano'
  71. }
  72. }
  73. ```
  74. """
  75. assert type(languages) is dict, "Supported languages must be in a dict"
  76. if len(languages) == 0:
  77. return
  78. if self._supported_languages is None:
  79. self._supported_languages = dict()
  80. self._supported_languages.update(languages)
  81. def get_language(self, update=None, user_record=None, language=None):
  82. """Get language.
  83. Language will be the first non-null value of this list:
  84. - `language` parameter
  85. - `user_record['selected_language_code']`: language selected by user
  86. - `update['language_code']`: language of incoming telegram update
  87. - Fallback to default language if none of the above fits
  88. """
  89. if user_record is None:
  90. user_record = dict()
  91. if update is None:
  92. update = dict()
  93. if (
  94. language is None
  95. and 'selected_language_code' in user_record
  96. ):
  97. language = user_record['selected_language_code']
  98. if (
  99. language is None
  100. and 'from' in update
  101. and 'language_code' in update['from']
  102. ):
  103. language = update['from']['language_code']
  104. return language or self.default_language
  105. def get_message(self, *fields, update=None, user_record=None,
  106. default_message=None, language=None, messages=None, **format_kwargs):
  107. """Given a list of strings (`fields`), return proper message.
  108. Language will be determined by `get_language` method.
  109. `format_kwargs` will be passed to format function on the result.
  110. """
  111. # Choose language
  112. if update is None:
  113. update = dict()
  114. if user_record is None:
  115. user_record = dict()
  116. language = self.get_language(
  117. update=update,
  118. user_record=user_record,
  119. language=language
  120. )
  121. # Find result for `language`
  122. result = messages or self.messages
  123. for field in fields:
  124. if field not in result:
  125. if not default_message:
  126. logging.debug(
  127. "Please define self.message{f}".format(
  128. f=''.join(
  129. '[\'{field}\']'.format(
  130. field=field
  131. )
  132. for field in fields
  133. )
  134. )
  135. )
  136. return default_message or self.missing_message
  137. result = result[field]
  138. if language not in result:
  139. # For specific languages, try generic ones
  140. language = language.partition('-')[0]
  141. if language not in result:
  142. language = 'en'
  143. if language not in result:
  144. if not default_message:
  145. logging.debug(
  146. "Please define self.message{f}['en']".format(
  147. f=''.join(
  148. '[\'{field}\']'.format(
  149. field=field
  150. )
  151. for field in fields
  152. )
  153. )
  154. )
  155. return default_message or self.missing_message
  156. if type(result) is str:
  157. return result
  158. return result[language].format(
  159. **format_kwargs
  160. )
  161. async def _language_command(bot, update, user_record):
  162. text, reply_markup = get_language_panel(
  163. bot=bot,
  164. update=update,
  165. user_record=user_record
  166. )
  167. return dict(
  168. text=text,
  169. reply_markup=reply_markup
  170. )
  171. def get_language_panel(bot, update, user_record):
  172. """Get language panel for user.
  173. Return text and reply_markup of the message about user's language
  174. preferences.
  175. """
  176. text = bot.get_message(
  177. 'language', 'language_panel', 'text',
  178. update=update, user_record=user_record,
  179. )
  180. text += "\n"
  181. if 'selected_language_code' in user_record:
  182. current_code = user_record['selected_language_code']
  183. else:
  184. current_code = None
  185. for code, language in bot.supported_languages.items():
  186. text += (f"\n{'✅' if code == current_code else '☑️'} "
  187. f"{language['name']} {language['flag']}")
  188. reply_markup = make_inline_keyboard(
  189. [
  190. make_button(
  191. text=(
  192. f"{'✅' if code == current_code else '☑️'} "
  193. f"{language['name']} {language['flag']}"
  194. ),
  195. prefix='lang:///',
  196. delimiter='|',
  197. data=['set', code]
  198. )
  199. for code, language in bot.supported_languages.items()
  200. ],
  201. 3
  202. )
  203. return text, reply_markup
  204. async def _language_button(bot, update, user_record, data):
  205. result, text, reply_markup = '', '', None
  206. if len(data) > 1 and data[0] == 'set':
  207. # If message is already updated, do not update it
  208. if (
  209. 'selected_language_code' in user_record
  210. and data[1] == user_record['selected_language_code']
  211. and data[1] in bot.supported_languages
  212. and bot.supported_languages[data[1]]['flag'] in extract(
  213. update['message']['text'],
  214. starter='✅',
  215. ender='\n'
  216. )
  217. ):
  218. return
  219. # If database-stored information is not updated, update it
  220. if (
  221. 'selected_language_code' not in user_record
  222. or data[1] != user_record['selected_language_code']
  223. ):
  224. with bot.db as db:
  225. db['users'].update(
  226. dict(
  227. selected_language_code=data[1],
  228. id=user_record['id']
  229. ),
  230. ['id'],
  231. ensure=True
  232. )
  233. user_record['selected_language_code'] = data[1]
  234. if 'chat' in update['message'] and update['message']['chat']['id'] > 0:
  235. asyncio.ensure_future(
  236. bot.send_message(
  237. text=bot.get_message(
  238. 'language', 'language_button', 'language_set',
  239. update=update['message'], user_record=user_record
  240. ),
  241. chat_id=update['message']['chat']['id']
  242. )
  243. )
  244. if len(data) == 0 or data[0] in ('show', 'set'):
  245. text, reply_markup = get_language_panel(bot=bot, update=update, user_record=user_record)
  246. if text:
  247. return dict(
  248. text=result,
  249. edit=dict(
  250. text=text,
  251. reply_markup=reply_markup
  252. )
  253. )
  254. return result
  255. def init(telegram_bot,
  256. language_messages=None,
  257. show_in_keyboard=True,
  258. supported_languages=None):
  259. """Set language support to `bot`."""
  260. if supported_languages is None:
  261. supported_languages = {}
  262. from .bot import Bot
  263. assert isinstance(telegram_bot, MultiLanguageObject), (
  264. "Bot must be a MultiLanguageObject subclass in order to support "
  265. "multiple languages."
  266. )
  267. assert isinstance(telegram_bot, Bot), (
  268. "Bot must be a davtelepot Bot subclass in order to support "
  269. "command and button decorators."
  270. )
  271. if language_messages is None:
  272. language_messages = default_language_messages
  273. telegram_bot.messages['language'] = language_messages
  274. telegram_bot.add_supported_languages(supported_languages)
  275. aliases = [
  276. alias
  277. for alias in language_messages[
  278. 'language_command']['alias'].values()
  279. ]
  280. @telegram_bot.command(
  281. command='/language',
  282. aliases=aliases,
  283. reply_keyboard_button=language_messages['language_command'][
  284. 'reply_keyboard_button'],
  285. show_in_keyboard=show_in_keyboard,
  286. description=language_messages['language_command']['description'],
  287. authorization_level='everybody'
  288. )
  289. async def language_command(bot, update, user_record):
  290. return await _language_command(bot, update, user_record)
  291. @telegram_bot.button(
  292. prefix='lang:///',
  293. separator='|',
  294. description=language_messages['language_button']['description'],
  295. authorization_level='everybody'
  296. )
  297. async def language_button(bot, update, user_record, data):
  298. return await _language_button(bot, update, user_record, data)