Queer European MD passionate about IT

administration_tools.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. """Administration tools for telegram bots.
  2. Usage:
  3. ```
  4. import davtelepot
  5. my_bot = davtelepot.Bot.get('my_token', 'my_database.db')
  6. davtelepot.admin_tools.init(my_bot)
  7. ```
  8. """
  9. # Third party modules
  10. from davtelepot.utilities import (
  11. async_wrapper, Confirmator, get_cleaned_text, get_user, escape_html_chars,
  12. extract, line_drawing_unordered_list, make_button, make_inline_keyboard,
  13. remove_html_tags
  14. )
  15. TALK_MESSAGES = dict(
  16. admin_session_ended=dict(
  17. en=(
  18. 'Session with user {u} ended.'
  19. ),
  20. it=(
  21. 'Sessione terminata con l\'utente {u}.'
  22. ),
  23. ),
  24. admin_warning=dict(
  25. en=(
  26. 'You are now talking to {u}.\n'
  27. 'Until you end this session, your messages will be '
  28. 'forwarded to each other.'
  29. ),
  30. it=(
  31. 'Sei ora connesso con {u}.\n'
  32. 'Finché non chiuderai la connessione, i messaggi che scriverai '
  33. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  34. ),
  35. ),
  36. end_session=dict(
  37. en=(
  38. 'End session?'
  39. ),
  40. it=(
  41. 'Chiudere la sessione?'
  42. ),
  43. ),
  44. help_text=dict(
  45. en='Press the button to search for user.',
  46. it='Premi il pulsante per scegliere un utente.'
  47. ),
  48. search_button=dict(
  49. en="🔍 Search for user",
  50. it="🔍 Cerca utente",
  51. ),
  52. select_user=dict(
  53. en='Which user would you like to talk to?',
  54. it='Con quale utente vorresti parlare?'
  55. ),
  56. user_not_found=dict(
  57. en=(
  58. "Sory, but no user matches your query for\n"
  59. "<code>{q}</code>"
  60. ),
  61. it=(
  62. "Spiacente, ma nessun utente corrisponde alla ricerca per\n"
  63. "<code>{q}</code>"
  64. ),
  65. ),
  66. instructions=dict(
  67. en=(
  68. 'Write a part of name, surname or username of the user you want '
  69. 'to talk to.'
  70. ),
  71. it=(
  72. 'Scrivi una parte del nome, cognome o username dell\'utente con '
  73. 'cui vuoi parlare.'
  74. ),
  75. ),
  76. stop=dict(
  77. en=(
  78. 'End session'
  79. ),
  80. it=(
  81. 'Termina la sessione'
  82. ),
  83. ),
  84. user_session_ended=dict(
  85. en=(
  86. 'Session with admin {u} ended.'
  87. ),
  88. it=(
  89. 'Sessione terminata con l\'amministratore {u}.'
  90. ),
  91. ),
  92. user_warning=dict(
  93. en=(
  94. '{u}, admin of this bot, wants to talk to you.\n'
  95. 'Until this session is ended by {u}, your messages will be '
  96. 'forwarded to each other.'
  97. ),
  98. it=(
  99. '{u}, amministratore di questo bot, vuole parlare con te.\n'
  100. 'Finché non chiuderà la connessione, i messaggi che scriverai '
  101. 'qui saranno inoltrati a {u}, e ti inoltrerò i suoi.'
  102. ),
  103. ),
  104. # key=dict(
  105. # en='',
  106. # it='',
  107. # ),
  108. # key=dict(
  109. # en=(
  110. # ''
  111. # ),
  112. # it=(
  113. # ''
  114. # ),
  115. # ),
  116. )
  117. async def _forward_to(update, bot, sender, addressee, is_admin=False):
  118. if update['text'].lower() in ['stop'] and is_admin:
  119. with bot.db as db:
  120. admin_record = db['users'].find_one(
  121. telegram_id=sender
  122. )
  123. session_record = db['talking_sessions'].find_one(
  124. admin=admin_record['id'],
  125. cancelled=0
  126. )
  127. user_record = db['users'].find_one(
  128. id=session_record['user']
  129. )
  130. await end_session(
  131. bot=bot,
  132. user_record=user_record,
  133. admin_record=admin_record
  134. )
  135. else:
  136. bot.set_custom_parser(
  137. await async_wrapper(
  138. _forward_to,
  139. bot=bot,
  140. sender=sender,
  141. addressee=addressee,
  142. is_admin=is_admin
  143. ),
  144. sender
  145. )
  146. await bot.forward_message(
  147. chat_id=addressee,
  148. update=update
  149. )
  150. return
  151. def get_talk_panel(update, bot, text=''):
  152. """Return text and reply markup of talk panel.
  153. `text` may be:
  154. - `user_id` as string
  155. - `username` as string
  156. - `''` (empty string) for main menu (default)
  157. """
  158. users = []
  159. if len(text):
  160. with bot.db as db:
  161. if text.isnumeric():
  162. users = list(
  163. db['users'].find(id=int(text))
  164. )
  165. else:
  166. users = list(
  167. db.query(
  168. """SELECT *
  169. FROM users
  170. WHERE COALESCE(
  171. first_name || last_name || username,
  172. last_name || username,
  173. first_name || username,
  174. username,
  175. first_name || last_name,
  176. last_name,
  177. first_name
  178. ) LIKE '%{username}%'
  179. ORDER BY LOWER(
  180. COALESCE(
  181. first_name || last_name || username,
  182. last_name || username,
  183. first_name || username,
  184. username,
  185. first_name || last_name,
  186. last_name,
  187. first_name
  188. )
  189. )
  190. LIMIT 26
  191. """.format(
  192. username=text
  193. )
  194. )
  195. )
  196. if len(text) == 0:
  197. text = (
  198. bot.get_message(
  199. 'talk',
  200. 'help_text',
  201. update=update,
  202. q=escape_html_chars(
  203. remove_html_tags(text)
  204. )
  205. )
  206. )
  207. reply_markup = make_inline_keyboard(
  208. [
  209. make_button(
  210. bot.get_message(
  211. 'talk', 'search_button',
  212. update=update
  213. ),
  214. prefix='talk:///',
  215. data=['search']
  216. )
  217. ],
  218. 1
  219. )
  220. elif len(users) == 0:
  221. text = (
  222. bot.get_message(
  223. 'talk',
  224. 'user_not_found',
  225. update=update,
  226. q=escape_html_chars(
  227. remove_html_tags(text)
  228. )
  229. )
  230. )
  231. reply_markup = make_inline_keyboard(
  232. [
  233. make_button(
  234. bot.get_message(
  235. 'talk', 'search_button',
  236. update=update
  237. ),
  238. prefix='talk:///',
  239. data=['search']
  240. )
  241. ],
  242. 1
  243. )
  244. else:
  245. text = "{header}\n\n{u}{etc}".format(
  246. header=bot.get_message(
  247. 'talk', 'select_user',
  248. update=update
  249. ),
  250. u=line_drawing_unordered_list(
  251. [
  252. get_user(user)
  253. for user in users[:25]
  254. ]
  255. ),
  256. etc=(
  257. '\n\n[...]'
  258. if len(users) > 25
  259. else ''
  260. )
  261. )
  262. reply_markup = make_inline_keyboard(
  263. [
  264. make_button(
  265. '👤 {u}'.format(
  266. u=get_user(
  267. {
  268. key: val
  269. for key, val in user.items()
  270. if key in (
  271. 'first_name',
  272. 'last_name',
  273. 'username'
  274. )
  275. }
  276. )
  277. ),
  278. prefix='talk:///',
  279. data=[
  280. 'select',
  281. user['id']
  282. ]
  283. )
  284. for user in users[:25]
  285. ],
  286. 2
  287. )
  288. return text, reply_markup
  289. async def _talk_command(update, bot):
  290. text = get_cleaned_text(
  291. update,
  292. bot,
  293. ['talk']
  294. )
  295. text, reply_markup = get_talk_panel(update, bot, text)
  296. return dict(
  297. text=text,
  298. parse_mode='HTML',
  299. reply_markup=reply_markup,
  300. )
  301. async def start_session(bot, user_record, admin_record):
  302. """Start talking session between user and admin.
  303. Register session in database, so it gets loaded before message_loop starts.
  304. Send a notification both to admin and user, set custom parsers and return.
  305. """
  306. with bot.db as db:
  307. db['talking_sessions'].insert(
  308. dict(
  309. user=user_record['id'],
  310. admin=admin_record['id'],
  311. cancelled=0
  312. )
  313. )
  314. await bot.send_message(
  315. chat_id=user_record['telegram_id'],
  316. text=bot.get_message(
  317. 'talk', 'user_warning',
  318. user_record=user_record,
  319. u=get_user(admin_record)
  320. )
  321. )
  322. await bot.send_message(
  323. chat_id=admin_record['telegram_id'],
  324. text=bot.get_message(
  325. 'talk', 'admin_warning',
  326. user_record=admin_record,
  327. u=get_user(user_record)
  328. ),
  329. reply_markup=make_inline_keyboard(
  330. [
  331. make_button(
  332. bot.get_message(
  333. 'talk', 'stop',
  334. user_record=admin_record
  335. ),
  336. prefix='talk:///',
  337. data=['stop', user_record['id']]
  338. )
  339. ]
  340. )
  341. )
  342. bot.set_custom_parser(
  343. await async_wrapper(
  344. _forward_to,
  345. bot=bot,
  346. sender=user_record['telegram_id'],
  347. addressee=admin_record['telegram_id'],
  348. is_admin=False
  349. ),
  350. user_record['telegram_id']
  351. )
  352. bot.set_custom_parser(
  353. await async_wrapper(
  354. _forward_to,
  355. bot=bot,
  356. sender=admin_record['telegram_id'],
  357. addressee=user_record['telegram_id'],
  358. is_admin=True
  359. ),
  360. admin_record['telegram_id']
  361. )
  362. return
  363. async def end_session(bot, user_record, admin_record):
  364. """End talking session between user and admin.
  365. Cancel session in database, so it will not be loaded anymore.
  366. Send a notification both to admin and user, clear custom parsers
  367. and return.
  368. """
  369. with bot.db as db:
  370. db['talking_sessions'].update(
  371. dict(
  372. admin=admin_record['id'],
  373. cancelled=1
  374. ),
  375. ['admin']
  376. )
  377. await bot.send_message(
  378. chat_id=user_record['telegram_id'],
  379. text=bot.get_message(
  380. 'talk', 'user_session_ended',
  381. user_record=user_record,
  382. u=get_user(admin_record)
  383. )
  384. )
  385. await bot.send_message(
  386. chat_id=admin_record['telegram_id'],
  387. text=bot.get_message(
  388. 'talk', 'admin_session_ended',
  389. user_record=admin_record,
  390. u=get_user(user_record)
  391. ),
  392. )
  393. for record in (admin_record, user_record, ):
  394. telegram_id = record['telegram_id']
  395. if telegram_id in bot.custom_parsers:
  396. del bot.custom_parsers[telegram_id]
  397. return
  398. async def _talk_button(update, bot):
  399. telegram_id = update['from']['id']
  400. command, *arguments = extract(update['data'], '///').split('|')
  401. result, text, reply_markup = '', '', None
  402. if command == 'search':
  403. bot.set_custom_parser(
  404. await async_wrapper(
  405. _talk_command,
  406. bot=bot
  407. ),
  408. update
  409. )
  410. text = bot.get_message(
  411. 'talk', 'instructions',
  412. update=update
  413. )
  414. reply_markup = None
  415. elif command == 'select':
  416. if (
  417. len(arguments) < 1
  418. or not arguments[0].isnumeric()
  419. ):
  420. result = "Errore!"
  421. else:
  422. with bot.db as db:
  423. user_record = db['users'].find_one(
  424. id=int(arguments[0])
  425. )
  426. admin_record = db['users'].find_one(
  427. telegram_id=telegram_id
  428. )
  429. await start_session(
  430. bot,
  431. user_record=user_record,
  432. admin_record=admin_record
  433. )
  434. elif command == 'stop':
  435. if (
  436. len(arguments) < 1
  437. or not arguments[0].isnumeric()
  438. ):
  439. result = "Errore!"
  440. elif not Confirmator.get('stop_bots').confirm(telegram_id):
  441. result = bot.get_message(
  442. 'talk', 'end_session',
  443. update=update,
  444. )
  445. else:
  446. with bot.db as db:
  447. user_record = db['users'].find_one(
  448. id=int(arguments[0])
  449. )
  450. admin_record = db['users'].find_one(
  451. telegram_id=telegram_id
  452. )
  453. await end_session(
  454. bot,
  455. user_record=user_record,
  456. admin_record=admin_record
  457. )
  458. text = "Session ended."
  459. reply_markup = None
  460. if text:
  461. return dict(
  462. text=result,
  463. edit=dict(
  464. text=text,
  465. parse_mode='HTML',
  466. reply_markup=reply_markup,
  467. disable_web_page_preview=True
  468. )
  469. )
  470. return result
  471. def init(bot):
  472. """Assign parsers, commands, buttons and queries to given `bot`."""
  473. if not hasattr(bot, 'messages'):
  474. bot.messages = dict()
  475. bot.messages['talk'] = TALK_MESSAGES
  476. with bot.db as db:
  477. if 'talking_sessions' not in db.tables:
  478. db['talking_sessions'].insert(
  479. dict(
  480. user=0,
  481. admin=0,
  482. cancelled=1
  483. )
  484. )
  485. @bot.additional_task(when='BEFORE')
  486. async def load_talking_sessions():
  487. sessions = []
  488. with bot.db as db:
  489. for session in db.query(
  490. """SELECT *
  491. FROM talking_sessions
  492. WHERE NOT cancelled
  493. """
  494. ):
  495. sessions.append(
  496. dict(
  497. user_record=db['users'].find_one(
  498. id=session['user']
  499. ),
  500. admin_record=db['users'].find_one(
  501. id=session['admin']
  502. ),
  503. )
  504. )
  505. for session in sessions:
  506. await start_session(
  507. bot=bot,
  508. user_record=session['user_record'],
  509. admin_record=session['admin_record']
  510. )
  511. @bot.command(command='/talk', aliases=[], show_in_keyboard=False,
  512. description="Choose a user and forward messages to each "
  513. "other.",
  514. authorization_level='admin')
  515. async def talk_command(update):
  516. return await _talk_command(update, bot)
  517. @bot.button(prefix='talk:///', authorization_level='admin')
  518. async def talk_button(update):
  519. return await _talk_button(update, bot)
  520. return