customservice.py 54 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. # customservice.py
  4. # Copyright (C) 2019-2021 github.com/googlehosts Group:Z
  5. #
  6. # This module is part of googlehosts/telegram-repeater and is released under
  7. # the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt
  8. #
  9. # This program is free software: you can redistribute it and/or modify
  10. # it under the terms of the GNU Affero General Public License as published by
  11. # the Free Software Foundation, either version 3 of the License, or
  12. # any later version.
  13. #
  14. # This program is distributed in the hope that it will be useful,
  15. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. # GNU Affero General Public License for more details.
  18. #
  19. # You should have received a copy of the GNU Affero General Public License
  20. # along with this program. If not, see <https://www.gnu.org/licenses/>.
  21. from __future__ import annotations
  22. import asyncio
  23. import base64
  24. import gettext
  25. import hashlib
  26. import logging
  27. import random
  28. import re
  29. import time
  30. import traceback
  31. from configparser import ConfigParser
  32. from datetime import datetime
  33. from typing import (Awaitable, Callable, Dict, List, Mapping, Optional,
  34. Sequence, Tuple, TypeVar, Union)
  35. import aioredis
  36. import asyncpg
  37. import pyrogram
  38. import pyrogram.errors
  39. from pyrogram import Client, filters
  40. from pyrogram.handlers import CallbackQueryHandler, MessageHandler
  41. from pyrogram.types import (CallbackQuery, InlineKeyboardButton,
  42. InlineKeyboardMarkup, KeyboardButton, Message,
  43. ReplyKeyboardMarkup, ReplyKeyboardRemove)
  44. import utils
  45. logger = logging.getLogger('telegram-repeater').getChild('customservice')
  46. translation = gettext.translation('customservice', 'translations/',
  47. languages=[utils.get_language()], fallback=True)
  48. _T = translation.gettext
  49. _problemT = TypeVar('_problemT', Dict, str, bool, int)
  50. _anyT = TypeVar('_anyT')
  51. class TextParser(utils.TextParser):
  52. def __init__(self, msg: Message):
  53. super().__init__()
  54. self._msg = self.BuildMessage(msg)
  55. self.parsed_msg = self.parse_main()
  56. def __str__(self) -> str:
  57. return self.parsed_msg
  58. class Ticket:
  59. def __init__(self, msg: Message, section: str, status: str):
  60. self._origin_msg = TextParser(msg).parsed_msg
  61. self.hash_value = CustomServiceBot.hash_msg(msg)
  62. self.section = section
  63. self.status = status
  64. self.sql = (
  65. '''INSERT INTO "tickets" ("user_id", "hash", "timestamp", "origin_msg", "section", "status")
  66. VALUES ($1, $2, CURRENT_TIMESTAMP, $3, $4, $5)''',
  67. msg.chat.id, self.hash_value, base64.b64encode(self._origin_msg.encode()).decode(), self.section,
  68. self.status
  69. )
  70. def __str__(self) -> Tuple[str, int, str, str, str, str]:
  71. return self.sql
  72. class RemovePunctuations:
  73. def __init__(self, enable: bool, items: List[str]):
  74. self.enable = enable
  75. self.items = items
  76. def replace(self, text: str) -> str:
  77. if not self.enable:
  78. return text
  79. return ''.join(x for x in text if x not in self.items)
  80. class ProblemSet:
  81. _self = None
  82. def __init__(self, redis_conn: aioredis.Redis, problem_set: Mapping[str, _anyT],
  83. remove_punctuations: RemovePunctuations):
  84. self._redis: aioredis.Redis = redis_conn
  85. self._prefix: str = utils.get_random_string()
  86. self.version: int = problem_set['version']
  87. self.problem_length: int = len(problem_set['problems']['problem_set'])
  88. self.sample_problem: Dict[str, str] = problem_set['problems'].get('sample_problem')
  89. self._has_sample: bool = bool(self.sample_problem)
  90. self.remove_punctuations: RemovePunctuations = remove_punctuations
  91. async def init(self, problem_set: Mapping[str, _anyT]):
  92. if self.sample_problem:
  93. await self._redis.mset({f'{self._prefix}_{key}_sample': item for key, item in self.sample_problem.items()})
  94. for x in range(self.problem_length):
  95. problems = problem_set['problems']['problem_set']
  96. if problems[x].get('use_regular_expression'):
  97. await self._redis.set(f'{self._prefix}_re_{x}', 1)
  98. await self._redis.set(f'{self._prefix}_Q_{x}', problems[x]['Q'])
  99. await self._redis.set(f'{self._prefix}_A_{x}', self.remove_punctuations.replace(problems[x]['A']))
  100. await self._redis.set(f'{self._prefix}_OA_{x}', problems[x]['A'])
  101. @classmethod
  102. async def create(cls, redis_conn: aioredis.Redis, problem_set: Dict[str, _anyT],
  103. remove_punctuations: RemovePunctuations) -> 'ProblemSet':
  104. self = ProblemSet(redis_conn, problem_set, remove_punctuations)
  105. await self.init(problem_set)
  106. return self
  107. async def destroy(self) -> None:
  108. for x in range(self.problem_length):
  109. await self._redis.delete(f'{self._prefix}_re_{x}')
  110. await self._redis.delete(f'{self._prefix}_Q_{x}')
  111. await self._redis.delete(f'{self._prefix}_A_{x}')
  112. await self._redis.delete(f'{self._prefix}_OA_{x}')
  113. if self._has_sample:
  114. await self._redis.delete(f'{self._prefix}_Q_sample')
  115. await self._redis.delete(f'{self._prefix}_A_sample')
  116. def get_random_number(self) -> int:
  117. return random.randint(0, self.problem_length - 1)
  118. async def get(self, key: int) -> Dict[str, str]:
  119. return {'use_regular_expression': await self._redis.get(f'{self._prefix}_re_{key}'),
  120. 'Q': (await self._redis.get(f'{self._prefix}_Q_{key}')).decode(),
  121. 'A': (await self._redis.get(f'{self._prefix}_A_{key}')).decode()}
  122. async def get_origin(self, key: int) -> str:
  123. return (await self._redis.get(f'{self._prefix}_OA_{key}')).decode()
  124. @property
  125. def length(self) -> int:
  126. return self.problem_length
  127. @property
  128. def has_sample(self) -> bool:
  129. return self._has_sample
  130. async def get_sample(self) -> Optional[Mapping[str, str]]:
  131. if not self._has_sample:
  132. return None
  133. return {'Q': (await self._redis.get(f'{self._prefix}_Q_sample')).decode(),
  134. 'A': (await self._redis.get(f'{self._prefix}_A_sample')).decode()}
  135. @staticmethod
  136. def get_instance() -> ProblemSet:
  137. if ProblemSet._self is None:
  138. raise RuntimeError()
  139. return ProblemSet._self
  140. @staticmethod
  141. async def init_instance(redis_conn: aioredis.Redis, problem_set: Dict[str, _problemT],
  142. remove_punctuations: RemovePunctuations) -> 'ProblemSet':
  143. ProblemSet._self = await ProblemSet.create(redis_conn, problem_set, remove_punctuations)
  144. return ProblemSet._self
  145. class JoinGroupVerify:
  146. class ProblemVersionException(Exception):
  147. pass
  148. def __init__(self, conn: utils.PgSQLdb, botapp: Client, target_group: int, working_group: int):
  149. self.conn: utils.PgSQLdb = conn
  150. self.botapp: Client = botapp
  151. self.target_group: int = target_group
  152. self.working_group: int = working_group
  153. self._revoke_tracker_coro: utils.InviteLinkTracker = None # type: ignore
  154. self._keyboard: Dict[str, InlineKeyboardMarkup] = {}
  155. self._welcome_msg: Optional[str] = None # type: ignore
  156. self.remove_punctuations: Optional[RemovePunctuations] = None
  157. self.problems: Optional[ProblemSet] = None
  158. self.max_retry: Optional[int] = None # type: ignore
  159. self.max_retry_error: Optional[str] = None # type: ignore
  160. self.max_retry_error_detail: Optional[str] = None # type: ignore
  161. self.try_again: Optional[str] = None # type: ignore
  162. self._send_link_confirm: Optional[bool] = None # type: ignore
  163. self._confirm_message: Optional[str] = None # type: ignore
  164. self._confirm_button_text: Optional[str] = None # type: ignore
  165. def init(self) -> None:
  166. self.botapp.add_handler(MessageHandler(self.handle_bot_private, filters.private & filters.text))
  167. def init_other_object(self, problem_set: Dict[str, _anyT]):
  168. self._revoke_tracker_coro: utils.InviteLinkTracker = utils.InviteLinkTracker(
  169. self.botapp,
  170. problem_set,
  171. self.target_group
  172. )
  173. self._welcome_msg: str = problem_set['messages']['welcome_msg']
  174. self.max_retry: int = problem_set['configs']['max_retry']
  175. self.max_retry_error: str = problem_set['messages']['max_retry_error']
  176. self.max_retry_error_detail: str = problem_set['messages']['max_retry_error_detail']
  177. self.try_again: str = problem_set['messages']['try_again']
  178. self._send_link_confirm: bool = problem_set.get('confirm_msg') and problem_set['confirm_msg'].get(
  179. 'enable') # type: ignore
  180. if self._send_link_confirm:
  181. self._confirm_message: str = problem_set['confirm_msg']['text']
  182. self._confirm_button_text: str = problem_set['confirm_msg']['button_text']
  183. if problem_set['ticket_bot']['enable']:
  184. self._keyboard = {
  185. 'reply_markup': InlineKeyboardMarkup(
  186. inline_keyboard=[
  187. [InlineKeyboardButton(text=_T('I need help.'), url=problem_set['ticket_bot']['link'])]
  188. ]
  189. )
  190. }
  191. self._revoke_tracker_coro.start()
  192. @classmethod
  193. async def create(cls, conn: utils.PgSQLdb, botapp: Client, target_group: int, working_group: int,
  194. load_problem_set: Callable[[], Dict[str, _problemT]], redis_conn: aioredis.Redis):
  195. self = JoinGroupVerify(conn, botapp, target_group, working_group)
  196. problem_set = load_problem_set()
  197. self.remove_punctuations = RemovePunctuations(
  198. **problem_set['configs'].get('ignore_punctuations', {'enable': False, 'items': []}))
  199. self.problems = await ProblemSet.init_instance(redis_conn, problem_set, self.remove_punctuations)
  200. self.init_other_object(problem_set)
  201. return self
  202. @property
  203. def problem_list(self) -> ProblemSet:
  204. if self.problems is None:
  205. raise RuntimeError()
  206. return self.problems
  207. @property
  208. def revoke_tracker_coro(self) -> utils.InviteLinkTracker:
  209. return self._revoke_tracker_coro
  210. async def query_user_passed(self, user_id: int) -> bool:
  211. sql_obj = await self.conn.query1('''SELECT "passed", "bypass" FROM "exam_user_session" WHERE "user_id" = $1''',
  212. user_id)
  213. return sql_obj is not None and (sql_obj['passed'] or sql_obj['bypass'])
  214. async def handle_bot_private(self, client: Client, msg: Message) -> None:
  215. if msg.text.startswith('/') and msg.text != '/start newbie':
  216. return
  217. user_obj = await self.conn.query1(
  218. '''SELECT "problem_id", "problem_version", "baned", "bypass", "retries", "passed", "unlimited"
  219. FROM "exam_user_session" WHERE "user_id" = $1''',
  220. msg.chat.id)
  221. if msg.text == '/start newbie':
  222. try:
  223. try:
  224. # raise Exception
  225. user = await self.botapp.get_chat_member(self.target_group, msg.chat.id)
  226. # print(user.status)
  227. # if user.status in ('member', 'administrator', 'creator', 'restricted'):
  228. if user.status == 'left':
  229. raise ValueError('left')
  230. await msg.reply(_T('You are already in the group.'))
  231. return
  232. except pyrogram.errors.exceptions.bad_request_400.UserNotParticipant:
  233. pass
  234. except:
  235. logger.exception('Exception occurred while checking user status')
  236. if user_obj is not None:
  237. if user_obj['bypass']:
  238. await self._revoke_tracker_coro.send_link(msg.chat.id, True)
  239. elif user_obj['passed']:
  240. await msg.reply(_T('You have already answered the question.'))
  241. elif user_obj['baned']:
  242. await msg.reply(_T('Due to privacy settings, you are temporarily unable to join this group.'))
  243. else:
  244. await msg.reply(_T('An existing session is currently active.'), True)
  245. else:
  246. random_id = self.problems.get_random_number()
  247. # Query user status
  248. await self.conn.execute(
  249. '''INSERT INTO "exam_user_session" ("user_id", "problem_version", "problem_id", "timestamp")
  250. VALUES ($1, $3, $2, CURRENT_TIMESTAMP)''',
  251. msg.chat.id, random_id, self.problems.version)
  252. await msg.reply(
  253. self._welcome_msg,
  254. parse_mode='html',
  255. disable_web_page_preview=True,
  256. **self._keyboard
  257. )
  258. # Send sample problem
  259. if self.problems.has_sample:
  260. await msg.reply(
  261. _T('For example:\n</b> <code>{Q}</code>\n<b>A:</b> <code>{A}</code>').format(
  262. **await self.problems.get_sample()
  263. ),
  264. parse_mode='html',
  265. disable_web_page_preview=True
  266. )
  267. # Send problem body
  268. await msg.reply(
  269. (await self.problems.get(random_id))['Q'],
  270. # self.problem_set['problems']['problem_set'][random_id]['Q'],
  271. parse_mode='html',
  272. disable_web_page_preview=True
  273. )
  274. except pyrogram.errors.exceptions.bad_request_400.UserIsBlocked:
  275. logger.warning('Caught blocked user %s', msg.chat.id)
  276. await client.send_message(
  277. self.working_group,
  278. _T('The bot is blocked by user {}').format(TextParser.parse_user_markdown(msg.chat.id)),
  279. 'markdown'
  280. )
  281. except:
  282. logger.exception('Unexpect exception occurred in check newbie function')
  283. else:
  284. if user_obj is None:
  285. return
  286. if user_obj['problem_version'] != self.problems.version:
  287. await msg.reply(_T('Problem version updated, please request new problem by submitting a ticket.'))
  288. return
  289. if user_obj['unlimited'] or user_obj['retries'] <= self.max_retry:
  290. if self.valid_answer(msg, await self.problems.get(user_obj['problem_id'])):
  291. await self.conn.execute('''UPDATE "exam_user_session" SET "passed" = true WHERE "user_id" = $1''',
  292. msg.chat.id)
  293. await self.send_link(msg)
  294. return
  295. elif user_obj['bypass']: # and user_obj['passed']:
  296. await self.conn.execute('''UPDATE "exam_user_session" SET "passed" = true WHERE "user_id" = $1''',
  297. msg.chat.id)
  298. await self.send_link(msg)
  299. return
  300. retries = user_obj['retries'] + 2
  301. if retries > self.max_retry:
  302. if retries == self.max_retry + 1:
  303. await msg.reply(
  304. '\n\n'.join((self.max_retry_error, self.max_retry_error_detail)),
  305. parse_mode='html', disable_web_page_preview=True
  306. )
  307. logger.debug('%d %s', msg.chat.id, repr(msg.text))
  308. await self._insert_answer_history(msg)
  309. else:
  310. await msg.reply(self.max_retry_error_detail, parse_mode='html',
  311. disable_web_page_preview=True)
  312. else:
  313. await msg.reply(self.try_again, parse_mode='html', disable_web_page_preview=True)
  314. logger.debug('%d %s', msg.chat.id, repr(msg.text))
  315. await self._insert_answer_history(msg)
  316. await self.conn.execute('''UPDATE "exam_user_session" SET "retries" = $1 WHERE "user_id" = $2''',
  317. retries, msg.chat.id)
  318. async def _insert_answer_history(self, msg: Message) -> None:
  319. await self.conn.execute('''INSERT INTO "answer_history" ("user_id", "body") VALUES ($1, $2)''',
  320. msg.chat.id, msg.text[:200])
  321. async def check_joined_group(self, user_id: int) -> None:
  322. logger.debug('Track %d status', user_id)
  323. await asyncio.sleep(30) # Wait up to 30 second
  324. try:
  325. await self.botapp.get_chat_member(self.target_group, user_id)
  326. except pyrogram.errors.exceptions.bad_request_400.UserNotParticipant:
  327. await self.conn.insert_user_to_banlist(user_id)
  328. await self.botapp.send_message(self.working_group, 'Baned not joined group user {}'.format(
  329. TextParser.parse_user_markdown(user_id)), 'markdown')
  330. logger.info('Baned not joined group user %d', user_id)
  331. async def click_to_join(self, client: Client, msg: CallbackQuery) -> bool:
  332. if msg.data == 'iamready':
  333. if not await self.query_user_passed(msg.message.chat.id):
  334. await msg.answer(_T('Function is not ready, please try again later.'), True)
  335. logger.warning('User clicked but function is not ready during request link')
  336. else:
  337. try:
  338. await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id)
  339. await self._revoke_tracker_coro.send_link(msg.message.chat.id, True)
  340. await msg.answer()
  341. except:
  342. logger.exception('Exception occurred on process click function')
  343. return True
  344. return False
  345. async def send_link(self, msg: Message, from_ticket: bool = False) -> None:
  346. if self._send_link_confirm:
  347. reply_obj = dict(
  348. text=self._confirm_message,
  349. parse_mode='html',
  350. reply_markup=InlineKeyboardMarkup(inline_keyboard=[
  351. [InlineKeyboardButton(text=self._confirm_button_text, callback_data='iamready')]
  352. ])
  353. )
  354. if isinstance(msg, int):
  355. reply_obj.update(dict(chat_id=msg))
  356. await self.botapp.send_message(**reply_obj)
  357. else:
  358. await msg.reply(**reply_obj)
  359. else:
  360. await self._revoke_tracker_coro.send_link(msg.chat.id, from_ticket)
  361. def valid_answer(self, msg: Message, problem_body: Dict[str, str]) -> bool:
  362. text = self.remove_punctuations.replace(msg.text)
  363. if problem_body.get('use_regular_expression', False):
  364. b = bool(re.match(problem_body['A'], text))
  365. else:
  366. b = text == problem_body['A']
  367. logger.debug('verify %s %s == %s', b, text, problem_body['A'])
  368. return b
  369. class CustomServiceBot:
  370. INIT_STATUS = 0
  371. SELECT_SECTION = 1
  372. SEND_QUESTION = 2
  373. SEND_FINISH = 3
  374. RE_TICKET_ID = re.compile(r'[a-f\d]{32}')
  375. def __init__(self, config_file: Union[str, ConfigParser], pgsql_handle: utils.PgSQLdb,
  376. send_link_callback: Optional[Callable[[Message, bool], Awaitable]], redis_conn: aioredis.Redis):
  377. if isinstance(config_file, ConfigParser):
  378. config = config_file
  379. else:
  380. config = ConfigParser()
  381. config.read(config_file)
  382. self.pgsqldb: utils.PgSQLdb = pgsql_handle
  383. self._redis: aioredis.Redis = redis_conn
  384. self.bot_id: int = int(config['custom_service']['custom_api_key'].split(':')[0])
  385. self.bot: Client = Client(
  386. session_name=str(self.bot_id),
  387. bot_token=config['custom_service']['custom_api_key'],
  388. api_id=config['account']['api_id'],
  389. api_hash=config['account']['api_hash']
  390. )
  391. self.help_group: int = config.getint('custom_service', 'help_group')
  392. self.send_link_callback: Optional[Callable[[Message, bool], Awaitable]] = send_link_callback
  393. self.SECTION: List[str] = [
  394. _T("VERIFICATION"),
  395. _T("OTHER")
  396. ]
  397. self.init_handle()
  398. def init_handle(self) -> None:
  399. self.bot.add_handler(MessageHandler(self.handle_start, filters.command('start') & filters.private))
  400. self.bot.add_handler(MessageHandler(self.handle_create, filters.command('create') & filters.private))
  401. self.bot.add_handler(MessageHandler(self.handle_cancel, filters.command('cancel') & filters.private))
  402. self.bot.add_handler(MessageHandler(self.handle_list, filters.command('list') & filters.private))
  403. self.bot.add_handler(MessageHandler(self.handle_close, filters.command('close') & filters.private))
  404. self.bot.add_handler(MessageHandler(self.handle_reply, filters.reply & filters.text & filters.private))
  405. self.bot.add_handler(MessageHandler(self.handle_msg, filters.text & filters.private))
  406. self.bot.add_handler(MessageHandler(self.query_user_status,
  407. filters.chat(self.help_group) & filters.command('q')))
  408. self.bot.add_handler(MessageHandler(self.call_superuser_function,
  409. filters.chat(self.help_group) & filters.reply & filters.command('m')))
  410. self.bot.add_handler(MessageHandler(self.handle_group, filters.reply & filters.chat(self.help_group)))
  411. self.bot.add_handler(MessageHandler(self.handle_other, filters.private))
  412. self.bot.add_handler(CallbackQueryHandler(self.answer))
  413. async def start(self) -> Client:
  414. return await self.bot.start()
  415. async def stop(self) -> Client:
  416. return await self.bot.stop()
  417. @staticmethod
  418. async def idle() -> None:
  419. await pyrogram.idle()
  420. async def active(self) -> None:
  421. await self.start()
  422. await self.idle()
  423. @staticmethod
  424. def hash_msg(msg: Message) -> str:
  425. return hashlib.md5(' '.join(map(str, (msg.from_user.id, msg.date, msg.message_id))).encode()).hexdigest()
  426. def get_hash_from_reply_msg(self, msg: Message) -> str:
  427. if msg.reply_to_message is None or \
  428. msg.reply_to_message.text is None or \
  429. msg.reply_to_message.from_user.id != self.bot_id or \
  430. msg.reply_to_message.entities is None or \
  431. msg.reply_to_message.entities[0].type != 'hashtag':
  432. raise ValueError("hash message info error")
  433. r = self.RE_TICKET_ID.search(msg.reply_to_message.text)
  434. if r is not None:
  435. return r.group(0)
  436. else:
  437. raise ValueError('hash info not found')
  438. def generate_section_pad(self) -> ReplyKeyboardMarkup:
  439. return ReplyKeyboardMarkup(keyboard=[
  440. [KeyboardButton(text=x)] for x in self.SECTION
  441. ], resize_keyboard=True, one_time_keyboard=True)
  442. @staticmethod
  443. def generate_ticket_keyboard(ticket_id: str, user_id: int, closed: bool = False,
  444. other: bool = False) -> InlineKeyboardMarkup:
  445. kb = [
  446. InlineKeyboardButton(text=_T('Close'), callback_data=f'close {ticket_id}'),
  447. InlineKeyboardButton(text=_T('Send link'), callback_data=f'send {user_id}'),
  448. InlineKeyboardButton(text=_T('Block'), callback_data=f'block {user_id}')
  449. ]
  450. if closed:
  451. kb = kb[2:]
  452. elif other:
  453. kb.pop(1)
  454. return InlineKeyboardMarkup(
  455. inline_keyboard=[kb]
  456. )
  457. @staticmethod
  458. def return_bool_emoji(i: _anyT) -> str:
  459. return '\u2705' if i else '\u274c'
  460. async def handle_list(self, _client: Client, msg: Message) -> None:
  461. q = [dict(x) for x in (await self.pgsqldb.query(
  462. '''SELECT "hash", "status" FROM "tickets" WHERE "user_id" = $1 ORDER BY "timestamp" DESC LIMIT 3''',
  463. msg.chat.id))]
  464. if not q:
  465. await msg.reply(_T('You have never used this system before.'), True)
  466. return
  467. for _ticket in q:
  468. _ticket['status'] = self.return_bool_emoji(_ticket['status'] != 'closed') # type: ignore
  469. await msg.reply(_T('Here are the last three tickets (up to 3)\n#{}').format(
  470. '\n#'.join(' '.join(value for _, value in _ticket.items()) for _ticket in q)), True)
  471. async def handle_close(self, client: Client, msg: Message) -> None:
  472. if msg.reply_to_message is not None and msg.text == '/close':
  473. try:
  474. ticket_id = self.get_hash_from_reply_msg(msg)
  475. except ValueError:
  476. await msg.reply(_T(
  477. 'TICKET NUMBER NOT FOUND\n'
  478. 'Please make sure that you have replied to the message which contains the ticket number.'),
  479. True)
  480. return
  481. else:
  482. if len(msg.text) < 8:
  483. await msg.reply(_T(
  484. 'ERROR: COMMAND FORMAT Please use `/close <ticket number>` or'
  485. ' **Reply to the message which contains the ticket number** to close the ticket'),
  486. True, 'markdown', disable_notification=True)
  487. return
  488. ticket_id = msg.text.split()[-1]
  489. if len(ticket_id) != 32:
  490. await msg.reply(_T('ERROR: TICKET NUMBER FORMAT'), True)
  491. return
  492. q = await self.pgsqldb.query1('''SELECT "user_id" FROM "tickets" WHERE "hash" = $1 AND "status" != 'closed' ''',
  493. ticket_id)
  494. if q is None:
  495. await msg.reply(_T('TICKET NUMBER NOT FOUND or TICKET CLOSED'), True)
  496. return
  497. if q['user_id'] != msg.chat.id:
  498. await msg.reply(_T(
  499. '403 Forbidden(You cannot close a ticket created by others. '
  500. 'If this ticket is indeed created by yourself, please report the problem using the same ticket.)'),
  501. True)
  502. return
  503. await self.pgsqldb.execute('''UPDATE "tickets" SET "status" = 'closed' WHERE "user_id" = $1 AND "hash" = $2''',
  504. msg.chat.id, ticket_id)
  505. await self._update_last_time(msg)
  506. await client.send_message(self.help_group,
  507. _T('UPDATE\n[ #{} ]\nThis ticket is already closed by {}').format(
  508. ticket_id,
  509. utils.TextParser.parse_user_markdown(msg.chat.id, _T('Creator'))),
  510. reply_markup=self.generate_ticket_keyboard(ticket_id, msg.chat.id, other=True))
  511. await msg.reply(_T('Close ticket success.'), True)
  512. async def add_user(self, user_id: int, step: int = 0) -> None:
  513. await self.pgsqldb.execute(
  514. '''INSERT INTO "tickets_user" ("user_id", "create_time", "step") VALUES ($1, CURRENT_TIMESTAMP, $2)''',
  515. user_id, step)
  516. async def change_step(self, user_id: int, step: int, section: str = '') -> None:
  517. if section == '':
  518. await self.pgsqldb.execute('''UPDATE "tickets_user" SET "step" = $1 WHERE "user_id" = $2''', step, user_id)
  519. else:
  520. await self.pgsqldb.execute('''UPDATE "tickets_user" SET "step" = $1, "section" = $2 WHERE "user_id" = $3''',
  521. step, section, user_id)
  522. async def query_status(self, user_id: int) -> Optional[asyncpg.Record]:
  523. return await self.pgsqldb.query1('''SELECT "step", "section" FROM "tickets_user" WHERE "user_id" = $1''',
  524. user_id)
  525. async def query_user(self, user_id: int) -> Optional[asyncpg.Record]:
  526. return await self.pgsqldb.query1('''SELECT "section" FROM "tickets_user" WHERE "user_id" = $1''', user_id)
  527. async def set_section(self, user_id: int, section: str) -> None:
  528. await self.pgsqldb.execute('''UPDATE "tickets_user" SET "section" = $1 WHERE "user_id" = $2''', section,
  529. user_id)
  530. async def query_user_exam_status(self, user_id: int) -> Optional[asyncpg.Record]:
  531. return await self.pgsqldb.query1(
  532. '''SELECT "problem_id", "baned", "bypass", "passed", "unlimited", "retries"
  533. FROM "exam_user_session" WHERE "user_id" = $1''',
  534. user_id)
  535. async def handle_start(self, _client: Client, msg: Message) -> None:
  536. q = await self.pgsqldb.query1('''SELECT "last_msg_sent" FROM "tickets_user" WHERE "user_id" = $1''',
  537. msg.chat.id)
  538. await msg.reply(_T(
  539. 'Welcome to Google Hosts Telegram Ticket System\n\n'
  540. 'ATTENTION:PLEASE DO NOT ABUSE THIS SYSTEM. Otherwise there is a possibility of getting blocked.\n\n'
  541. '/create - to create a new ticket\n'
  542. '/list - to list recent tickets\n'
  543. '/close - to close the ticket\n'
  544. '/cancel - to reset'),
  545. True)
  546. if q is None:
  547. await self.add_user(msg.chat.id)
  548. async def handle_create(self, client: Client, msg: Message) -> None:
  549. if await self.flood_check(client, msg):
  550. return
  551. q = await self.pgsqldb.query1(
  552. '''SELECT "hash" FROM "tickets" WHERE "user_id" = $1 AND "status" = 'open' LIMIT 1''',
  553. msg.chat.id)
  554. if q:
  555. await msg.reply(_T('UNABLE TO CREATE A NEW TICKET: An existing ticket is currently open.'), True)
  556. return
  557. sql_obj = await self.pgsqldb.query1('''SELECT "user_id" FROM "tickets_user" WHERE "user_id" = $1''',
  558. msg.chat.id)
  559. await (self.add_user if sql_obj is None else self.change_step)(msg.chat.id, CustomServiceBot.SELECT_SECTION)
  560. await msg.reply(_T('You are creating a new ticket.\n\nPlease choose the correct department.'), True,
  561. reply_markup=self.generate_section_pad())
  562. async def handle_cancel(self, _client: Client, msg: Message) -> None:
  563. await self.change_step(msg.chat.id, CustomServiceBot.INIT_STATUS)
  564. await msg.reply(_T('Reset Successful'), reply_markup=ReplyKeyboardRemove())
  565. async def handle_reply(self, client: Client, msg: Message) -> None:
  566. if await self.flood_check(client, msg):
  567. return
  568. try:
  569. ticket_hash = self.get_hash_from_reply_msg(msg)
  570. except ValueError:
  571. return
  572. # print(self.get_hash_from_reply_msg(msg))
  573. sql_obj = await self.pgsqldb.query1(
  574. '''SELECT "status", "section" FROM "tickets" WHERE "hash" = $1 AND "user_id" = $2''',
  575. ticket_hash, msg.chat.id)
  576. if sql_obj is None or sql_obj['status'] == 'closed':
  577. await msg.reply(_T('TICKET NUMBER NOT FOUND or TICKET CLOSED. REPLY FUNCTION NO LONGER AVAILABLE.'), True)
  578. return
  579. await self._update_last_time(msg)
  580. await client.send_message(
  581. self.help_group,
  582. _T("\'NEW REPLY\n[ #{} ]:\nMESSAGE: {}").format(ticket_hash, TextParser(msg).parsed_msg),
  583. 'html',
  584. reply_markup=self.generate_ticket_keyboard(ticket_hash, msg.chat.id, sql_obj['section'] != self.SECTION[0])
  585. )
  586. await msg.reply(_T('The new reply is added successfully!'))
  587. async def handle_msg(self, client: Client, msg: Message) -> None:
  588. sql_obj = await self.query_status(msg.chat.id)
  589. if sql_obj is None or sql_obj['step'] not in (CustomServiceBot.SELECT_SECTION, CustomServiceBot.SEND_QUESTION):
  590. if await self.flood_check(client, msg):
  591. return
  592. await msg.reply(_T('Please use bot command to interact.'))
  593. return
  594. if sql_obj['step'] == CustomServiceBot.SELECT_SECTION:
  595. if msg.text in self.SECTION:
  596. await self.change_step(msg.chat.id, CustomServiceBot.SEND_QUESTION, msg.text)
  597. await msg.reply(_T(
  598. 'Please describe your problem briefly(up to 500 characters)\n'
  599. '(Please use external links to send pictures.):\n\n'
  600. 'ATTENTION: Receiving a confirmation message in return '
  601. 'indicates that the ticket is created successfully.\n\n'
  602. 'Use /cancel to cancel creating the ticket.'),
  603. True, reply_markup=ReplyKeyboardRemove())
  604. else:
  605. await msg.reply(_T('Please use the menu below to choose the correct department.'), True)
  606. elif sql_obj['step'] == CustomServiceBot.SEND_QUESTION:
  607. if len(msg.text) > 500:
  608. await msg.reply(_T('The number of characters you have entered is larger than 500. Please re-enter.'),
  609. True)
  610. return
  611. ticket_hash = self.hash_msg(msg)
  612. await self.pgsqldb.execute(*Ticket(msg, sql_obj['section'], 'open').sql)
  613. await self.change_step(msg.chat.id, CustomServiceBot.INIT_STATUS)
  614. await msg.reply(
  615. _T(
  616. 'The ticket is created successfully!\n[ #{ticket_id} ]\nDepartment: {section}\n'
  617. 'Message: \n{text}\n\nReply to this message to add a new reply to the ticket.').format(
  618. ticket_id=ticket_hash,
  619. text=TextParser(msg).parsed_msg,
  620. section=sql_obj['section']
  621. ),
  622. parse_mode='html'
  623. )
  624. msg_id = (await client.send_message(
  625. self.help_group,
  626. _T('NEW TICKET\n[ #{} ]\nClick {} to check the user profile\nDepartment: {}\nMessage: \n{}').format(
  627. ticket_hash,
  628. TextParser.parse_user_html(msg.chat.id, _T('Here')),
  629. sql_obj['section'],
  630. TextParser(msg).parsed_msg
  631. ),
  632. 'html',
  633. reply_markup=self.generate_ticket_keyboard(
  634. ticket_hash,
  635. msg.chat.id,
  636. other=sql_obj['section'] != self.SECTION[0]
  637. )
  638. )).message_id
  639. if sql_obj['section'] == self.SECTION[0]:
  640. await client.send_message(
  641. self.help_group,
  642. await self.generate_user_status(msg.chat.id),
  643. 'html',
  644. reply_to_message_id=msg_id
  645. )
  646. else:
  647. logger.error("throw! user_id: %d, sql_obj = %s", msg.chat.id, repr(sql_obj))
  648. @staticmethod
  649. async def generate_question_and_answer(user_session: asyncpg.Record) -> str:
  650. _text = 'Question: <code>{Q}</code>\n{question_type} Answer: <code>{A}</code>'.format(
  651. **await ProblemSet.get_instance().get(user_session['problem_id']),
  652. question_type='Except' if ProblemSet.get_instance().remove_punctuations.enable else 'Standard')
  653. if ProblemSet.get_instance().remove_punctuations.enable:
  654. _text += f'\nStandard Answer: <code>' \
  655. f'{await ProblemSet.get_instance().get_origin(user_session["problem_id"])}</code>'
  656. return _text
  657. async def __generate_answer_history(self, user_id: int) -> str:
  658. sql_obj = await self.pgsqldb.query(
  659. '''SELECT "body", "timestamp" FROM "answer_history" WHERE "user_id" = $1 ORDER BY "id" DESC LIMIT 3''',
  660. user_id)
  661. if sql_obj is None:
  662. return 'QUERY ERROR (user_id => %d)' % user_id
  663. if ProblemSet.get_instance().remove_punctuations.enable:
  664. return '\n\n'.join('<code>{}</code> <pre>{}</pre>\nOriginal answer: <pre>{}</pre>'.format(
  665. x['timestamp'], ProblemSet.get_instance().remove_punctuations.replace(x['body']), x['body']) for x in
  666. sql_obj)
  667. return '\n\n'.join(f'<code>{x["timestamp"]}</code> <pre>{x["body"]}</pre>' for x in sql_obj)
  668. async def _generate_answer_history(self, user_id: int, retries: int) -> str:
  669. sql_obj = await self.pgsqldb.query1('''SELECT COUNT(*) FROM "answer_history" WHERE "user_id" = $1''',
  670. user_id)
  671. if retries > 0 or sql_obj['count'] > 0:
  672. return '\n\nAnswer History:\n{}'.format(await self.__generate_answer_history(user_id))
  673. return ''
  674. async def generate_question_rate(self, user_session: Mapping[str, int]) -> str:
  675. problem_id = user_session['problem_id']
  676. total_count = (
  677. await self.pgsqldb.query1('''SELECT COUNT(*) FROM "exam_user_session" WHERE "problem_id" = $1''',
  678. problem_id))['count']
  679. correct_count = (await self.pgsqldb.query1(
  680. '''SELECT COUNT(*) FROM "exam_user_session" WHERE "problem_id" = $1 and "passed" = true''',
  681. problem_id))['count']
  682. rate = (correct_count / total_count) * 100
  683. return '\n\nProblem {} correct rate: {:.2f}%'.format(problem_id, rate)
  684. async def generate_user_status(self, user_id: int) -> str:
  685. user_status = await self.query_user_exam_status(user_id)
  686. return ('User {5} status:\nPassed exam: {0}\nBan status: {1}\nBypass: {2}\nUnlimited: {3}\n'
  687. 'Retries: {4}\n\n{6}{7}{8}').format(
  688. self.return_bool_emoji(user_status['passed']),
  689. self.return_bool_emoji(user_status['baned']),
  690. self.return_bool_emoji(user_status['bypass']),
  691. self.return_bool_emoji(user_status['unlimited']),
  692. user_status['retries'],
  693. TextParser.parse_user_html(user_id),
  694. await self.generate_question_and_answer(user_status),
  695. await self.generate_question_rate(user_status),
  696. await self._generate_answer_history(user_id, user_status['retries']) # type: ignore
  697. ) if user_status is not None else '<b>{}</b>'.format(_T('WARNING: THIS USER HAS NEVER USED THE BOT BEFORE.'))
  698. async def handle_other(self, _client: Client, msg: Message) -> None:
  699. if time.time() - await self._query_last_msg_send(msg) < 120:
  700. return
  701. await msg.reply(_T('Please use bot command to interact. TEXT ONLY.'))
  702. await self._update_last_msg_send(msg)
  703. async def handle_group(self, client: Client, msg: Message) -> None:
  704. if msg.reply_to_message.from_user.id != self.bot_id or (msg.text and msg.text.startswith('/')):
  705. return
  706. try:
  707. ticket_hash = self.get_hash_from_reply_msg(msg)
  708. except ValueError:
  709. return
  710. sql_obj = await self.pgsqldb.query1('''SELECT * FROM "tickets" WHERE "hash" = $1''', ticket_hash)
  711. if sql_obj is None:
  712. await msg.reply(_T('ERROR: TICKET NOT FOUND'))
  713. return
  714. if sql_obj['status'] == 'closed':
  715. await msg.reply(_T('This ticket is already closed.'))
  716. return
  717. try:
  718. msg_reply = await client.send_message(sql_obj['user_id'],
  719. _T(
  720. 'NEW UPDATE!\n[ #{} ]\nMessage: \n{}\n\n'
  721. 'Reply to this message to add a new reply to the ticket').format(
  722. ticket_hash, TextParser(msg).parsed_msg
  723. ), 'html')
  724. await msg.reply(_T('REPLY [ #{} ] SUCCESSFUL').format(ticket_hash),
  725. reply_markup=InlineKeyboardMarkup(inline_keyboard=[
  726. [
  727. InlineKeyboardButton(text=_T('recall'),
  728. callback_data=f'del '
  729. f'{msg_reply.chat.id} {msg_reply.message_id}')
  730. ]
  731. ]))
  732. r = await self._query_last_time(msg)
  733. if time.time() - r < 120:
  734. await self._redis.delete(f'CSLAST_{sql_obj["user_id"]}')
  735. except pyrogram.errors.UserIsBlocked:
  736. await msg.reply(_T('Replay [ #{} ] fail,user blocked this bot.').format(ticket_hash))
  737. except pyrogram.errors.RPCError:
  738. await msg.reply(_T('Replay [ #{} ] fail, {}\n'
  739. 'View console to get more information').format(ticket_hash,
  740. traceback.format_exc().splitlines()[
  741. -1]))
  742. raise
  743. @staticmethod
  744. def generate_confirm_keyboard(first: str, last: Union[str, Sequence[str]]) -> InlineKeyboardMarkup:
  745. if isinstance(last, list) or isinstance(last, tuple):
  746. lastg = last
  747. else:
  748. lastg = (str(last),)
  749. return InlineKeyboardMarkup(inline_keyboard=[
  750. [
  751. InlineKeyboardButton(text='Yes', callback_data=' '.join((first, 'confirm', *lastg))),
  752. InlineKeyboardButton(text='No', callback_data='cancel')
  753. ]
  754. ])
  755. async def generate_superuser_text(self, user_id: Union[str, int]) -> str:
  756. return '\n\n'.join(
  757. (_T("Please choose the section below"), await self.generate_user_status(user_id), # type: ignore
  758. ' '.join((_T('Last refresh:'), str(datetime.now().replace(microsecond=0))))))
  759. async def generate_superuser_detail(self, user_id: Union[str, int]) -> Dict[str, _anyT]:
  760. return {
  761. 'text': await self.generate_superuser_text(user_id),
  762. 'reply_markup': InlineKeyboardMarkup(
  763. inline_keyboard=[
  764. [
  765. InlineKeyboardButton(text=_T('BYPASS'), callback_data=f'bypass {user_id}'),
  766. InlineKeyboardButton(text=_T('UNLIMITED RETRIES'), callback_data=f'unlimited {user_id}'),
  767. InlineKeyboardButton(text=_T('REFRESH'), callback_data=f'refresh {user_id}')
  768. ],
  769. [
  770. InlineKeyboardButton(text=_T('PASS'), callback_data=f'setpass {user_id}'),
  771. InlineKeyboardButton(text=_T('RESET TIMES'), callback_data=f'reset {user_id}')
  772. ],
  773. [
  774. InlineKeyboardButton(text=_T('RESET USER STATUS'), callback_data=f'renew {user_id}')
  775. ],
  776. [
  777. InlineKeyboardButton(text='INSERT USER PROFILE', callback_data=f'insert {user_id}')
  778. ],
  779. [
  780. InlineKeyboardButton(text=_T('Cancel'), callback_data='cancel')
  781. ]
  782. ]
  783. )
  784. }
  785. async def query_user_status(self, _client: Client, msg: Message) -> None:
  786. if len(msg.command) < 2:
  787. await msg.reply('Arguments should contain user_id')
  788. return
  789. await self.get_user_status(int(msg.command[1]), msg.message_id)
  790. async def get_user_status(self, user_id: int, reply_to_message_id: int) -> None:
  791. await self.bot.send_message(
  792. self.help_group,
  793. parse_mode='html',
  794. reply_to_message_id=reply_to_message_id,
  795. **await self.generate_superuser_detail(user_id)
  796. )
  797. async def call_superuser_function(self, _client: Client, msg: Message) -> None:
  798. sql_obj = await self.pgsqldb.query1('''SELECT "user_id", "section" FROM "tickets" WHERE "hash" = $1''',
  799. self.get_hash_from_reply_msg(msg))
  800. if sql_obj['section'] != self.SECTION[0]:
  801. await msg.reply(_T("This ticket doesn't support admin menus for now."), True)
  802. return
  803. user_id = sql_obj['user_id']
  804. await self.get_user_status(user_id, msg.reply_to_message.message_id)
  805. async def confirm_dialog(self, msg: CallbackQuery, additional_msg: str, callback_prefix: str,
  806. id_: Union[str]) -> None:
  807. asyncio.run_coroutine_threadsafe(msg.answer(), asyncio.get_event_loop())
  808. if len(id_) < 32:
  809. await self.bot.send_message(
  810. self.help_group,
  811. _T('Do you really want to {} {}?').format(additional_msg, TextParser.parse_user_markdown(id_)),
  812. 'markdown',
  813. reply_markup=self.generate_confirm_keyboard(callback_prefix, id_)
  814. )
  815. else:
  816. await self.bot.send_message(
  817. self.help_group,
  818. _T('Do you really want to {} #{}?').format(additional_msg, id_),
  819. reply_markup=self.generate_confirm_keyboard(callback_prefix, id_)
  820. )
  821. async def confirm(self, client: Client, msg: CallbackQuery) -> None:
  822. if time.time() - msg.message.date > 15:
  823. raise TimeoutError()
  824. if msg.data.startswith('close'):
  825. ticket_id = msg.data.split()[-1]
  826. q = await self.pgsqldb.query1('''SELECT "user_id", "status" FROM "tickets" WHERE "hash" = $1''', ticket_id)
  827. if q is None:
  828. return await msg.answer(_T('TICKET NOT FOUND'), True)
  829. if q['status'] == 'closed':
  830. return await msg.answer(_T('This ticket is already closed.'))
  831. await self.pgsqldb.execute('''UPDATE "tickets" SET "status" = 'closed' WHERE "hash" = $1''', ticket_id)
  832. await msg.answer(_T('This ticket is already closed.'))
  833. await client.send_message(
  834. self.help_group,
  835. _T('UPDATE\n[ #{} ]\nThis ticket is closed by {}.').format(
  836. ticket_id,
  837. utils.TextParser.parse_user_markdown(
  838. msg.from_user.id,
  839. utils.TextParser.UserName(msg.from_user).full_name
  840. )
  841. ),
  842. 'markdown',
  843. reply_markup=self.generate_ticket_keyboard(ticket_id, q['user_id'], True)
  844. )
  845. await client.send_message(q['user_id'], _T('Your ticket [ #{} ] is closed').format(ticket_id))
  846. elif msg.data.startswith('block'):
  847. await self.pgsqldb.execute('''UPDATE "tickets_user" SET "banned" = true WHERE "user_id" = $1''',
  848. int(msg.data.split()[-1]))
  849. await msg.answer(_T('DONE!'))
  850. await self.bot.send_message(
  851. self.help_group,
  852. _T('blocked {}').format(TextParser.parse_user_markdown(msg.data.split()[-1], msg.data.split()[-1])),
  853. parse_mode='markdown',
  854. reply_markup=InlineKeyboardMarkup(inline_keyboard=[
  855. [InlineKeyboardButton(text=_T('UNBAN'), callback_data='unban {}'.format(msg.data.split()[-1]))]
  856. ])
  857. )
  858. elif msg.data.startswith('send'):
  859. try:
  860. await self.send_link_callback(int(msg.data.split()[-1]), True)
  861. await msg.answer(_T('The invitation link is sent successfully.'))
  862. except:
  863. await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True)
  864. await msg.answer(_T('Failed to send the invitation link. Please check the console.\n{}').format(
  865. traceback.format_exc().splitlines()[-1]), True)
  866. elif msg.data.startswith('reset'):
  867. await self.pgsqldb.execute('''UPDATE "exam_user_session" SET "retries" = 0 WHERE "user_id" = $1''',
  868. int(msg.data.split()[-1]))
  869. await msg.answer('Retry times has been reset')
  870. elif msg.data.startswith('del'):
  871. try:
  872. await client.delete_messages(int(msg.data.split()[-2]), int(msg.data.split()[-1]))
  873. await msg.answer('message has been deleted')
  874. except:
  875. await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True)
  876. await msg.answer(_T('Failed to delete the message. Please check the console.\n{}').format(
  877. traceback.format_exc().splitlines()[-1]), True)
  878. elif msg.data.startswith('renew'):
  879. await self.pgsqldb.execute('''DELETE FROM "exam_user_session" WHERE "user_id" = $1''',
  880. int(msg.data.split()[-1]))
  881. await msg.answer(_T('DONE!'))
  882. elif msg.data.startswith('bypass'):
  883. await self.pgsqldb.execute('''UPDATE "exam_user_session" SET "bypass" = true WHERE "user_id" = $1''',
  884. int(msg.data.split()[-1]))
  885. await msg.answer(_T('DONE!'))
  886. elif msg.data.startswith('setpass'):
  887. await self.pgsqldb.execute('''UPDATE "exam_user_session" SET "passed" = true WHERE "user_id" = $1''',
  888. int(msg.data.split()[-1]))
  889. await msg.answer(_T('DONE!'))
  890. elif msg.data.startswith('unlimited'):
  891. await self.pgsqldb.execute('''UPDATE "exam_user_session" SET "unlimited" = true WHERE "user_id" = $1''',
  892. int(msg.data.split()[-1]))
  893. await msg.answer(_T('DONE!'))
  894. elif msg.data.startswith('insert'):
  895. await self.pgsqldb.execute('''INSERT INTO "exam_user_session" ("user_id", "problem_id") VALUES ($1, 2)''',
  896. int(msg.data.split()[-1]))
  897. await msg.answer(_T('DONE!'))
  898. await client.delete_messages(msg.message.chat.id, msg.message.message_id)
  899. async def send_confirm(self, _client: Client, msg: CallbackQuery) -> None:
  900. def make_msg_handle(additional_msg: str, callback_prefix: str):
  901. async def wrapper():
  902. await self.confirm_dialog(msg, additional_msg, callback_prefix, msg.data.split()[-1])
  903. return wrapper
  904. if msg.data.startswith('del'):
  905. await msg.answer('Please press again to make sure. If you really want to delete this reply', True)
  906. await self.bot.send_message(
  907. self.help_group,
  908. 'Do you want to delete reply message to {}?'.format(
  909. TextParser.parse_user_markdown(msg.data.split()[-2])),
  910. 'markdown',
  911. reply_markup=self.generate_confirm_keyboard('del', msg.data[4:])
  912. )
  913. command_mapping = {
  914. 'close': make_msg_handle(_T('close this ticket'), 'close'),
  915. 'block': make_msg_handle(_T('block this user'), 'block'),
  916. 'send': make_msg_handle(_T('send the link to'), 'send'),
  917. 'reset': make_msg_handle(_T('reset retry times for'), 'reset'),
  918. 'bypass': make_msg_handle(_T('set bypass for'), 'bypass'),
  919. 'renew': make_msg_handle(_T('reset user status'), 'renew'),
  920. 'setpass': make_msg_handle(_T('set pass'), 'setpass'),
  921. 'unlimited': make_msg_handle(_T('set unlimited retries for'), 'unlimited'),
  922. 'insert': make_msg_handle('insert new profile', 'insert')
  923. }
  924. for name, func in command_mapping.items():
  925. if msg.data.startswith(name):
  926. await func()
  927. break
  928. async def answer(self, client: Client, msg: CallbackQuery) -> None:
  929. if msg.data.startswith('cancel'):
  930. await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id)
  931. await msg.answer('Canceled')
  932. elif msg.data.startswith('unban'):
  933. await self.pgsqldb.execute('''UPDATE "tickets_user" SET "banned" = false WHERE "user_id" = $1''',
  934. int(msg.data.split()[-1]))
  935. await msg.answer('UNBANED')
  936. await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id)
  937. elif msg.data.startswith('refresh'):
  938. try:
  939. await client.edit_message_text(
  940. msg.message.chat.id,
  941. msg.message.message_id,
  942. await self.generate_superuser_text(int(msg.data.split()[-1])),
  943. 'html',
  944. reply_markup=msg.message.reply_markup
  945. )
  946. except pyrogram.errors.exceptions.bad_request_400.MessageNotModified:
  947. pass
  948. await msg.answer()
  949. elif 'confirm' in msg.data:
  950. try:
  951. await self.confirm(client, msg)
  952. except TimeoutError:
  953. await asyncio.gather(msg.answer('Confirmation time out'),
  954. client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id))
  955. elif any(msg.data.startswith(x) for x in
  956. ('close', 'block', 'send', 'bypass', 'reset', 'unlimited', 'del', 'renew', 'setpass', 'insert')):
  957. await self.send_confirm(client, msg)
  958. else:
  959. try:
  960. raise ValueError(msg.data)
  961. except:
  962. await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True)
  963. async def _query_last_time(self, msg: Message) -> int:
  964. return await self._query_redis_time(f'CSLAST_{msg.chat.id}')
  965. async def _query_last_msg_send(self, msg: Message) -> int:
  966. return await self._query_redis_time(f'CSLASTMSG_{msg.chat.id}')
  967. async def _query_redis_time(self, key: str) -> int:
  968. r = await self._redis.get(key)
  969. return 0 if r is None else int(r.decode())
  970. async def _update_redis_time(self, key: str) -> None:
  971. await self._redis.set(key, str(int(time.time())), expire=180)
  972. async def _update_last_time(self, msg: Message) -> None:
  973. await self._update_redis_time(f'CSLAST_{msg.chat.id}')
  974. async def _update_last_msg_send(self, msg: Message) -> None:
  975. await self._update_redis_time(f'CSLASTMSG_{msg.chat.id}')
  976. async def flood_check(self, _client: Client, msg: Message) -> bool:
  977. r = await self._query_last_time(msg)
  978. if time.time() - r < 120:
  979. if msg.text:
  980. logger.warning('Caught flood %s: %s', msg.chat.id, msg.text)
  981. await self._update_last_msg_send(msg)
  982. sq = await self.pgsqldb.query1('''SELECT "banned" FROM "tickets_user" WHERE "user_id" = $1''', msg.chat.id)
  983. if sq and sq['baned']:
  984. return await msg.reply(
  985. _T('Due to privacy settings, you are temporarily unable to operate.')) is not None
  986. await msg.reply(_T('You are driving too fast. Please try again later.'))
  987. return True
  988. return False