From 9bc7c217580d13dc23b5233177043f47a5b14134 Mon Sep 17 00:00:00 2001 From: beyondgfw Date: Thu, 21 May 2020 19:05:02 +0800 Subject: [PATCH] Make code asynchronized --- README.md | 29 +- bot_repeater.sql | 62 ++- config.ini.default | 15 +- customservice.py | 1159 ++++++++++++++++++++++++++++---------------- main.py | 744 ---------------------------- repeater.py | 984 +++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + tg_tools.py | 253 ---------- utils.py | 513 ++++++++++++++++++++ 9 files changed, 2320 insertions(+), 1444 deletions(-) delete mode 100644 main.py create mode 100644 repeater.py create mode 100644 requirements.txt delete mode 100644 tg_tools.py create mode 100644 utils.py diff --git a/README.md b/README.md index a3b1125..72239a8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The bot forwards your messages to another group. * Support media (except voice messages). * Reply function available. * Anonymity: Protect the identity of the user. -* Authorised users in this group can manage the target group function, such as `/ban`, `/del`, `/kick`, etc. +* Authorised users in this group can manage the target group function, such as `/ban`, `/del`, `/grant`, `/kick`, etc. * Use MySQL engine to mark the time, message ID and user ID. * When the bot is mentioned in the target group, the user ID specified in the config file will also be mentioned in this group. * Once promoted to admin, the bot can add new admins. @@ -17,12 +17,14 @@ The bot forwards your messages to another group. ## Operating Environment -Python 3.4 and above is required +Python 3.7 and above is required The following libraries are required: -- pyrogram (==0.11.0) -- pymysql +- pyrogram.async (==0.17.0) +- aiomysql +- aioredis +- aiofile ## Configure @@ -44,35 +46,38 @@ The following libraries are required: ## Instruction -* Use `python3 main.py` or other command lines to run the program. +* Use `python3 repeater.py` or other command lines to run the program. * Log in using the account you set in the `owner` field. * If you want to authorize a certain user, you should invite the user to this group first, then use `/auth`. -* To turn off the repeater, send `/bot off` (`/boff` also available) to the target group, vice versa. +* To turn off the repeater, send `/off` to the target group, vice versa. ## Available Commands Command | Description | Reply to the message ---|---|--- -`/bon` or `/boff` | Switch on/off the bot | False -`/status` | check the user's authorization status| False +`/on` or `/off` | Switch on/off the bot | False +`/status` | check the user's authorization status | False `/auth` | authorize to another user | True -`/ban` | put restrictions on the target user, a certain length of time can be specified (e.g. `/ban` 1m means to restrict the user for one minute) | True +`/ban` | put restrictions on the target user, a certain length of time can be specified (e.g. `/ban 1m` means to restrict the user for one minute) | True `/kick` | remove the user from the target group | True `/fw` | forward a message to the target group using the bot account | True `/get` | forward the original message to this group | True `/del` | delete the selected message in the target group | True -`/p` | gain admin access immediately for yourself in the target group | False +`/sudo` or `/su` | gain admin access immediately for yourself in the target group | False `/promote` | authorise other users to become admins | True +`/grant` | grant specify privileges to specify user in group | False +`/pin` | pin a message in group | True +`/warn` | send a warn to user with reason | True ## Special Thanks -Special thanks to Group:J, who helped me with the translation. +Special thanks to ``, who helped me with the translation. ## License [![](https://www.gnu.org/graphics/agplv3-155x51.png)](https://www.gnu.org/licenses/agpl-3.0.txt) -Copyright (C) 2018-2019 github.com/googlehosts Group:Z +Copyright (C) 2018-2020 github.com/googlehosts Group:Z This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or any later version. diff --git a/bot_repeater.sql b/bot_repeater.sql index 23c882c..adc452e 100644 --- a/bot_repeater.sql +++ b/bot_repeater.sql @@ -4,35 +4,69 @@ /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; +-- Dumping structure for table answer_history +CREATE TABLE IF NOT EXISTS `answer_history` ( + `_id` int(10) unsigned NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `body` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT current_timestamp(), + PRIMARY KEY (`_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Dumping structure for table auth_user +CREATE TABLE IF NOT EXISTS `auth_user` ( + `id` bigint(20) NOT NULL, + `authorized` enum('Y','N') NOT NULL DEFAULT 'N', + `muted` enum('Y','N') NOT NULL DEFAULT 'N', + `whitelist` enum('Y','N') NOT NULL DEFAULT 'N', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8; + +-- Dumping structure for table banlist +CREATE TABLE IF NOT EXISTS `banlist` ( + `id` bigint(20) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- Dumping structure for table exam_user_session CREATE TABLE IF NOT EXISTS `exam_user_session` ( `user_id` int(11) NOT NULL, `problem_id` int(11) DEFAULT NULL, - `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `baned` tinyint(4) NOT NULL DEFAULT '0', - `bypass` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'bypass exam', - `passed` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'passed the exam', - `unlimited` tinyint(4) NOT NULL DEFAULT '0', - `retries` int(11) NOT NULL DEFAULT '0', + `timestamp` timestamp NOT NULL DEFAULT current_timestamp(), + `baned` tinyint(4) NOT NULL DEFAULT 0, + `bypass` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'bypass exam', + `passed` tinyint(4) NOT NULL DEFAULT 0 COMMENT 'passed the exam', + `unlimited` tinyint(4) NOT NULL DEFAULT 0, + `retries` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- Dumping structure for table msg_id CREATE TABLE IF NOT EXISTS `msg_id` ( `msg_id` int(11) NOT NULL, - `target_id` int(11) NOT NULL DEFAULT '0', - `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, - `user_id` bigint(20) NOT NULL DEFAULT '0', + `target_id` int(11) NOT NULL DEFAULT 0, + `timestamp` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), + `user_id` bigint(20) DEFAULT NULL, PRIMARY KEY (`msg_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; +-- Dumping structure for table reasons +CREATE TABLE IF NOT EXISTS `reasons` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `user_id` int(10) unsigned NOT NULL, + `timestamp` timestamp NOT NULL DEFAULT current_timestamp(), + `text` text COLLATE utf8mb4_unicode_ci NOT NULL, + `msg_id` int(10) unsigned DEFAULT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + -- Dumping structure for table tickets CREATE TABLE IF NOT EXISTS `tickets` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `user_id` int(11) NOT NULL DEFAULT '0', - `timestamp` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, + `user_id` int(11) NOT NULL DEFAULT 0, + `timestamp` timestamp NOT NULL DEFAULT current_timestamp(), `hash` varchar(32) DEFAULT '', - `origin_msg` text, + `origin_msg` text DEFAULT NULL, `section` varchar(20) DEFAULT '', `status` varchar(10) DEFAULT '', PRIMARY KEY (`id`), @@ -44,9 +78,9 @@ CREATE TABLE IF NOT EXISTS `tickets_user` ( `user_id` bigint(20) NOT NULL, `create_time` timestamp NULL DEFAULT NULL, `last_time` timestamp NULL DEFAULT NULL, - `baned` tinyint(4) NOT NULL DEFAULT '0', + `baned` tinyint(4) NOT NULL DEFAULT 0, `last_msg_sent` timestamp NULL DEFAULT NULL, - `step` tinyint(4) NOT NULL DEFAULT '0', + `step` tinyint(4) NOT NULL DEFAULT 0, `section` varchar(20) DEFAULT '', PRIMARY KEY (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; diff --git a/config.ini.default b/config.ini.default index d4704cc..818df92 100644 --- a/config.ini.default +++ b/config.ini.default @@ -3,18 +3,23 @@ api_id = api_hash = owner = api_key = + +[join_group_verify] +enable = false + +[custom_service] +enable = false custom_api_key = -emerg_contact = +help_group = [fuduji] target_group = -auth_user = -ignore_user = bot_id = fudu_group = -help_group = replace_to_id = -whitelist = + +[i18n] +language=en_US [database] host = diff --git a/customservice.py b/customservice.py index 9a0cf9c..795bae4 100644 --- a/customservice.py +++ b/customservice.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # customservice.py -# Copyright (C) 2019 github.com/googlehosts Group:Z +# Copyright (C) 2019-2020 github.com/googlehosts Group:Z # # This module is part of googlehosts/telegram-repeater and is released under # the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt @@ -17,240 +17,444 @@ # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -from pyrogram import Client, MessageHandler, Message, CallbackQuery, CallbackQueryHandler, \ - Filters, InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, \ - ReplyKeyboardRemove, api -from configparser import ConfigParser -import tg_tools +import base64 +import gettext import hashlib +import logging +import random +import re import time -from threading import Lock, Thread -import base64 -import queue import traceback -from datetime import datetime, timedelta -import re -import random +from configparser import ConfigParser +from datetime import datetime +from typing import (Awaitable, Callable, Dict, List, Mapping, Optional, + Sequence, Tuple, Union) + +import pyrogram.errors +import redis +from pyrogram import (CallbackQuery, CallbackQueryHandler, Client, Filters, + InlineKeyboardButton, InlineKeyboardMarkup, + KeyboardButton, Message, MessageHandler, + ReplyKeyboardMarkup, ReplyKeyboardRemove) + +import utils +from utils import _anyT, _kT, _rT + +logger = logging.getLogger('customservice') -class build_html_parse(tg_tools.build_html_parse): +translation = gettext.translation('customservice', 'translations/', + languages=[utils.get_language()], fallback=True) + +_T = translation.gettext + + +class TextParser(utils.TextParser): def __init__(self, msg: Message): - self._msg = self.gen_msg(msg) + super().__init__() + self._msg = self.BuildMessage(msg) self.parsed_msg = self.parse_main() + def __str__(self): return self.parsed_msg -class ticket(object): + +class Ticket: def __init__(self, msg: Message, section: str, status: str): - self._origin_msg = build_html_parse(msg).parsed_msg - self.hash_value = custom_service_bot_class.hash_msg(msg) + self._origin_msg = TextParser(msg).parsed_msg + self.hash_value = CustomServiceBot.hash_msg(msg) self.section = section self.status = status - self.sql = "INSERT INTO `tickets` (`user_id`, `hash`, `timestamp`, `origin_msg`, `section`, `status`) VALUES ({0}, '{1}', CURRENT_TIMESTAMP(), '{2}', '{3}', '{4}')".format( - msg.chat.id, self.hash_value, base64.b64encode(self._origin_msg.encode()).decode(), self.section, self.status - ) - def __str__(self): + self.sql = ( + "INSERT INTO `tickets` (`user_id`, `hash`, `timestamp`, `origin_msg`, `section`, `status`) VALUES (%s, %s, CURRENT_TIMESTAMP(), %s, %s, %s)", + ( + msg.chat.id, self.hash_value, base64.b64encode(self._origin_msg.encode()).decode(), self.section, + self.status + )) + + def __str__(self) -> Tuple[str, Tuple[_anyT, ...]]: return self.sql -class join_group_verify_class(object): - def __init__(self, conn: tg_tools.mysqldb, botapp: Client, target_group: int, load_problem_set: callable): - self.problem_set = load_problem_set() - self.conn = conn - self.botapp = botapp - self.target_group = target_group - self.revoke_tracker_thread = tg_tools.invite_link_tracker( - self.botapp, - self.problem_set, - self.target_group - ) - def init(self): - self.botapp.add_handler(MessageHandler(self.handle_bot_private, Filters.private & Filters.text)) +class RemovePunctuations: + def __init__(self, enable: bool, items: List[str]): + self.enable = enable + self.items = items + + def replace(self, text: str) -> str: + if not self.enable: + return text + return ''.join(x for x in text if x not in self.items) + + +class ProblemSet: + _self = None + + def __init__(self, redis_conn: redis.Redis, problem_set: Mapping[str, _anyT], + remove_punctuations: RemovePunctuations): + self._redis: redis.Redis = redis_conn + self._prefix: str = utils.get_random_string() + self.problem_length: int = len(problem_set['problems']['problem_set']) + self.sample_problem: Dict[str, str] = problem_set['problems'].get('sample_problem') + self._has_sample: bool = bool(self.sample_problem) + self.remove_punctuations: RemovePunctuations = remove_punctuations + + async def init(self, problem_set: Mapping[str, _anyT]): + if self.sample_problem: + await self._redis.mset({f'{self._prefix}_{key}_sample': item for key, item in self.sample_problem.items()}) + for x in range(self.problem_length): + problems = problem_set['problems']['problem_set'] + if problems[x].get('use_regular_expression'): + await self._redis.set(f'{self._prefix}_re_{x}', 1) + await self._redis.set(f'{self._prefix}_Q_{x}', problems[x]['Q']) + await self._redis.set(f'{self._prefix}_A_{x}', self.remove_punctuations.replace(problems[x]['A'])) + await self._redis.set(f'{self._prefix}_OA_{x}', problems[x]['A']) + + @classmethod + async def create(cls, redis_conn: redis.Redis, problem_set: Dict[str, _anyT], + remove_punctuations: RemovePunctuations) -> 'ProblemSet': + self = ProblemSet(redis_conn, problem_set, remove_punctuations) + await self.init(problem_set) + return self + + async def destroy(self) -> None: + for x in range(self.problem_length): + await self._redis.delete(f'{self._prefix}_re_{x}') + await self._redis.delete(f'{self._prefix}_Q_{x}') + await self._redis.delete(f'{self._prefix}_A_{x}') + await self._redis.delete(f'{self._prefix}_OA_{x}') + if self._has_sample: + await self._redis.delete(f'{self._prefix}_Q_sample') + await self._redis.delete(f'{self._prefix}_A_sample') + + def get_random_number(self) -> int: + return random.randint(0, self.problem_length - 1) + + async def get(self, key: int) -> Dict[str, str]: + return {'use_regular_expression': await self._redis.get(f'{self._prefix}_re_{key}'), + 'Q': (await self._redis.get(f'{self._prefix}_Q_{key}')).decode(), + 'A': (await self._redis.get(f'{self._prefix}_A_{key}')).decode()} + + async def get_origin(self, key: int) -> str: + return (await self._redis.get(f'{self._prefix}_OA_{key}')).decode() + + @property + def length(self) -> int: + return self.problem_length + + @property + def has_sample(self) -> bool: + return self._has_sample + + async def get_sample(self) -> Optional[Mapping[str, str]]: + if not self._has_sample: + return None + return {'Q': (await self._redis.get(f'{self._prefix}_Q_sample')).decode(), + 'A': (await self._redis.get(f'{self._prefix}_A_sample')).decode()} - def get_revoke_tracker_thread(self): - return self.revoke_tracker_thread + @staticmethod + def get_instance() -> 'ProblemSet': + if ProblemSet._self is None: + raise RuntimeError() + return ProblemSet._self + + @staticmethod + async def init_instance(redis_conn: redis.Redis, problem_set: dict, + remove_punctuations: RemovePunctuations) -> 'ProblemSet': + ProblemSet._self = await ProblemSet.create(redis_conn, problem_set, remove_punctuations) + return ProblemSet._self + + +class JoinGroupVerify: + + def __init__(self, conn: utils.MySQLdb, botapp: Client, target_group: int, working_group: int): + self.conn: utils.MySQLdb = conn + self.botapp: Client = botapp + self.target_group: int = target_group + self.working_group: int = working_group + self._revoke_tracker_coro: Optional[utils.InviteLinkTracker] = None + self._keyboard: Dict[str, InlineKeyboardMarkup] = {} + self._welcome_msg: Optional[str] = None + self.remove_punctuations: Optional[RemovePunctuations] = None + self.problems: Optional[ProblemSet] = None + self.max_retry: Optional[int] = None + self.max_retry_error: Optional[str] = None + self.max_retry_error_detail: Optional[str] = None + self.try_again: Optional[str] = None + self._send_link_confirm: Optional[bool] = None + self._confirm_message: Optional[str] = None + self._confirm_button_text: Optional[str] = None + + def init(self) -> None: + self.botapp.add_handler(MessageHandler(self.handle_bot_private, Filters.private & Filters.text)) - def generate_ticket_keyboard(self): - if self.problem_set['ticket_bot']['enable']: - return { + def init_other_object(self, problem_set: Dict[str, _anyT]): + self._revoke_tracker_coro: utils.InviteLinkTracker = utils.InviteLinkTracker( + self.botapp, + problem_set, + self.target_group + ) + self._welcome_msg: str = problem_set['messages']['welcome_msg'] + self.max_retry: int = problem_set['configs']['max_retry'] + self.max_retry_error: str = problem_set['messages']['max_retry_error'] + self.max_retry_error_detail: str = problem_set['messages']['max_retry_error_detail'] + self.try_again: str = problem_set['messages']['try_again'] + self._send_link_confirm: bool = problem_set.get('confirm_msg') and problem_set['confirm_msg'].get('enable') + if self._send_link_confirm: + self._confirm_message: str = problem_set['confirm_msg']['text'] + self._confirm_button_text: str = problem_set['confirm_msg']['button_text'] + if problem_set['ticket_bot']['enable']: + self._keyboard = { 'reply_markup': InlineKeyboardMarkup( - inline_keyboard = [ - [InlineKeyboardButton(text = 'I need help.', url = self.problem_set['ticket_bot']['link'])] + inline_keyboard=[ + [InlineKeyboardButton(text=_T('I need help.'), url=problem_set['ticket_bot']['link'])] ] ) } - else: - return {} - - def query_user_passed(self, user_id: int): - sqlObj = self.conn.query1("SELECT `passed`, `bypass` FROM `exam_user_session` WHERE `user_id` = {}".format(user_id)) + self._revoke_tracker_coro.start() + + @classmethod + async def create(cls, conn: utils.MySQLdb, botapp: Client, target_group: int, working_group: int, + load_problem_set: Awaitable[Callable[[], Mapping[str, _rT]]], redis_conn: redis.Redis): + self = JoinGroupVerify(conn, botapp, target_group, working_group) + problem_set = await load_problem_set() + self.remove_punctuations = RemovePunctuations( + **problem_set['configs'].get('ignore_punctuations', {'enable': False, 'items': []})) + self.problems = await ProblemSet.init_instance(redis_conn, problem_set, self.remove_punctuations) + self.init_other_object(problem_set) + return self + + @property + def problem_list(self) -> ProblemSet: + if self.problems is None: + raise RuntimeError() + return self.problems + + @property + def revoke_tracker_coro(self) -> utils.InviteLinkTracker: + return self._revoke_tracker_coro + + async def query_user_passed(self, user_id: int) -> bool: + sqlObj = await self.conn.query1("SELECT `passed`, `bypass` FROM `exam_user_session` WHERE `user_id` = %s", + user_id) return sqlObj is not None and (sqlObj['passed'] or sqlObj['bypass']) - def handle_bot_private(self, client: Client, msg: Message): + async def query_user_in_origin_group(self, user_id: int) -> bool: + userOriginObj = await self.conn.query1("SELECT * FROM `ingroup` WHERE `user_id` = %s", user_id) + return userOriginObj is not None + + async def handle_bot_private(self, client: Client, msg: Message) -> None: if msg.text.startswith('/') and msg.text != '/start newbie': return - userObj = self.conn.query1("SELECT `problem_id`, `baned`, `bypass`, `retries`, `passed`, `unlimited` FROM `exam_user_session` WHERE `user_id` = {}".format(msg.chat.id)) + if await self.query_user_in_origin_group(msg.chat.id): + await self._revoke_tracker_coro.send_link(msg.chat.id, True) + return + userObj = await self.conn.query1( + "SELECT `problem_id`, `baned`, `bypass`, `retries`, `passed`, `unlimited` FROM `exam_user_session` WHERE `user_id` = %s", + msg.chat.id) if msg.text == '/start newbie': try: try: - user = self.botapp.get_chat_member(self.target_group, msg.chat.id) - return msg.reply('You are already in the group.') - except api.errors.exceptions.bad_request_400.UserNotParticipant: + user = await self.botapp.get_chat_member(self.target_group, msg.chat.id) + if user.status == 'left': + raise ValueError('left') + await msg.reply(_T('You are already in the group.')) + return + except pyrogram.errors.exceptions.bad_request_400.UserNotParticipant: pass except: traceback.print_exc() if userObj is not None: if userObj['bypass']: - self.revoke_tracker_thread.send_link(msg.chat.id, True) + await self._revoke_tracker_coro.send_link(msg.chat.id, True) elif userObj['passed']: - msg.reply('You have already answered the question.') + await msg.reply(_T('You have already answered the question.')) + elif userObj['baned']: + await msg.reply(_T('Due to privacy settings, you are temporarily unable to join this group.')) else: - msg.reply('An existing session is currently active.', True) + await msg.reply(_T('An existing session is currently active.'), True) else: - randomid = random.randint(0, len(self.problem_set['problem_set']) -1) - self.conn.execute("INSERT INTO `exam_user_session` (`user_id`, `problem_id`, `timestamp`) VALUES ({0}, {1}, CURRENT_TIMESTAMP())".format(msg.chat.id, randomid)) - msg.reply( - self.problem_set['welcome_msg'], - parse_mode = 'html', - disable_web_page_preview = True, - **self.generate_ticket_keyboard() + randomid = self.problems.get_random_number() + await self.conn.execute( + "INSERT INTO `exam_user_session` (`user_id`, `problem_id`, `timestamp`) VALUES (%s, %s, CURRENT_TIMESTAMP())", + (msg.chat.id, randomid)) + await msg.reply( + self._welcome_msg, + parse_mode='html', + disable_web_page_preview=True, + **self._keyboard ) - if self.problem_set.get('sample_problem') is not None: - msg.reply( - "For example:\nQ: {Q}\nA: {A}".format( - **self.problem_set['sample_problem'] + if self.problems.has_sample: + await msg.reply( + _T('For example:\n {Q}\nA: {A}').format( + **await self.problems.get_sample() ), - parse_mode = 'html', - disable_web_page_preview = True + parse_mode='html', + disable_web_page_preview=True ) - msg.reply( - self.problem_set['problem_set'][randomid]['Q'], - parse_mode = 'html', - disable_web_page_preview = True + await msg.reply( + (await self.problems.get(randomid))['Q'], + parse_mode='html', + disable_web_page_preview=True ) - except api.errors.exceptions.bad_request_400.UserIsBlocked: - print('Caught blocked user {}'.format(msg.chat.id)) - client.send_message( - self.target_group, - 'The bot is blocked by user {}'.format(build_html_parse.parse_user(msg.chat.id)), + except pyrogram.errors.exceptions.bad_request_400.UserIsBlocked: + logger.warning('Caught blocked user %s', msg.chat.id) + await client.send_message( + self.working_group, + _T('The bot is blocked by user {}').format(TextParser.parse_user(msg.chat.id)), 'markdown' ) except: traceback.print_exc() else: if userObj is not None: - if (userObj['unlimited'] or userObj['retries'] <= self.problem_set['max_retry']) and msg.text == self.problem_set['problem_set'][userObj['problem_id']]['A']: - self.conn.execute("UPDATE `exam_user_session` SET `passed` = 1 WHERE `user_id` = {}".format(msg.chat.id)) - self.send_link(msg) + if (userObj['unlimited'] or userObj['retries'] <= self.max_retry) and \ + self.valid_answer(msg, await self.problems.get(userObj['problem_id'])): + await self.conn.execute("UPDATE `exam_user_session` SET `passed` = 1 WHERE `user_id` = %s", + msg.chat.id) + await self.send_link(msg) elif userObj['bypass']: - self.send_link(msg) + await self.conn.execute("UPDATE `exam_user_session` SET `passed` = 1 WHERE `user_id` = %s", + msg.chat.id) + await self.send_link(msg) else: - userObj['retries'] += 1 - if userObj['retries'] > self.problem_set['max_retry']: - msg.reply(self.problem_set['max_retry_error'], parse_mode = 'html', disable_web_page_preview = True) + retries = userObj['retries'] + 1 + retries += 1 + if retries > self.max_retry: + if retries == self.max_retry + 1: + await msg.reply( + '\n\n'.join((self.max_retry_error, self.max_retry_error_detail)), + parse_mode='html', disable_web_page_preview=True + ) + logger.debug('%d %s', msg.chat.id, repr(msg.text)) + await self._insert_answer_history(msg) + else: + await msg.reply(self.max_retry_error_detail, parse_mode='html', + disable_web_page_preview=True) else: - msg.reply(self.problem_set['try_again'], parse_mode = 'html', disable_web_page_preview = True) - self.conn.execute("UPDATE `exam_user_session` SET `retries` = {} WHERE `user_id` = {}".format(userObj['retries'], msg.chat.id)) - - def click_to_join(self, client: Client, msg: CallbackQuery): + await msg.reply(self.try_again, parse_mode='html', disable_web_page_preview=True) + logger.debug('%d %s', msg.chat.id, repr(msg.text)) + await self._insert_answer_history(msg) + await self.conn.execute("UPDATE `exam_user_session` SET `retries` = %s WHERE `user_id` = %s", + (retries, msg.chat.id)) + + async def _insert_answer_history(self, msg: Message) -> None: + await self.conn.execute("INSERT INTO `answer_history` (`user_id`, `body`) VALUE (%s, %s)", + (msg.chat.id, msg.text[:200])) + + async def click_to_join(self, client: Client, msg: CallbackQuery) -> bool: if msg.data == 'iamready': try: - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - self.revoke_tracker_thread.send_link(msg.message.chat.id, True) - msg.answer('The invitation link has been sent.') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + await self._revoke_tracker_coro.send_link(msg.message.chat.id, True) + await msg.answer() except: traceback.print_exc() return True return False - - def send_link(self, msg: Message): - if self.problem_set.get('confirm_msg') and self.problem_set['confirm_msg']['enable']: - msg.reply( - self.problem_set['confirm_msg']['text'], - False, - 'html', - reply_markup = InlineKeyboardMarkup( inline_keyboard = [ - [InlineKeyboardButton( text = self.problem_set['confirm_msg']['button_text'], callback_data = b'iamready')] + + async def send_link(self, msg: Message, from_ticket: bool = False) -> None: + if self._send_link_confirm: + replyobj = dict( + text=self._confirm_message, + parse_mode='html', + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=self._confirm_button_text, callback_data='iamready')] ]) ) + if isinstance(msg, int): + replyobj.update(dict(chat_id=msg)) + await self.botapp.send_message(**replyobj) + else: + await msg.reply(**replyobj) else: - self.revoke_tracker_thread.send_link(msg.chat.id) + await self._revoke_tracker_coro.send_link(msg.chat.id, from_ticket) -class custom_service_bot_class(object): + def valid_answer(self, msg: Message, problem_body: Dict[str, str]) -> bool: + b = False + text = self.remove_punctuations.replace(msg.text) + if problem_body.get('use_regular_expression', False): + b = re.match(problem_body['A'], text) + else: + b = text == problem_body['A'] + logger.debug('verify %s %s == %s', b, text, problem_body['A']) + return b - SECTION = [ - 'VERIFICATION', - 'OTHER' - ] + +class CustomServiceBot: INIT_STATUS = 0 SELECT_SECTION = 1 SEND_QUESTION = 2 SEND_FINISH = 3 RE_TICKET_ID = re.compile(r'[a-f\d]{32}') - def __init__(self, config_file: str or ConfigParser, mysql_handle: tg_tools.mysqldb, send_link_callback: callable): + def __init__(self, config_file: Union[str, ConfigParser], mysql_handle: Optional[utils.MySQLdb], + send_link_callback: Optional[Awaitable[Callable[[Message, int], None]]], redis_conn: redis.Redis): if isinstance(config_file, ConfigParser): - self.config = config_file + config = config_file else: - self.config = ConfigParser() - self.config.read(config_file) - - self.mysqldb = mysql_handle if mysql_handle else tg_tools.mysqldb('localhost', 'root', self.config['database']['passwd'], self.config['database']['db_name']) - self.bot = Client( - self.config['account']['custom_api_key'], - api_id = self.config['account']['api_id'], - api_hash = self.config['account']['api_hash'] + config = ConfigParser() + config.read(config_file) + + self.mysqldb: utils.MySQLdb = mysql_handle if mysql_handle else utils.MySQLdb('localhost', 'root', + config['database']['passwd'], + config['database']['db_name']) + self._redis: redis.Redis = redis_conn + self.bot_id: int = int(config['custom_service']['custom_api_key'].split(':')[0]) + self.bot: Client = Client( + session_name=str(self.bot_id), + bot_token=config['custom_service']['custom_api_key'], + api_id=config['account']['api_id'], + api_hash=config['account']['api_hash'] ) - self.bot_id = int(self.config['account']['custom_api_key'].split(':')[0]) - self.help_group = int(self.config['fuduji']['help_group']) - self.send_link_callback = send_link_callback - self.emerg_contact = eval(self.config['account']['emerg_contact']) if self.config.has_option('account', 'emerg_contact') and self.config['account']['emerg_contact'] != '' else self.config['account']['owner'] - self.create_lock = Lock() + self.help_group: int = config.getint('custom_service', 'help_group') + self.send_link_callback: Optional[Awaitable[Callable[[Message, int], None]]] = send_link_callback + + self.SECTION: List[str] = [ + _T("VERIFICATION"), + _T("OTHER") + ] + + self.init_handle() + - def start(self): + def init_handle(self) -> None: self.bot.add_handler(MessageHandler(self.handle_start, Filters.command('start') & Filters.private)) - self.bot.add_handler(MessageHandler(self.handle_create, Filters.command('create',) & Filters.private)) + self.bot.add_handler(MessageHandler(self.handle_create, Filters.command('create', ) & Filters.private)) self.bot.add_handler(MessageHandler(self.handle_cancel, Filters.command('cancel') & Filters.private)) self.bot.add_handler(MessageHandler(self.handle_list, Filters.command('list') & Filters.private)) self.bot.add_handler(MessageHandler(self.handle_close, Filters.command('close') & Filters.private)) self.bot.add_handler(MessageHandler(self.handle_reply, Filters.reply & Filters.text & Filters.private)) self.bot.add_handler(MessageHandler(self.handle_msg, Filters.text & Filters.private)) - self.bot.add_handler(MessageHandler(self.call_superuser_function, Filters.chat(self.help_group) & Filters.reply & Filters.command('m'))) + self.bot.add_handler(MessageHandler(self.call_superuser_function, + Filters.chat(self.help_group) & Filters.reply & Filters.command('m'))) self.bot.add_handler(MessageHandler(self.handle_group, Filters.reply & Filters.chat(self.help_group))) - self.bot.add_handler(MessageHandler(self.handle_manual_add_blacklist, Filters.command('a') & Filters.chat(self.help_group))) self.bot.add_handler(MessageHandler(self.handle_other, Filters.private)) self.bot.add_handler(CallbackQueryHandler(self.answer)) - return self.bot.start() - def stop(self): - #self.save_session() - return self.bot.stop() + async def start(self) -> Client: + return await self.bot.start() - def idle(self): - return self.bot.idle() + async def stop(self) -> Client: + return await self.bot.stop() - def active(self): - self.start() - self.idle() + async def idle(self) -> Client: + return await self.bot.idle() - def send_emerg_msg(self, text: str): - if isinstance(self.emerg_contact, str): - self.bot.send_message(self.emerg_contact, text) - else: - for x in self.emerg_contact: - self.bot.send_message(x, text) + async def active(self) -> None: + await self.start() + await self.idle() @staticmethod - def hash_msg(msg: Message): - return hashlib.md5(' '.join((str(msg.from_user.id), str(msg.date), str(msg.message_id))).encode()).hexdigest() + def hash_msg(msg: Message) -> str: + return hashlib.md5(' '.join(map(str, (msg.from_user.id, msg.date, msg.message_id))).encode()).hexdigest() - def get_hash_from_reply_msg(self, msg: Message): + def get_hash_from_reply_msg(self, msg: Message) -> str: if msg.reply_to_message is None or \ - msg.reply_to_message.text is None or \ - msg.reply_to_message.from_user.id != self.bot_id or \ - msg.reply_to_message.entities is None or \ - msg.reply_to_message.entities[0].type != 'hashtag': - print(msg.reply_to_message is None, msg.reply_to_message.text is None, msg.reply_to_message.from_user.id != self.bot_id, msg.reply_to_message.entities is None, msg.reply_to_message.entities[0].type != 'hashtag') + msg.reply_to_message.text is None or \ + msg.reply_to_message.from_user.id != self.bot_id or \ + msg.reply_to_message.entities is None or \ + msg.reply_to_message.entities[0].type != 'hashtag': raise ValueError("hash message info error") r = self.RE_TICKET_ID.search(msg.reply_to_message.text) if r is not None: @@ -258,427 +462,550 @@ def get_hash_from_reply_msg(self, msg: Message): else: raise ValueError('hash info not found') - @staticmethod - def generate_section_pad(): - return ReplyKeyboardMarkup( keyboard = [ - [KeyboardButton( text = x )] for x in custom_service_bot_class.SECTION - ], resize_keyboard = True, one_time_keyboard = True) + def generate_section_pad(self) -> ReplyKeyboardMarkup: + return ReplyKeyboardMarkup(keyboard=[ + [KeyboardButton(text=x)] for x in self.SECTION + ], resize_keyboard=True, one_time_keyboard=True) @staticmethod - def generate_ticket_keyboard(ticket_id: str, user_id: int, closed: bool = False, other: bool = False): + def generate_ticket_keyboard(ticket_id: str, user_id: int, closed: bool=False, + other: bool=False) -> InlineKeyboardMarkup: kb = [ - InlineKeyboardButton(text = 'Close', callback_data = 'close {}'.format(ticket_id).encode()), - InlineKeyboardButton(text = 'Send link', callback_data = 'send {}'.format(user_id).encode()), - InlineKeyboardButton(text = 'Block', callback_data = 'block {}'.format(user_id).encode()) + InlineKeyboardButton(text=_T('Close'), callback_data=f'close {ticket_id}'), + InlineKeyboardButton(text=_T('Send link'), callback_data=f'send {user_id}'), + InlineKeyboardButton(text=_T('Block'), callback_data=f'block {user_id}') ] - if closed: kb = kb[2:] - elif other: kb.pop(1) + if closed: + kb = kb[2:] + elif other: + kb.pop(1) return InlineKeyboardMarkup( - inline_keyboard = [kb] + inline_keyboard=[kb] ) @staticmethod - def returnYNemoji(i: int): - return '✅' if i else '❌' - - def handle_list(self, client: Client, msg: Message): - q = self.mysqldb.query3("SELECT `hash`, `status` FROM `tickets` WHERE `user_id` = {} ORDER BY `timestamp` DESC LIMIT 3".format(msg.chat.id)) - if len(q) == 0 or q is None: - return msg.reply('You have never used this system before.', True) + def return_bool_emoji(i: _anyT) -> str: + return '\u2705' if i else '\u274c' + + async def handle_list(self, _client: Client, msg: Message) -> None: + q = await self.mysqldb.query( + "SELECT `hash`, `status` FROM `tickets` WHERE `user_id` = %s ORDER BY `timestamp` DESC LIMIT 3", + msg.chat.id) + if not q: + await msg.reply(_T('You have never used this system before.'), True) + return for _ticket in q: - _ticket['status'] = self.returnYNemoji(_ticket['status'] != 'closed') - msg.reply('Here are the last three tickets (up to 3)\n#{}'.format('\n#'.join(' '.join(value for _, value in _ticket.items()) for _ticket in q)), True) + _ticket['status'] = self.return_bool_emoji(_ticket['status'] != 'closed') + await msg.reply(_T('Here are the last three tickets (up to 3)\n#{}').format( + '\n#'.join(' '.join(value for _, value in _ticket.items()) for _ticket in q)), True) - def handle_close(self, client: Client, msg: Message): + async def handle_close(self, client: Client, msg: Message) -> None: if msg.reply_to_message is not None and msg.text == '/close': try: ticket_id = self.get_hash_from_reply_msg(msg) except ValueError: - return msg.reply('TICKET NUMBER NOT FOUND\nPlease make sure that you have replied to the message which contains the ticket number.', True) + await msg.reply(_T( + 'TICKET NUMBER NOT FOUND\nPlease make sure that you have replied to the message which contains the ticket number.'), + True) + return else: if len(msg.text) < 8: - return msg.reply('ERROR: COMMAND FORMAT Please use `/close ` or **Reply to the message which contains the ticket number** to close the ticket', True, 'markdown', True) + await msg.reply(_T( + 'ERROR: COMMAND FORMAT Please use `/close ` or **Reply to the message which contains the ticket number** to close the ticket'), + True, 'markdown', True) + return ticket_id = msg.text.split()[-1] if len(ticket_id) != 32: - return msg.reply('ERROR: TICKET NUMBER FORMAT', True) - q = self.mysqldb.query1("SELECT `user_id` FROM `tickets` WHERE `hash` = '{}' AND `status` != 'closed'".format(ticket_id)) + await msg.reply(_T('ERROR: TICKET NUMBER FORMAT'), True) + return + q = await self.mysqldb.query1("SELECT `user_id` FROM `tickets` WHERE `hash` = %s AND `status` != 'closed'", + ticket_id) if q is None: - return msg.reply('TICKET NUMBER NOT FOUND or TICKET CLOSED', True) + await msg.reply(_T('TICKET NUMBER NOT FOUND or TICKET CLOSED'), True) + return if q['user_id'] != msg.chat.id: - return msg.reply('403 Forbidden(You cannot close a ticket created by others. If this ticket is indeed created by yourself, please report the problem using the same ticket.)', True) - self.mysqldb.execute("UPDATE `tickets` SET `status` = 'closed' WHERE `user_id` = {} AND `hash` = '{}'".format(msg.chat.id, ticket_id)) - self.mysqldb.execute("UPDATE `tickets_user` SET `last_time` = CURRENT_TIMESTAMP() WHERE `user_id` = {}".format(msg.chat.id)) - self.mysqldb.commit() - client.send_message(self.help_group, "UPDATE\n[ #{} ]\nThis ticket is already closed by {}".format(ticket_id, tg_tools.build_html_parse.parse_user(msg.chat.id, '创建者')), reply_markup = self.generate_ticket_keyboard(ticket_id, msg.chat.id, True)) - msg.reply('DONE!', True) - - def add_user(self, user_id: int, step: int = 0): - self.mysqldb.execute("INSERT INTO `tickets_user` (`user_id`, `create_time`, `last_time`, `last_msg_sent`, `step`) VALUES ({}, CURRENT_TIMESTAMP(), DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 5 minute), CURRENT_TIMESTAMP(), {})".format(user_id, step)) - - def change_step(self, user_id: int, step: int, section: str = ''): + await msg.reply(_T( + '403 Forbidden(You cannot close a ticket created by others. If this ticket is indeed created by yourself, please report the problem using the same ticket.)'), + True) + return + await self.mysqldb.execute("UPDATE `tickets` SET `status` = 'closed' WHERE `user_id` = %s AND `hash` = %s", + (msg.chat.id, ticket_id)) + await self._update_last_time(msg) + await client.send_message(self.help_group, + _T('UPDATE\n[ #{} ]\nThis ticket is already closed by {}').format( + ticket_id, + utils.TextParser.parse_user(msg.chat.id, _T('Creater'))), + reply_markup=self.generate_ticket_keyboard(ticket_id, msg.chat.id, other=True)) + await msg.reply(_T('Close ticket success.'), True) + + async def add_user(self, user_id: int, step: int=0) -> None: + await self.mysqldb.execute( + "INSERT INTO `tickets_user` (`user_id`, `create_time`, `step`) VALUES (%s, CURRENT_TIMESTAMP(), %s)", + (user_id, step)) + + async def change_step(self, user_id: int, step: int, section: str = '') -> None: if section == '': - self.mysqldb.execute("UPDATE `tickets_user` SET `step` = {} WHERE `user_id` = {}".format(step, user_id)) + await self.mysqldb.execute("UPDATE `tickets_user` SET `step` = %s WHERE `user_id` = %s", (step, user_id)) else: - self.mysqldb.execute("UPDATE `tickets_user` SET `step` = {0}, `section` = '{2}' WHERE `user_id` = {1}".format(step, user_id, section)) - self.mysqldb.commit() + await self.mysqldb.execute("UPDATE `tickets_user` SET `step` = %s, `section` = %s WHERE `user_id` = %s", + (step, section, user_id)) - def query_status(self, user_id: int): - return self.mysqldb.query1("SELECT `step`, `section` FROM `tickets_user` WHERE `user_id` = {}".format(user_id)) - def query_user(self, user_id: int): - return self.mysqldb.query1("SELECT `section` FROM `tickets_user` WHERE `user_id` = {}".format(user_id)) + async def query_status(self, user_id: int) -> Optional[Mapping[_kT, _rT]]: + return await self.mysqldb.query1("SELECT `step`, `section` FROM `tickets_user` WHERE `user_id` = %s", user_id) - def set_section(self, user_id: int, section: str): - self.mysqldb.execute("UPDATE `tickets_user` SET `section` = '{1}' WHERE `user_id` = {0}".format(user_id, section)) - self.mysqldb.commit() + async def query_user(self, user_id: int) -> Optional[Mapping[_anyT, _anyT]]: + return await self.mysqldb.query1("SELECT `section` FROM `tickets_user` WHERE `user_id` = %s", user_id) - def query_user_exam_status(self, user_id: int): - return self.mysqldb.query1("SELECT `baned`, `bypass`, `passed`, `unlimited`, `retries` FROM `exam_user_session` WHERE `user_id` = {}".format(user_id)) + async def set_section(self, user_id: int, section: str) -> None: + await self.mysqldb.execute("UPDATE `tickets_user` SET `section` = %s WHERE `user_id` = %s", (section, user_id)) - def handle_start(self, client: Client, msg: Message): - q = self.mysqldb.query1("SELECT `last_msg_sent` FROM `tickets_user` WHERE `user_id` = {}".format(msg.chat.id)) - msg.reply('Welcome to Google Hosts Telegram Ticket System\n\nATTENTION:PLEASE DO NOT ABUSE THIS SYSTEM. Otherwise there is a possibility of getting blocked.\n\n/create - to create a new ticket\n/list - to list recent tickets\n/close - to close the ticket\n/cancel - to reset', True) + + async def query_user_exam_status(self, user_id: int) -> Optional[Mapping[_anyT, _anyT]]: + return await self.mysqldb.query1( + "SELECT `problem_id`, `baned`, `bypass`, `passed`, `unlimited`, `retries` FROM `exam_user_session` WHERE `user_id` = %s", + user_id) + + async def handle_start(self, _client: Client, msg: Message) -> None: + q = await self.mysqldb.query1("SELECT `last_msg_sent` FROM `tickets_user` WHERE `user_id` = %s", msg.chat.id) + await msg.reply(_T( + 'Welcome to Google Hosts Telegram Ticket System\n\nATTENTION:PLEASE DO NOT ABUSE THIS SYSTEM. Otherwise there is a possibility of getting blocked.\n\n/create - to create a new ticket\n/list - to list recent tickets\n/close - to close the ticket\n/cancel - to reset'), + True) if q is None: - self.add_user(msg.chat.id) + await self.add_user(msg.chat.id) - def handle_create(self, client: Client, msg: Message): - if self.flood_check(client, msg): + async def handle_create(self, client: Client, msg: Message) -> None: + if await self.flood_check(client, msg): return - q = self.mysqldb.query1("SELECT `hash` FROM `tickets` WHERE `user_id` = {} AND `status` = 'open' LIMIT 1".format(msg.chat.id)) + q = await self.mysqldb.query1("SELECT `hash` FROM `tickets` WHERE `user_id` = %s AND `status` = 'open' LIMIT 1", + msg.chat.id) if q: - msg.reply('UNABLE TO CREATE A NEW TICKET: An existing ticket is currently open.', True) + await msg.reply(_T('UNABLE TO CREATE A NEW TICKET: An existing ticket is currently open.'), True) return - sqlObj = self.mysqldb.query1("SELECT `user_id` FROM `tickets_user` WHERE `user_id` = {}".format(msg.chat.id)) - (self.add_user if sqlObj is None else self.change_step)(msg.chat.id, custom_service_bot_class.SELECT_SECTION) - msg.reply('You are creating a new ticket.\n\nPlease choose the correct department.', True, reply_markup=self.generate_section_pad()) + sqlObj = await self.mysqldb.query1("SELECT `user_id` FROM `tickets_user` WHERE `user_id` = %s", msg.chat.id) + await (self.add_user if sqlObj is None else self.change_step)(msg.chat.id, CustomServiceBot.SELECT_SECTION) + await msg.reply(_T('You are creating a new ticket.\n\nPlease choose the correct department.'), True, + reply_markup=self.generate_section_pad()) - def handle_cancel(self, client: Client, msg: Message): - self.change_step(msg.chat.id, custom_service_bot_class.INIT_STATUS) - msg.reply('Reset Successful', reply_markup=ReplyKeyboardRemove()) + async def handle_cancel(self, _client: Client, msg: Message) -> None: + await self.change_step(msg.chat.id, CustomServiceBot.INIT_STATUS) + await msg.reply(_T("Reset Successful"), reply_markup=ReplyKeyboardRemove()) - def handle_reply(self, client: Client, msg: Message): - if self.flood_check(client, msg): + async def handle_reply(self, client: Client, msg: Message) -> None: + if await self.flood_check(client, msg): + return + try: + ticket_hash = self.get_hash_from_reply_msg(msg) + except ValueError: return - ticket_hash = self.get_hash_from_reply_msg(msg) - sqlObj = self.mysqldb.query1("SELECT `status`, `section` FROM `tickets` WHERE `hash` = '{}' AND `user_id` = {}".format(ticket_hash, msg.chat.id)) + sqlObj = await self.mysqldb.query1( + "SELECT `status`, `section` FROM `tickets` WHERE `hash` = %s AND `user_id` = %s", + (ticket_hash, msg.chat.id)) if sqlObj is None or sqlObj['status'] == 'closed': - msg.reply('TICKET NUMBER NOT FOUND or TICKET CLOSED. REPLY FUNCTION NO LONGER AVAILABLE.', True) + await msg.reply(_T('TICKET NUMBER NOT FOUND or TICKET CLOSED. REPLY FUNCTION NO LONGER AVAILABLE.'), True) return - self.mysqldb.execute("UPDATE `tickets_user` SET `last_time` = CURRENT_TIMESTAMP() WHERE `user_id` = {}".format(msg.chat.id)) - self.mysqldb.commit() - client.send_message( + await self._update_last_time(msg) + await client.send_message( self.help_group, - 'NEW REPLY\n[ #{} ]:\nMESSAGE: {}'.format(ticket_hash, build_html_parse(msg).parsed_msg), + _T("\'NEW REPLY\n[ #{} ]:\nMESSAGE: {}").format(ticket_hash, TextParser(msg).parsed_msg), 'html', - reply_markup = self.generate_ticket_keyboard(ticket_hash, msg.chat.id, sqlObj['section'] != self.SECTION[0]) + reply_markup=self.generate_ticket_keyboard(ticket_hash, msg.chat.id, sqlObj['section'] != self.SECTION[0]) ) - msg.reply('The new reply is added successfully!') + await msg.reply(_T('The new reply is added successfully!')) - def handle_msg(self, client: Client, msg: Message): - sqlObj = self.query_status(msg.chat.id) - if sqlObj is None or sqlObj['step'] not in (custom_service_bot_class.SELECT_SECTION, custom_service_bot_class.SEND_QUESTION): - if self.flood_check(client, msg): + async def handle_msg(self, client: Client, msg: Message) -> None: + sqlObj = await self.query_status(msg.chat.id) + if sqlObj is None or sqlObj['step'] not in (CustomServiceBot.SELECT_SECTION, CustomServiceBot.SEND_QUESTION): + if await self.flood_check(client, msg): return - return msg.reply('Please use bot command to interact.') - if sqlObj['step'] == custom_service_bot_class.SELECT_SECTION: + await msg.reply(_T('Please use bot command to interact.')) + return + if sqlObj['step'] == CustomServiceBot.SELECT_SECTION: if msg.text in self.SECTION: - self.change_step(msg.chat.id, custom_service_bot_class.SEND_QUESTION, msg.text) - msg.reply('Please describe your problem briefly(up to 500 characters)\n(Please use external links to send pictures.):\n\nATTENTION: Receiving a confirmation message in return indicates that the ticket is created successfully.\n\nUse /cancel to cancel creating the ticket. ', True, reply_markup = ReplyKeyboardRemove()) + await self.change_step(msg.chat.id, CustomServiceBot.SEND_QUESTION, msg.text) + await msg.reply(_T( + 'Please describe your problem briefly(up to 500 characters)\n(Please use external links to send pictures.):\n\nATTENTION: Receiving a confirmation message in return indicates that the ticket is created successfully.\n\nUse /cancel to cancel creating the ticket.'), + True, reply_markup=ReplyKeyboardRemove()) else: - msg.reply('Please use the menu below to choose the correct department.', True) - elif sqlObj['step'] == custom_service_bot_class.SEND_QUESTION: + await msg.reply(_T('Please use the menu below to choose the correct department.'), True) + elif sqlObj['step'] == CustomServiceBot.SEND_QUESTION: if len(msg.text) > 500: - msg.reply('The number of characters you have entered is larger than 500. Please re-enter.', True) + await msg.reply(_T('The number of characters you have entered is larger than 500. Please re-enter.'), + True) return ticket_hash = self.hash_msg(msg) - self.mysqldb.execute(ticket(msg, sqlObj['section'], 'open').sql) - self.mysqldb.commit() - self.change_step(msg.chat.id, custom_service_bot_class.INIT_STATUS) - msg.reply( - 'The ticket is created successfully!\n[ #{ticket_id} ]\nDepartment: {section}\nMessage: \n{text}\n\nReply to this message to add a new reply to the ticket.'.format( - ticket_id = ticket_hash, - text = build_html_parse(msg).parsed_msg, - section = sqlObj['section'] + await self.mysqldb.execute(*Ticket(msg, sqlObj['section'], 'open').sql) + await self.change_step(msg.chat.id, CustomServiceBot.INIT_STATUS) + await msg.reply( + _T( + 'The ticket is created successfully!\n[ #{ticket_id} ]\nDepartment: {section}\nMessage: \n{text}\n\nReply to this message to add a new reply to the ticket.').format( + ticket_id=ticket_hash, + text=TextParser(msg).parsed_msg, + section=sqlObj['section'] ), - parse_mode = 'html' + parse_mode='html' ) - msg_id = client.send_message( + msg_id = (await client.send_message( self.help_group, - 'NEW TICKET\n[ #{} ]\nClick {} to check the user profile\nDepartment: {}\nMessage: \n{}'.format( + _T('NEW TICKET\n[ #{} ]\nClick {} to check the user profile\nDepartment: {}\nMessage: \n{}').format( ticket_hash, - build_html_parse.parse_user_ex(msg.chat.id, 'Here'), + TextParser.parse_user_ex(msg.chat.id, _T('Here')), sqlObj['section'], - build_html_parse(msg).parsed_msg + TextParser(msg).parsed_msg ), 'html', - reply_markup = self.generate_ticket_keyboard( + reply_markup=self.generate_ticket_keyboard( ticket_hash, msg.chat.id, - other = sqlObj['section'] != custom_service_bot_class.SECTION[0] + other=sqlObj['section'] != self.SECTION[0] ) - ).message_id - if sqlObj['section'] == custom_service_bot_class.SECTION[0]: - client.send_message( + )).message_id + if sqlObj['section'] == self.SECTION[0]: + await client.send_message( self.help_group, - self.generate_user_status(msg.chat.id), - 'markdown', - reply_to_message_id = msg_id + await self.generate_user_status(msg.chat.id), + 'html', + reply_to_message_id=msg_id ) else: - print("throw! user_id: {}, sqlObj = {}".format(msg.chat.id, repr(sqlObj))) - - def generate_user_status(self, user_id: int): - user_status = self.query_user_exam_status(user_id) - return 'User {5} status:\nPassed exam: {0}\nBan status: {1}\nBypass: {2}\nUnlimited: {3}\nRetries: {4}'.format( - self.returnYNemoji(user_status['passed']), - self.returnYNemoji(user_status['baned']), - self.returnYNemoji(user_status['bypass']), - self.returnYNemoji(user_status['unlimited']), + logger.error("throw! user_id: %d, sqlObj = %s", msg.chat.id, repr(sqlObj)) + + async def generate_question_and_answer(self, user_session: Mapping[str, _rT]) -> str: + _text = 'Question: {Q}\n{qtype} Answer: {A}'.format( + **await ProblemSet.get_instance().get(user_session['problem_id']), + qtype='Except' if ProblemSet.get_instance().remove_punctuations.enable else 'Standard') + if ProblemSet.get_instance().remove_punctuations.enable: + _text += f'\nStandard Answer: {await ProblemSet.get_instance().get_origin(user_session["problem_id"])}' + return _text + + async def __generate_answer_history(self, user_id: int) -> str: + sqlObj = await self.mysqldb.query( + 'SELECT `body`, `timestamp` FROM `answer_history` WHERE `user_id` = %s ORDER BY `_id` DESC LIMIT 3', + user_id) + if sqlObj is None: + return 'QUERY ERROR (user_id => %d)' % user_id + if ProblemSet.get_instance().remove_punctuations.enable: + return '\n\n'.join('{}
{}
\nOriginal answer:
{}
'.format( + x['timestamp'], ProblemSet.get_instance().remove_punctuations.replace(x['body']), x['body']) for x in + sqlObj) + return '\n\n'.join('{}
{}
'.format(x['timestamp'], x['body']) for x in sqlObj) + + async def _generate_answer_history(self, user_id: int, retries: int) -> str: + hsqlObj = await self.mysqldb.query1("SELECT COUNT(*) as `count` FROM `answer_history` WHERE `user_id` = %s", + user_id) + if retries > 0 or hsqlObj['count'] > 0: + return '\n\nAnswer History:\n{}'.format(await self.__generate_answer_history(user_id)) + return '' + + async def generate_question_rate(self, user_session: Mapping[str, int]) -> str: + problem_id = user_session['problem_id'] + total_count = ( + await self.mysqldb.query1("SELECT COUNT(*) as `count` FROM `exam_user_session` WHERE `problem_id` = %s", + problem_id))['count'] + correct_count = (await self.mysqldb.query1( + "SELECT COUNT(*) as `count` FROM `exam_user_session` WHERE `problem_id` = %s and `passed` = 1", + problem_id))['count'] + rate = (correct_count / total_count) * 100 + return '\n\nProblem {} correct rate: {:.2f}%'.format(problem_id, rate) + + async def generate_user_status(self, user_id: int) -> str: + user_status = await self.query_user_exam_status(user_id) + return 'User {5} status:\nPassed exam: {0}\nBan status: {1}\nBypass: {2}\nUnlimited: {3}\nRetries: {4}\n\n{6}{7}{8}'.format( + self.return_bool_emoji(user_status['passed']), + self.return_bool_emoji(user_status['baned']), + self.return_bool_emoji(user_status['bypass']), + self.return_bool_emoji(user_status['unlimited']), user_status['retries'], - build_html_parse.parse_user(user_id) - ) if user_status is not None else '**WARNING: THIS USER HAS NEVER USED THE BOT BEFORE.**' - - def handle_other(self, client: Client, msg: Message): - q = self.mysqldb.query1("SELECT `last_msg_sent` FROM `tickets_user` WHERE `user_id` = {}".format(msg.chat.id)) - if (datetime.now() - q['last_msg_sent']).total_seconds() < 120: + TextParser.parse_user_ex(user_id), + await self.generate_question_and_answer(user_status), + await self.generate_question_rate(user_status), + await self._generate_answer_history(user_id, user_status['retries']) + ) if user_status is not None else '{}'.format(_T('WARNING: THIS USER HAS NEVER USED THE BOT BEFORE.')) + + async def handle_other(self, _client: Client, msg: Message) -> None: + if time.time() - await self._query_last_msg_send(msg) < 120: return - msg.reply('Please use bot command to interact. TEXT ONLY.') + await msg.reply(_T('Please use bot command to interact. TEXT ONLY.')) + await self._update_last_msg_send(msg) - def handle_group(self, client: Client, msg: Message): - #print(msg) + async def handle_group(self, client: Client, msg: Message) -> None: if msg.reply_to_message.from_user.id != self.bot_id or (msg.text and msg.text.startswith('/')): return - ticket_hash = self.get_hash_from_reply_msg(msg) - sqlObj = self.mysqldb.query1("SELECT * FROM `tickets` WHERE `hash` = '{}'".format(ticket_hash)) + try: + ticket_hash = self.get_hash_from_reply_msg(msg) + except ValueError: + return + sqlObj = await self.mysqldb.query1("SELECT * FROM `tickets` WHERE `hash` = %s", ticket_hash) if sqlObj is None: - return msg.reply('ERROR: TICKET NOT FOUND') + await msg.reply(_T('ERROR: TICKET NOT FOUND')) + return if sqlObj['status'] == 'closed': - return msg.reply('This ticket is already closed.') - msg_reply = client.send_message(sqlObj['user_id'], 'NEW UPDATE!\n[ #{} ]\nMessage: \n{}\n\nReply to this message to add a new reply to the ticket.'.format(ticket_hash, build_html_parse(msg).parsed_msg), 'html') - msg.reply('REPLY SUCCESSFUL', reply_markup = InlineKeyboardMarkup(inline_keyboard = [ - [ - InlineKeyboardButton( text = 'recall', callback_data = ' '.join(('del', str(msg_reply.chat.id), str(msg_reply.message_id))).encode()) - ] - ])) - sqlObj = self.mysqldb.query1("SELECT `last_time`, `user_id` FROM `tickets_user` WHERE user_id = {}".format(sqlObj['user_id'])) - if (datetime.now() - sqlObj['last_time']).total_seconds() < 120: - self.mysqldb.execute("UPDATE `tickets_user` SET `last_time` = DATE_SUB(CURRENT_TIMESTAMP(), INTERVAL 3 minute) WHERE `user_id` = %s", (sqlObj['user_id'],)) + await msg.reply(_T('This ticket is already closed.')) + return + try: + msg_reply = await client.send_message(sqlObj['user_id'], + _T( + 'NEW UPDATE!\n[ #{} ]\nMessage: \n{}\n\nReply to this message to add a new reply to the ticket').format( + ticket_hash, TextParser(msg).parsed_msg + ), 'html') + await msg.reply(_T('REPLY [ #{} ] SUCCESSFUL').format(ticket_hash), + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text=_T('recall'), + callback_data=f'del {msg_reply.chat.id} {msg_reply.message_id}') + ] + ])) + r = await self._query_last_time(msg) + if time.time() - r < 120: + await self._redis.delete(f'CSLAST_{sqlObj["user_id"]}') + except pyrogram.errors.UserIsBlocked: + await msg.reply(_T('Replay [ #{} ] fail,user blocked this bot.').format(ticket_hash)) + except pyrogram.errors.RPCError: + await msg.reply(_T('Replay [ #{} ] fail, {}\nView console to get more information').format(ticket_hash, + traceback.format_exc().splitlines()[ + -1])) + raise - def handle_manual_add_blacklist(self, client: Client, msg: Message): - pass @staticmethod - def generate_confirm_keyboard(first: str, last: str): + def generate_confirm_keyboard(first: str, last: Union[str, Sequence[str]]) -> InlineKeyboardMarkup: if isinstance(last, list) or isinstance(last, tuple): lastg = last else: lastg = (str(last),) - return InlineKeyboardMarkup(inline_keyboard = [ + return InlineKeyboardMarkup(inline_keyboard=[ [ - InlineKeyboardButton(text = 'Yes', callback_data = ' '.join((first, 'confirm', *lastg)).encode()), - InlineKeyboardButton(text = 'No', callback_data = b'cancel') + InlineKeyboardButton(text='Yes', callback_data=' '.join((first, 'confirm', *lastg))), + InlineKeyboardButton(text='No', callback_data='cancel') ] ]) - def generate_superuser_text(self, user_id: str or int): - return '\n\n'.join(('Please choose the section below.', self.generate_user_status(user_id), ' '.join(('Last refresh:', time.strftime('%Y-%m-%d %H:%M:%S'))))) + async def generate_superuser_text(self, user_id: Union[str, int]) -> str: + return '\n\n'.join((_T("Please choose the section below"), await self.generate_user_status(user_id), + ' '.join((_T('Last refresh:'), str(datetime.now().replace(microsecond=0)))))) - def generate_superuser_detail(self, user_id: str or int): + async def generate_superuser_detail(self, user_id: Union[str, int]) -> Dict[str, _rT]: return { - 'text': self.generate_superuser_text(user_id), + 'text': await self.generate_superuser_text(user_id), 'reply_markup': InlineKeyboardMarkup( - inline_keyboard = [ + inline_keyboard=[ [ - InlineKeyboardButton( text = 'BYPASS', callback_data = 'bypass {}'.format(user_id).encode()), - InlineKeyboardButton( text = 'UNLIMITED RETRIES', callback_data = 'unlimited {}'.format(user_id).encode()), - InlineKeyboardButton( text = 'REFRESH', callback_data = 'refresh {}'.format(user_id).encode()) + InlineKeyboardButton(text=_T('BYPASS'), callback_data=f'bypass {user_id}'), + InlineKeyboardButton(text=_T('UNLIMITED RETRIES'), callback_data=f'unlimited {user_id}'), + InlineKeyboardButton(text=_T('REFRESH'), callback_data=f'refresh {user_id}') ], [ - InlineKeyboardButton( text = 'PASS', callback_data = 'setpass {}'.format(user_id).encode()), - InlineKeyboardButton( text = 'RESET TIMES', callback_data = 'reset {}'.format(user_id).encode()) + InlineKeyboardButton(text=_T('PASS'), callback_data=f'setpass {user_id}'), + InlineKeyboardButton(text=_T('RESET TIMES'), callback_data=f'reset {user_id}') ], [ - InlineKeyboardButton( text = 'RESET USER STATUS', callback_data = 'renew {}'.format(user_id).encode()) + InlineKeyboardButton(text=_T('RESET USER STATUS'), callback_data=f'renew {user_id}') ], [ - InlineKeyboardButton( text = 'Cancel', callback_data = b'cancel') + InlineKeyboardButton(text=_T('Cancel'), callback_data='cancel') ] ] ) } - def call_superuser_function(self, client: Client, msg: Message): - sqlObj = self.mysqldb.query1("SELECT `user_id`, `section` FROM `tickets` WHERE `hash` = '{}'".format(self.get_hash_from_reply_msg(msg))) + async def call_superuser_function(self, client: Client, msg: Message) -> None: + sqlObj = await self.mysqldb.query1("SELECT `user_id`, `section` FROM `tickets` WHERE `hash` = %s", + self.get_hash_from_reply_msg(msg)) if sqlObj['section'] != self.SECTION[0]: - return msg.reply('This ticket doesn\'t support admin menus for now.', True) + await msg.reply(_T("This ticket doesn\'t support admin menus for now."), True) + return user_id = sqlObj['user_id'] - client.send_message( + await client.send_message( self.help_group, - parse_mode = 'markdown', - reply_to_message_id = msg.reply_to_message.message_id, - **self.generate_superuser_detail(user_id) + parse_mode='html', + reply_to_message_id=msg.reply_to_message.message_id, + **await self.generate_superuser_detail(user_id) ) - def confirm_dialog(self, msg: Message, additional_msg: str, callback_prefix: str, user_id: int = None, ticket_id: str = None): - if user_id is not None: - self.bot.send_message( + async def confirm_dialog(self, msg: CallbackQuery, additional_msg: str, callback_prefix: str, + id_: Optional[Union[str, int]]) -> None: + await msg.answer() + if len(id_) < 32: + await self.bot.send_message( self.help_group, - 'Do you really want to {} {}?'.format(additional_msg, build_html_parse.parse_user(user_id)), + _T('Do you really want to {} {}?').format(additional_msg, TextParser.parse_user(id_)), 'markdown', - reply_markup = self.generate_confirm_keyboard(callback_prefix, user_id) + reply_markup=self.generate_confirm_keyboard(callback_prefix, id_) ) else: - self.bot.send_message( + await self.bot.send_message( self.help_group, - 'Do you really want to {} #{}?'.format(additional_msg, ticket_id), - reply_markup = self.generate_confirm_keyboard(callback_prefix, ticket_id) + _T('Do you really want to {} #{}?').format(additional_msg, id_), + reply_markup=self.generate_confirm_keyboard(callback_prefix, id_) ) - def confirm(self, client: Client, msg: CallbackQuery): + async def confirm(self, client: Client, msg: CallbackQuery) -> None: if time.time() - msg.message.date > 15: raise TimeoutError() if msg.data.startswith('close'): ticket_id = msg.data.split()[-1] - q = self.mysqldb.query1("SELECT `user_id`, `status` FROM `tickets` WHERE `hash` = '{}'".format(ticket_id)) + q = await self.mysqldb.query1("SELECT `user_id`, `status` FROM `tickets` WHERE `hash` = %s", ticket_id) if q is None: - return msg.answer('TICKET NOT FOUND', True) + return await msg.answer(_T('TICKET NOT FOUND'), True) if q['status'] == 'closed': - return msg.answer('TICKET CLOSED') - self.mysqldb.execute("UPDATE `tickets` SET `status` = 'closed' WHERE `hash` = '{}'".format(ticket_id)) - msg.answer('This ticket is closed.') - client.send_message( + return await msg.answer(_T('This ticket is already closed.')) + await self.mysqldb.execute("UPDATE `tickets` SET `status` = 'closed' WHERE `hash` = %s", ticket_id) + await msg.answer(_T('This ticket is already closed.')) + await client.send_message( self.help_group, - "UPDATE\n[ #{} ]\nThis ticket is closed by {}.".format( + _T('UPDATE\n[ #{} ]\nThis ticket is closed by {}.').format( ticket_id, - tg_tools.build_html_parse.parse_user( + utils.TextParser.parse_user( msg.from_user.id, - tg_tools.build_html_parse.user_name(msg.from_user).full_name + utils.TextParser.UserName(msg.from_user).full_name ) ), 'markdown', - reply_markup = self.generate_ticket_keyboard(ticket_id, q['user_id'], True) + reply_markup=self.generate_ticket_keyboard(ticket_id, q['user_id'], True) ) - client.send_message(q['user_id'], "Your ticket [ #{} ] is closed".format(ticket_id)) + await client.send_message(q['user_id'], _T('Your ticket [ #{} ] is closed').format(ticket_id)) elif msg.data.startswith('block'): - self.mysqldb.execute("UPDATE `tickets_user` SET `baned` = 1 WHERE `user_id` = {}".format(msg.data.split()[-1])) - msg.answer('DONE!') - self.bot.send_message( + await self.mysqldb.execute("UPDATE `tickets_user` SET `baned` = 1 WHERE `user_id` = %s", + msg.data.split()[-1]) + await msg.answer(_T('DONE!')) + await self.bot.send_message( self.help_group, - 'blocked {}'.format(build_html_parse.parse_user(msg.data.split()[-1], msg.data.split()[-1])), - parse_mode = 'markdown', - reply_markup = InlineKeyboardMarkup( inline_keyboard = [ - [InlineKeyboardButton(text = 'UNBAN', callback_data = 'unban {}'.format(msg.data.split()[-1]).encode())] + _T('blocked {}').format(TextParser.parse_user(msg.data.split()[-1], msg.data.split()[-1])), + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=_T('UNBAN'), callback_data='unban {}'.format(msg.data.split()[-1]))] ]) ) elif msg.data.startswith('send'): try: - self.send_link_callback(int(msg.data.split()[-1]), True) - msg.answer('The invitation link is sent successfully.') + await self.send_link_callback(int(msg.data.split()[-1]), True) + await msg.answer(_T('The invitation link is sent successfully.')) except: - client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview = True) - msg.answer('Failed to send the invitation link. Please check the console.\n{}'.format(traceback.format_exc().splitlines()[-1]), True) + await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True) + await msg.answer(_T('Failed to send the invitation link. Please check the console.\n{}').format( + traceback.format_exc().splitlines()[-1]), True) elif msg.data.startswith('reset'): - self.mysqldb.execute('UPDATE `exam_user_session` SET `retries` = 0 WHERE `user_id` = {}'.format(msg.data.split()[-1])) - msg.answer('Retry times has been reset') + await self.mysqldb.execute('UPDATE `exam_user_session` SET `retries` = 0 WHERE `user_id` = %s', + msg.data.split()[-1]) + await msg.answer('Retry times has been reset') elif msg.data.startswith('del'): try: - client.delete_messages(int(msg.data.split()[-2]), int(msg.data.split()[-1])) - msg.answer('message has been deleted') + await client.delete_messages(int(msg.data.split()[-2]), int(msg.data.split()[-1])) + await msg.answer('message has been deleted') except: - client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview = True) - msg.answer('Failed to delete the message. Please check the console.\n{}'.format(traceback.format_exc().splitlines()[-1]), True) + await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True) + await msg.answer(_T('Failed to delete the message. Please check the console.\n{}').format( + traceback.format_exc().splitlines()[-1]), True) elif msg.data.startswith('renew'): - self.mysqldb.execute('DELETE FROM `exam_user_session` WHERE `user_id` = {}'.format(msg.data.split()[-1])) - msg.answer('User Profile Deleted') + await self.mysqldb.execute('DELETE FROM `exam_user_session` WHERE `user_id` = %s', msg.data.split()[-1]) + await msg.answer(_T('DONE!')) elif msg.data.startswith('bypass'): - self.mysqldb.execute('UPDATE `exam_user_session` SET `bypass` = 1 WHERE `user_id` = {}'.format(msg.data.split()[-1])) - msg.answer('BYPASS SET SUCCESSFULLY') + await self.mysqldb.execute('UPDATE `exam_user_session` SET `bypass` = 1 WHERE `user_id` = %s', + msg.data.split()[-1]) + await msg.answer(_T('DONE!')) elif msg.data.startswith('setpass'): - self.mysqldb.execute('UPDATE `exam_user_session` SET `passed` = 1 WHERE `user_id` = {}'.format(msg.data.split()[-1])) - msg.answer('PASS SET SUCCESSFULLY') + await self.mysqldb.execute('UPDATE `exam_user_session` SET `passed` = 1 WHERE `user_id` = %s', + msg.data.split()[-1]) + await msg.answer(_T('DONE!')) elif msg.data.startswith('unlimited'): - self.mysqldb.execute('UPDATE `exam_user_session` SET `unlimited` = 1 WHERE `user_id` = {}'.format(msg.data.split()[-1])) - msg.answer('UNLIMITED RETRIES SET SUCCESSFULLY') - self.mysqldb.commit() - client.delete_messages(msg.message.chat.id, msg.message.message_id) - - def send_confirm(self, client: Client, msg: CallbackQuery): - if msg.data.startswith('close'): - self.confirm_dialog(msg, 'close this ticket', 'close',ticket_id = msg.data.split()[-1]) - elif msg.data.startswith('block'): - self.confirm_dialog(msg, 'block this user', 'block', user_id = int(msg.data.split()[-1])) - elif msg.data.startswith('send'): - self.confirm_dialog(msg, 'send the link to', 'send', user_id = int(msg.data.split()[-1])) - elif msg.data.startswith('reset'): - self.confirm_dialog(msg, 'reset retry times for', 'reset', user_id = int(msg.data.split()[-1])) - elif msg.data.startswith('del'): - msg.answer('Please press again to make sure. If you really want to delete this reply', True) - self.bot.send_message( + await self.mysqldb.execute('UPDATE `exam_user_session` SET `unlimited` = 1 WHERE `user_id` = %s', + msg.data.split()[-1]) + await msg.answer(_T('DONE!')) + await client.delete_messages(msg.message.chat.id, msg.message.message_id) + + async def send_confirm(self, _client: Client, msg: CallbackQuery) -> None: + def make_msg_handle(additional_msg: str, callback_prefix: str): + async def wrapper(): + await self.confirm_dialog(msg, additional_msg, callback_prefix, msg.data.split()[-1]) + return wrapper + if msg.data.startswith('del'): + await msg.answer('Please press again to make sure. If you really want to delete this reply', True) + await self.bot.send_message( self.help_group, - 'Do you want to delete reply message to {}?'.format(build_html_parse.parse_user(msg.data.split()[-2])), + 'Do you want to delete reply message to {}?'.format(TextParser.parse_user(msg.data.split()[-2])), 'markdown', - reply_markup = self.generate_confirm_keyboard('del', msg.data[4:]) + reply_markup=self.generate_confirm_keyboard('del', msg.data[4:]) ) - elif msg.data.startswith('bypass'): - self.confirm_dialog(msg, 'set bypass for', 'bypass', int(msg.data.split()[-1])) - elif msg.data.startswith('renew'): - self.confirm_dialog(msg, 'reset user status', 'renew', int(msg.data.split()[-1])) - elif msg.data.startswith('setpass'): - self.confirm_dialog(msg, 'set pass', 'setpass', int(msg.data.split()[-1])) - elif msg.data.startswith('unlimited'): - self.confirm_dialog(msg, 'set unlimited retries for', 'unlimited', int(msg.data.split()[-1])) - msg.answer() + COMMAND_MAPPING = { + 'close': make_msg_handle(_T('close this ticket'), 'close'), + 'block': make_msg_handle(_T('block this user'), 'block'), + 'send': make_msg_handle(_T('send the link to'), 'send'), + 'reset': make_msg_handle(_T('reset retry times for'), 'reset'), + 'bypass': make_msg_handle(_T('set bypass for'), 'bypass'), + 'renew': make_msg_handle(_T('reset user status'), 'renew'), + 'setpass': make_msg_handle(_T('set pass'), 'setpass'), + 'unlimited': make_msg_handle(_T('set unlimited retries for'), 'unlimited') + } + for name, func in COMMAND_MAPPING.items(): + if msg.data.startswith(name): + await func() + break - def answer(self, client: Client, msg: CallbackQuery): - msg.data = msg.data.decode(errors = 'ignore') + async def answer(self, client: Client, msg: CallbackQuery) -> None: if msg.data.startswith('cancel'): - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - msg.answer('Canceled') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + await msg.answer('Canceled') elif msg.data.startswith('unban'): - self.mysqldb.execute("UPDATE `tickets_user` SET `baned` = 0 WHERE `user_id` = {}".format(msg.data.split()[-1])) - msg.answer('UNBANED') - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + await self.mysqldb.execute("UPDATE `tickets_user` SET `baned` = 0 WHERE `user_id` = %s", + msg.data.split()[-1]) + await msg.answer('UNBANED') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) elif msg.data.startswith('refresh'): try: - client.edit_message_text( + await client.edit_message_text( msg.message.chat.id, msg.message.message_id, - self.generate_superuser_text(msg.data.split()[-1]), - 'markdown', - reply_markup = msg.message.reply_markup + await self.generate_superuser_text(msg.data.split()[-1]), + 'html', + reply_markup=msg.message.reply_markup ) - except api.errors.exceptions.bad_request_400.MessageNotModified: + except pyrogram.errors.exceptions.bad_request_400.MessageNotModified: pass - msg.answer('refreshed') + await msg.answer() elif 'confirm' in msg.data: try: - self.confirm(client, msg) + await self.confirm(client, msg) except TimeoutError: - msg.answer('Confirmation time out') - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - elif any(msg.data.startswith(x) for x in ('close', 'block', 'send', 'bypass', 'reset', 'unlimited', 'del', 'renew', 'setpass')): - self.send_confirm(client, msg) + await msg.answer('Confirmation time out') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + elif any(msg.data.startswith(x) for x in + ('close', 'block', 'send', 'bypass', 'reset', 'unlimited', 'del', 'renew', 'setpass')): + await self.send_confirm(client, msg) else: try: raise ValueError(msg.data) except: - client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview = True) + await client.send_message(self.help_group, traceback.format_exc(), disable_web_page_preview=True) - def flood_check(self, client: Client, msg: Message): - sq = self.mysqldb.query1("SELECT `last_time`, `last_msg_sent`, `baned` FROM `tickets_user` WHERE `user_id` = {}".format(msg.chat.id)) - if sq and (datetime.now() - sq['last_time']).total_seconds() < 120: - if msg.text: - print('Caught flood {}: {}'.format(msg.chat.id, msg.text)) - self.mysqldb.execute("UPDATE `tickets_user` SET `last_msg_sent` = CURRENT_TIMESTAMP() WHERE `user_id` = {}".format(msg.chat.id)) - self.mysqldb.commit() - if sq['baned']: - return msg.reply('Due to privacy settings, you are temporarily unable to operate.') is not None - msg.reply("You are driving too fast. Please try again later.") - return True - return False + async def _query_last_time(self, msg: Message) -> int: + return await self._query_redis_time(f'CSLAST_{msg.chat.id}') + + async def _query_last_msg_send(self, msg: Message) -> int: + return await self._query_redis_time(f'CSLASTMSG_{msg.chat.id}') + async def _query_redis_time(self, key: str) -> int: + r = await self._redis.get(key) + return 0 if r is None else int(r.decode()) -def main(): - custom_service_bot_class('config.ini', None, None).active() + async def _update_redis_time(self, key: str) -> None: + await self._redis.set(key, str(int(time.time()))) + await self._redis.expire(key, 180) -if __name__ == "__main__": - main() \ No newline at end of file + async def _update_last_time(self, msg: Message) -> None: + await self._update_redis_time(f'CSLAST_{msg.chat.id}') + + async def _update_last_msg_send(self, msg: Message) -> None: + await self._update_redis_time(f'CSLASTMSG_{msg.chat.id}') + + async def flood_check(self, _client: Client, msg: Message) -> bool: + r = await self._query_last_time(msg) + if time.time() - r < 120: + if msg.text: + logger.warning('Caught flood %s: %s', msg.chat.id, msg.text) + await self._update_last_msg_send(msg) + sq = await self.mysqldb.query1("SELECT `baned` FROM `tickets_user` WHERE `user_id` = %s", msg.chat.id) + if sq and sq['baned']: + return await msg.reply( + _T('Due to privacy settings, you are temporarily unable to operate.')) is not None + await msg.reply(_T('You are driving too fast. Please try again later.')) + return True + return False diff --git a/main.py b/main.py deleted file mode 100644 index a460c2f..0000000 --- a/main.py +++ /dev/null @@ -1,744 +0,0 @@ -# -*- coding: utf-8 -*- -# main.py -# Copyright (C) 2018-2019 github.com/googlehosts Group:Z -# -# This module is part of googlehosts/telegram-repeater and is released under -# the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -from configparser import ConfigParser -import hashlib -import os -import pymysql.cursors -from pyrogram import Client, Filters, ChatAction, api, MessageEntity, Message, PhotoSize, Photo, \ - Video, Animation, Document, InlineKeyboardButton, InlineKeyboardMarkup, CallbackQuery, User, Sticker, \ - ReplyKeyboardMarkup, KeyboardButton, ForceReply, CallbackQueryHandler, MessageHandler, CallbackQuery -import queue -import re -import io -from threading import Thread, Lock -import time -import traceback -import base64, sys, json -from tg_tools import build_html_parse as bhp -import tg_tools -from customservice import custom_service_bot_class, join_group_verify_class - -global bot_username -config = ConfigParser() -config.read('config.ini') - -class auth_system_class(object): - def __init__(self): - self.authed_user = eval(config['fuduji']['auth_user']) - self.ignore_user = eval(config['fuduji']['ignore_user']) - self.whitelist = eval(config['fuduji']['whitelist']) if config.has_option('fuduji', 'whitelist') else [] - self.user_suffix = eval(config['user']['custom_suffix']) - self.user_config = eval(config['user']['settings']) if config.has_option('user', 'settings') else {} - def check_ex(self, user_id: int): - return user_id in self.authed_user or user_id == int(config['account']['owner']) - def add_user(self, user_id: int): - self.authed_user.append(int(user_id)) - self.authed_user = list(set(self.authed_user)) - config['fuduji']['auth_user'] = repr(self.authed_user) - def del_user(self, user_id: int): - self.authed_user.remove(user_id) - config['fuduji']['auth_user'] = repr(self.authed_user) - def check_muted(self, user_id: int): - return user_id in self.ignore_user - def mute_user(self, user_id: int): - self.ignore_user.append(user_id) - self.ignore_user = list(set(self.ignore_user)) - config['fuduji']['ignore_user'] = repr(self.ignore_user) - def unmute_user(self, user_id: int): - try: - del self.ignore_user[self.ignore_user.index(user_id)] - config['fuduji']['ignore_user'] = repr(self.ignore_user) - with open('config.ini') as fout: config.write(fout) - except: pass - def check(self, user_id: int): - return self.check_ex(user_id) and not self.check_muted(user_id) - def check_full(self, user_id: int): - return self.check_ex(user_id) or user_id in self.whitelist - def set_suffix(self, user_id: int, suffix: str): - self.user_suffix[user_id] = suffix - config['user']['custom_suffix'] = repr(self.user_suffix) - def get_suffix(self, user_id: int): - return self.user_suffix.get(user_id, '') if self.user_config.get(user_id, {}).get('suffix', False) else '' - def mute_or_unmute(self, r: str, chat_id: int): - if not self.check_ex(chat_id): return - try: (self.mute_user if r == 'off' else self.unmute_user)(chat_id) - except ValueError: - pass - -auth_system = auth_system_class() - -class build_html_parse(bhp): - def __init__(self, msg: Message): - self._msg = self.gen_msg(msg) - self.parsed_msg = self.parse_main() - if msg.chat.id == int(config['fuduji']['fudu_group']) and self.parsed_msg and self.parsed_msg.startswith('\\//'): self.parsed_msg = self.parsed_msg[1:] - if msg.chat.id == int(config['fuduji']['target_group']) and self.parsed_msg: self.parsed_msg = self.parsed_msg.replace('@{}'.format(bot_username), '@{}'.format(config['fuduji']['replace_to_id'])) - -class media_path(object): - def __init__(self, path: str): - self.path = path - -class mysqldb(tg_tools.mysqldb): - def __init__(self, host: str, user: str, password: str, db: str, emerg_send_message: callable, charset: str = 'utf8'): - tg_tools.mysqldb.__init__(self, host, user, password, db, charset) - self.emerg_send_message = emerg_send_message - def insert_ex(self, id1: int, id2: int, user_id: int = 0): - self.execute('INSERT INTO `msg_id` (`msg_id`, `target_id`, `timestamp`, `user_id`) VALUES ({}, {}, CURRENT_TIMESTAMP(), {})'.format(id1, id2, user_id)) - self.commit() - def insert(self, msg: Message, msg_2: Message): - try: - self.insert_ex(msg.message_id, msg_2.message_id, msg.from_user.id) - self.commit() - except: - traceback.print_exc() - self.insert_ex(msg.message_id, msg_2.message_id) - - def get_user_id(self, msg: Message or int): - return self.query1('SELECT `user_id` FROM `msg_id` WHERE `msg_id` = (SELECT `msg_id` WHERE `target_id` = {})'.format(msg.reply_to_message.message_id if isinstance(msg, Message) else msg)) - - def get_id(self, msg_id: int, reverse: bool = False): - r = self.query1('{} = {}'.format('SELECT `{}` FROM `msg_id` WHERE `{}`'.format(*(('target_id', 'msg_id') if not reverse else ('msg_id', 'target_id'))), msg_id)) - return r['target_id' if not reverse else 'msg_id'] if r else None - - def get_reply_id(self, msg: Message): - return self.get_id(msg.reply_to_message.message_id) if msg.reply_to_message else None - - def get_reply_id_Reverse(self, msg: Message): - return self.get_id(msg.reply_to_message.message_id, True) if msg.reply_to_message else None - -class mediaSender(Thread): - Locker = Lock() - def __init__(self, send_message: callable, conn: mysqldb): - Thread.__init__(self, daemon = True) - self.queue = queue.Queue() - self.send_message = send_message - self.conn = conn - self.start() - def put(self, iterable: tuple, check_mute: bool = False): - if check_mute and auth_system.check_muted(iterable[1].from_user.id): return - self.queue.put_nowait(iterable) - @staticmethod - def sticker_sender(func: callable, chat_id: int, file_id: str, reply_to_message_id: int): - return func(chat_id, file_id, reply_to_message_id = reply_to_message_id) - def sender(self, function: callable, msg: Message, file_id_class: PhotoSize or Video or Animation or Document or Sticker, reversed_: bool): - if not reversed_: time.sleep(2) - while True: - try: - try: - r = function( - int(config['fuduji']['fudu_group']) if not reversed_ else int(config['fuduji']['target_group']), - file_id_class.path if isinstance(file_id_class, media_path) else file_id_class.file_id, - build_html_parse(msg).call() if not reversed_ else build_html_parse(msg).split_offset(), - reply_to_message_id=self.conn.get_reply_id(msg), - parse_mode='html' - ) - except TypeError as e: - if 'got an unexpected keyword argument \'parse_mode\'' in e.args[0]: - r = self.sticker_sender( - function, - int(config['fuduji']['fudu_group']) if not reversed_ else int(config['fuduji']['target_group']), - file_id_class.file_id, - self.conn.get_reply_id(msg) - ) - else: raise e - finally: - if isinstance(file_id_class, io.BufferedReader): file_id_class.close() - if reversed_: - self.conn.insert_ex(r['message_id'], msg.message_id) - else: - self.conn.insert(msg, r) - break - except api.errors.exceptions.flood_420.FloodWait as e: - print('Pause {} seconds because flood 420 wait'.format(e.x)) - traceback.print_exc() - time.sleep(e.x) - except: - traceback.print_exc() - break - finally: - self.Locker.acquire(False) - self.Locker.release() - def run(self): - while True: - function, msg, file_id_class, reversed_ = self.queue.get() - Thread(target=self.sender, args=(function, msg, file_id_class, reversed_), daemon=True).start() - -def extern_load_problem_set(): - try: - with open('problem_set.json', encoding='utf8') as fin: - problem_set = json.load(fin) - if len(problem_set['problem_set']) == 0: - print('Problem set length is 0') - except: - traceback.print_exc() - print('Error in reading problem set', file=sys.stderr) - problem_set = {} - return problem_set - -class sleep_to_delete(Thread): - def __init__(self, client: Client, chat_id: int, message_ids: int): - Thread.__init__(self, daemon = True) - self.client = client - self.chat_id = chat_id - self.message_ids = message_ids - self.start() - def run(self): - time.sleep(5) - self.client.delete_messages(self.chat_id, self.message_ids) - -class OperationTimeoutError(Exception): pass - -class OperatorError(Exception): pass - -class bot_controller(object): - - def __init__(self): - self.problems_load() - self.target_group = int(config['fuduji']['target_group']) - self.fudu_group = int(config['fuduji']['fudu_group']) - - self.bot_id = int(config['account']['api_key'].split(':')[0]) - self.emerg_contact = eval(config['account']['emerg_contact']) \ - if config.has_option('account', 'emerg_contact') and config['account']['emerg_contact'] != '' else \ - int(config['account']['owner']) - self.app = Client( - session_name = 'session', - api_id = config['account']['api_id'], - api_hash = config['account']['api_hash'], - app_version = 'repeater' - ) - self.botapp = Client( - session_name = config['account']['api_key'], - api_id = config['account']['api_id'], - api_hash = config['account']['api_hash'] - ) - - self.conn = mysqldb(config['database']['host'], config['database']['user'], config['database']['passwd'], config['database']['db_name'], self.emerg_contact) - self.media_sender = mediaSender(self.app.send_message, self.conn) - self.join_group_verify = join_group_verify_class(self.conn, self.botapp, self.target_group, extern_load_problem_set) - self.revoke_tracker_thread = self.join_group_verify.get_revoke_tracker_thread() - self.custom_service = custom_service_bot_class(config, self.conn, self.revoke_tracker_thread.send_link) - self.db_keepAlive = Thread(target = self.conn.keep_alive, daemon = True) - self.db_keepAlive.start() - - def init(self): - global bot_username - bot_username = self.botapp.get_me().username - - def problems_load(self): - self.problem_set = extern_load_problem_set() - - def idle(self): - return self.app.idle() - - def start(self): - self.app.add_handler(MessageHandler(self.handle_edit, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.edited)) - self.app.add_handler(MessageHandler(self.handle_new_member, Filters.chat(self.target_group) & Filters.new_chat_members)) - self.app.add_handler(MessageHandler(self.handle_document, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.document)) - self.app.add_handler(MessageHandler(self.handle_photo, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.photo)) - self.app.add_handler(MessageHandler(self.handle_sticker, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.sticker)) - self.app.add_handler(MessageHandler(self.handle_gif, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.animation)) - self.app.add_handler(MessageHandler(self.handle_video, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.video)) - self.app.add_handler(MessageHandler(self.handle_speak, Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & Filters.text)) - self.app.add_handler(MessageHandler(self.handle_incoming, Filters.incoming & Filters.chat(self.fudu_group))) - self.botapp.add_handler(CallbackQueryHandler(self.handle_callback)) - self.join_group_verify.init() - self.app.start() - self.botapp.start() - self.init() - self.custom_service.start() - - def stop(self): - self.revoke_tracker_thread.set_stop() - self.revoke_tracker_thread.join(1.5) - if self.revoke_tracker_thread.is_alive(): - print('[WARN] revoke_tracker_thread still running!') - self.custom_service.stop() - self.botapp.stop() - self.app.stop() - - def emerg_send_message(self, msg_str: str): - ''' - Send message to emergancy contacts. - ''' - if isinstance(self.emerg_contact, int): - self.app.send_message(self.emerg_contact, msg_str, 'html') - else: - for user_id in self.emerg_contact: - self.app.send_message(user_id, msg_str, 'html') - - def process_imcoming_command(self, client: Client, msg: Message): - r = re.match(r'^\/bot (on|off)$', msg.text) - if r is None: r = re.match(r'^\/b?(on|off)$', msg.text) - if r: - if not auth_system.check_ex(msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id): return - auth_system.mute_or_unmute(r.group(1), msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id) - client.delete_messages(msg.chat.id, msg.message_id) - if msg.text == '/status': - user_id = msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id - status = [str(user_id), ' summary:\n\n', 'A' if auth_system.check_ex(user_id) else 'Una' ,'uthorized user\nBot status: ', '✅' if not auth_system.check_muted(user_id) else '❌'] - sleep_to_delete(client, msg.chat.id, (msg.message_id, msg.reply(''.join(status), True).message_id)) - del status - elif msg.text.startswith('/p'): - if msg.text.startswith('/promote'): - if len(msg.text.split()) == 1: - if msg.reply_to_message is None or not auth_system.check_ex(msg.reply_to_message.from_user.id): - self.botapp.send_message(msg.chat.id, 'Please reply to an Authorized user.', reply_to_message_id = msg.message_id) - return - user_id = msg.reply_to_message.from_user.id - else: - user_id = int(msg.text.split()[1]) - self.botapp.send_message(msg.chat.id, 'Please use bottom to make sure you want to add {} to Administrators'.format(build_html_parse.parse_user(user_id)), - parse_mode = 'markdown', - reply_to_message_id = msg.message_id, - reply_markup = InlineKeyboardMarkup(inline_keyboard = [ - [ - InlineKeyboardButton(text = 'Yes, confirm', callback_data = 'promote {}'.format(user_id).encode()) - ], - [ - InlineKeyboardButton(text = 'Cancel', callback_data = b'cancel d') - ] - ])) - else: - if not auth_system.check_ex(msg.from_user.id): return - self.botapp.promote_chat_member(self.target_group, int(msg.from_user.id), True, can_delete_messages = True, can_restrict_members = True, can_invite_users = True, can_pin_messages = True, can_promote_members = True) - self.botapp.send_message(msg.chat.id, '[Emergency]: Privileges has been promoted', reply_to_message_id = msg.message_id) - return - if msg.reply_to_message: - if msg.text == '/del': - try: - client.forward_messages(msg.chat.id, self.target_group, self.conn.get_reply_id_Reverse(msg)) - self.botapp.delete_messages(self.target_group, self.conn.get_reply_id_Reverse(msg)) - except: client.send_message(msg.chat.id, traceback.format_exc(), disable_web_page_preview=True) - try: - client.delete_messages(int(config['fuduji']['fudu_group']), [msg.message_id, msg.reply_to_message.message_id]) - except: pass - elif msg.text == '/getid': - user_id = self.conn.get_user_id(msg) - client.send_message(msg.chat.id, 'user_id is `{}`'.format(user_id['user_id'] if user_id is not None and user_id['user_id'] != 0 else 'ERROR_INVALID_USER_ID'), parse_mode='markdown', reply_to_message_id=msg.reply_to_message.message_id) - elif msg.text == '/get' and self.conn.get_reply_id_Reverse(msg): - try: - client.forward_messages(int(config['fuduji']['fudu_group']), self.target_group, self.conn.get_reply_id_Reverse(msg)) - except: - client.send_message(msg.chat.id, traceback.format_exc().splitlines()[-1]) - elif msg.text == '/getn': - pass - elif msg.text == '/fw': - self.conn.insert_ex(self.botapp.forward_messages(self.target_group, self.target_group, self.conn.get_reply_id_Reverse(msg)).message_id, msg.message_id) - elif msg.text.startswith('/ban'): - user_id = self.conn.get_user_id(msg) - if len(msg.text) == 4: - restrict_time = 0 - else: - r = re.match(r'^([1-9]\d*)(s|m|h|d)$', msg.text[5:]) - if r is not None: - restrict_time = int(r.group(1)) * {'s': 1, 'm': 60, 'h': 60 * 60, 'd': 60 * 60 * 24}.get(r.group(2)) - else: - self.botapp.send_message(msg.chat.id, 'Usage: `/ban` or `/ban `', reply_to_message_id = msg.message_id, parse_mode = 'markdown') - if user_id is not None and user_id['user_id'] != 0: - if user_id['user_id'] not in auth_system.whitelist: - self.botapp.send_message( - msg.chat.id, - 'What can {} only do? Press the button below.\nThis confirmation message will expire after 20 seconds.'.format( - build_html_parse.parse_user(user_id['user_id']) - ), - reply_to_message_id = msg.message_id, - parse_mode = 'markdown', - reply_markup = InlineKeyboardMarkup( - inline_keyboard = [ - [ - InlineKeyboardButton(text = 'READ', callback_data = 'res {} read {}'.format(restrict_time, user_id['user_id']).encode()) - ], - [ - InlineKeyboardButton(text = 'SEND_MESSAGES', callback_data = 'res {} write {}'.format(restrict_time, user_id['user_id']).encode()), - InlineKeyboardButton(text = 'SEND_MEDIA', callback_data = 'res {} media {}'.format(restrict_time, user_id['user_id']).encode()) - ], - [ - InlineKeyboardButton(text = 'SEND_STICKERS', callback_data = 'res {} stickers {}'.format(restrict_time, user_id['user_id']).encode()), - InlineKeyboardButton(text = 'EMBED_LINKS', callback_data = 'res {} link {}'.format(restrict_time, user_id['user_id']).encode()) - ], - [ - InlineKeyboardButton(text = 'Cancel', callback_data = b'cancel') - ] - ] - ) - ) - else: - self.botapp.send_message(msg.chat.id, 'ERROR_WHITELIST_USER_ID', reply_to_message_id=msg.message_id) - else: - self.botapp.send_message(msg.chat.id, 'ERROR_INVALID_USER_ID', reply_to_message_id=msg.message_id) - elif msg.text == '/kick': - user_id = self.conn.get_user_id(msg) - if user_id is not None and user_id['user_id'] != 0: - if user_id['user_id'] not in auth_system.whitelist: - self.botapp.send_message(msg.chat.id, 'Do you really want to kick {}?\nIf you really want to kick this user, press the button below.\nThis confirmation message will expire after 15 seconds.'.format( - build_html_parse.parse_user(user_id['user_id']) - ), - reply_to_message_id = msg.message_id, - parse_mode='markdown', - reply_markup=InlineKeyboardMarkup( - inline_keyboard = [ - [ - InlineKeyboardButton(text='Yes, kick it', callback_data = b' '.join((b'kick', str(msg.from_user.id).encode(), str(user_id['user_id']).encode()))) - ], - [ - InlineKeyboardButton(text = 'No', callback_data = b'cancel') - ], - ] - ) - ) - else: - self.botapp.send_message(msg.chat.id, 'ERROR_WHITELIST_USER_ID', reply_to_message_id=msg.message_id) - else: - self.botapp.send_message(msg.chat.id, 'ERROR_INVALID_USER_ID', reply_to_message_id = msg.message_id) - else: # Not reply message - if msg.text == '/ban': - client.send_message(msg.chat.id, 'Reply to the user you wish to restrict, if you want to kick this user, please use the /kick command.') - elif msg.text == '/join': - pass - elif msg.text.startswith('/set'): - auth_system.user_suffix[msg.from_user.id] = msg.text.split()[-1] - client.send_message(msg.chat.id, 'Set suffix to `{}`'.format(msg.text.split()[-1]), 'markdown', reply_to_message_id = msg.message_id) - - def func_auth_process(self, client: Client, msg: Message): - if not auth_system.check_ex(msg.from_user.id): - msg.reply('Permission denied') - return - if msg.reply_to_message.from_user: - if auth_system.check_ex(msg.reply_to_message.from_user.id): - msg.reply('Authorized') - else: - self.botapp.send_message( - msg.chat.id, - 'Do you want to authorize {} ?\nThis confirmation message will expire after 20 seconds.'.format( - build_html_parse.parse_user(msg.reply_to_message.from_user.id) - ), - reply_to_message_id = msg.message_id, - parse_mode = 'markdown', - reply_markup = InlineKeyboardMarkup( - inline_keyboard = [ - [ - InlineKeyboardButton(text = 'Yes', callback_data = 'auth {} add'.format(msg.reply_to_message.from_user.id).encode()), - InlineKeyboardButton(text = 'No', callback_data = b'cancel') - ] - ] - ) - ) - else: client.send_message(msg.chat.id, 'Unexpected error.', reply_to_message_id = msg.message_id) - - def cross_group_forward_request(self, msg: Message): - kb = [ - [InlineKeyboardButton(text = 'Yes, I know what I\'m doing.', callback_data = b'fwd original')], - [InlineKeyboardButton(text = 'Yes, but don\'t use forward.', callback_data = b'fwd text')], - [InlineKeyboardButton(text = 'No, please don\'t.', callback_data = b'cancel d')] - ] - if msg.text is None: kb.pop(1) - self.botapp.send_message( - msg.chat.id, - 'Warning: You are requesting forwarding an authorized user\'s message to the main group, please comfirm your action.', - 'html', - reply_to_message_id = msg.message_id, - reply_markup = InlineKeyboardMarkup(inline_keyboard = kb) - ) - del kb - - def handle_new_member(self, client: Client, msg: Message): - for new_user_id in (x.id for x in msg.new_chat_members): - # Exam check goes here - try: - if not self.join_group_verify.query_user_passed(new_user_id): - self.botapp.kick_chat_member(self.target_group, new_user_id) - self.botapp.send_message(self.fudu_group, 'Kicked challenge failure user {}'.format(build_html_parse.parse_user(new_user_id)), 'markdown', - ) - except: - traceback.print_exc() - self.conn.insert( - msg, - client.send_message( - self.fudu_group, - '`{}` invite `{}` joined the group'.format( - build_html_parse.user_name(msg.from_user).full_name, - '`,`'.join( - build_html_parse.user_name(user).full_name for user in msg.new_chat_members - ) - ), - 'markdown' - ) \ - if msg.new_chat_members[0].id != msg.from_user.id else \ - client.send_message( - self.fudu_group, - '`{}` joined the group'.format( - '`,`'.join( - build_html_parse.user_name(user).full_name for user in msg.new_chat_members - ) - ), - 'markdown' - ) - ) - - def handle_edit(self, client: Client, msg: Message): - if msg.via_bot and msg.via_bot.id == 166035794: return - if self.conn.get_id(msg.message_id) is None: - time.sleep(3) - if self.conn.get_id(msg.message_id) is None: - print(msg) - return print('Editing Failure: get_id return None') - try: - (client.edit_message_text if msg.text else client.edit_message_caption)(self.fudu_group, self.conn.get_id(msg.message_id), build_html_parse(msg).call(), 'html') - except: - traceback.print_exc() - - def handle_document(self, client: Client, msg: Message): - self.media_sender.put((client.send_document, msg, msg.document, False)) - - def handle_photo(self, client: Client, msg: Message): - self.media_sender.put((client.send_photo, msg, msg.photo.sizes[0], False)) - - def handle_sticker(self, client: Client, msg: Message): - self.conn.insert( - msg, - client.send_message( - self.fudu_group, - '{} {} sticker'.format( - build_html_parse(msg).call(), - msg.sticker.emoji - ), - 'html', - True, - reply_to_message_id = self.conn.get_reply_id(msg), - ) - ) - - def handle_gif(self, client: Client, msg: Message): - self.media_sender.put((client.send_animation, msg, msg.animation, False)) - - def handle_video(self, client: Client, msg: Message): - self.media_sender.put((client.send_video, msg, msg.video, False)) - - def handle_speak(self, client: Client, msg: Message): - if msg.text.startswith('/') and re.match(r'^\/\w+(@\w*)?$', msg.text): return - self.conn.insert( - msg, - client.send_message( - self.fudu_group, - build_html_parse(msg).call(), - 'html', - reply_to_message_id = self.conn.get_reply_id(msg), - disable_web_page_preview = True - ) - ) - - def handle_incoming(self, client: Client, msg: Message): - client.send(api.functions.channels.ReadHistory(client.resolve_peer(msg.chat.id), msg.message_id)) - if msg.text == '/auth' and msg.reply_to_message: - return self.func_auth_process(client, msg) - if not auth_system.check_ex(msg.from_user.id): return - if msg.text and re.match( - r'^\/(bot (on|off)|del|getn?|fw|ban( (([1-9]\d*)(s|m|h|d)|f))?|kick( confirm| -?\d+)?|status|b?o(n|ff)|join|p(romote( \d+)?)?|set [a-zA-Z])$', - msg.text - ): - return self.process_imcoming_command(client, msg) - if msg.text and msg.text.startswith('/') and re.match(r'^\/\w+(@\w*)?$', msg.text): return - if auth_system.check_muted(msg.from_user.id) or (msg.text and msg.text.startswith('//')) or (msg.caption and msg.caption.startswith('//')): return - - if msg.forward_from or msg.forward_from_chat: - if msg.forward_from: - if msg.forward_from.is_self: return - elif auth_system.check_ex(msg.forward_from.id): - return self.cross_group_forward_request(msg) - self.conn.insert_ex(self.botapp.forward_messages(self.target_group, self.fudu_group, msg.message_id).message_id, msg.message_id) - elif msg.text and (not msg.edit_date or (msg.edit_date and self.conn.get_id(msg.message_id, True) is None)): - self.conn.insert_ex( - self.botapp.send_message( - self.target_group, - build_html_parse(msg).split_offset(), - 'html', - True, - reply_to_message_id=self.conn.get_reply_id_Reverse(msg), - ).message_id, msg.message_id - ) - elif msg.photo: - self.media_sender.Locker.acquire() - msg.download('tmp.jpg') - self.media_sender.put((self.botapp.send_photo, msg, media_path('downloads/tmp.jpg'), True), True) - elif msg.video: - self.media_sender.put((self.botapp.send_video, msg, msg.video, True), True) - elif msg.document: - self.media_sender.put((self.botapp.send_document, msg, msg.document, True), True) - elif msg.edit_date: - try: - (self.botapp.edit_message_text if msg.text else self.botapp.edit_message_caption)( - self.target_group, - self.conn.get_id(msg.message_id, True), - build_html_parse(msg).split_offset(), - parse_mode='html', - disable_web_page_preview=True - ) - except: traceback.print_exc() - elif msg.sticker: - self.media_sender.put((self.botapp.send_sticker, msg, msg.sticker, True), True) - - def handle_callback(self, client: Client, msg: CallbackQuery): - msg.data = msg.data.decode(errors = 'ignore') - try: - if msg.data.startswith('cancel') or msg.data == 'rm': - msg.answer(msg.id, 'Canceled' if not msg.data == 'rm' else 'Button removed') - if msg.data.endswith('d'): - client.delete_messages(msg.message.chat.id, msg.message.message_id) - else: - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - if self.join_group_verify is not None and self.join_group_verify.click_to_join(client, msg): - return - if msg.data.startswith('res'): - if time.time() - msg.message.date > 20: - raise OperationTimeoutError() - _, dur, _type, _user_id = msg.data.split() - if client.restrict_chat_member( - self.target_group, - int(_user_id), - int(time.time()) + int(dur), - **( - { - 'write': {'can_send_messages': True}, - 'media': {'can_send_media_messages': True}, - 'stickers': {'can_send_other_messages': True}, - 'link': {'can_add_web_page_previews': True}, - 'read': {} - }.get(_type) - ) - ): - msg.answer('The user is restricted successfully.') - client.edit_message_text(msg.message.chat.id, - msg.message.message_id, - 'Restrictions applied to {} Duration: {}'.format(build_html_parse.parse_user(_user_id), '{}s'.format(dur) if int(dur) else 'Forever'), - parse_mode = 'markdown', - reply_markup = InlineKeyboardMarkup([ - [InlineKeyboardButton(text = 'UNBAN', callback_data = 'unban {}'.format(_user_id).encode())] - ] - ) - ) - - elif msg.data.startswith('unban'): - if client.restrict_chat_member(self.target_group, int(msg.data.split()[-1]), 0, True, True, True, True): - msg.answer('Unban successfully') - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - elif msg.data.startswith('auth'): - if time.time() - msg.message.date > 20: - raise OperationTimeoutError() - auth_system.add_user(msg.data.split()[1]) - msg.answer('{} added to the authorized group'.format(msg.data.split()[1])) - client.edit_message_text(msg.message.chat.id, msg.message.message_id, '{} added to the authorized group'.format(msg.data.split()[1])) - with open('config.ini', 'w') as fout: config.write(fout) - elif msg.data.startswith('fwd'): - if time.time() - msg.message.date > 30: - raise OperationTimeoutError() - if 'original' in msg.data: - self.conn.insert_ex(client.forward_messages(self.target_group, msg.message.chat.id, msg.message.reply_to_message.message_id).message_id, msg.message.reply_to_message.message_id) - else: - self.conn.insert_ex(client.send_message(self.target_group, build_html_parse(msg.message.reply_to_message).split_offset(), 'html').message_id, msg.message.reply_to_message.message_id) - msg.answer('Forward successfully') - client.delete_messages(msg.message.chat.id, msg.message.message_id) - elif msg.data.startswith('kick'): - if not msg.data.startswith('kickc') and msg.from_user.id != int(msg.data.split()[-2]): - raise OperatorError() - if 'true' not in msg.data: - if not msg.data.startswith('kickc') and time.time() - msg.message.date > 15: - raise OperationTimeoutError() - args = [ - msg.message.chat.id, - msg.message.message_id, - 'Press the button again to kick {}\nThis confirmation message will expire after 10 seconds.'.format( - build_html_parse.parse_user(msg.data.split()[-1]) - ), - ] - if msg.data.startswith('kickc'): - args.pop(1) - r = msg.data.split() - r.insert(1, msg.from_user.id) - msg.data = ' '.join(str(x) for x in r) - del r - kwargs = { - 'parse_mode': 'markdown', - 'reply_markup': InlineKeyboardMarkup(inline_keyboard=[ - [InlineKeyboardButton(text = 'Yes, please.', callback_data = b' '.join((b'kick true', ' '.join(msg.data.split()[1:]).encode())))], - [InlineKeyboardButton(text = 'Cancel', callback_data = b'cancel')] - ]) - } - (client.send_message if msg.data.startswith('kickc') else client.edit_message_text)(*args, **kwargs) - msg.answer('Please press again to make sure. Do you really want to kick {} ?'.format(msg.data.split()[-1]), True) - else: - if msg.message.edit_date: - if time.time() - msg.message.edit_date > 10: - raise OperationTimeoutError() - else: - if time.time() - msg.message.date > 10: - raise OperationTimeoutError() - client.kick_chat_member(self.target_group, int(msg.data.split()[-1])) - msg.answer('Kicked {}'.format(msg.data.split()[-1])) - client.edit_message_text(msg.message.chat.id, msg.message.message_id, 'Kicked {}'.format(build_html_parse.parse_user(msg.data.split()[-1]))) - #app.send_message(self.fudu_group, 'Kicked {}'.format(msg.message.entities[0].user.id)) - #client.delete_messages(msg.message.chat.id, msg.message.message_id) - elif msg.data.startswith('promote'): - if not msg.data.endswith('undo'): - if time.time() - msg.message.date > 10: - raise OperationTimeoutError() - self.botapp.promote_chat_member(self.target_group, int(msg.data.split()[1]), True, can_delete_messages = True, can_restrict_members = True, can_invite_users = True, can_pin_messages = True, can_promote_members = True) - msg.answer('Promote successfully') - client.edit_message_text(msg.message.chat.id, msg.message.message_id, 'Promoted {}'.format(build_html_parse.parse_user(int(msg.data.split()[1]))), parse_mode = 'markdown', - reply_markup = InlineKeyboardMarkup(inline_keyboard = [ - [InlineKeyboardButton(text = 'UNDO', callback_data = ' '.join((msg.data, 'undo')).encode())], - [InlineKeyboardButton(text = 'remove button', callback_data = b'rm')] - ]) - ) - else: - self.botapp.promote_chat_member(self.target_group, int(msg.data.split()[1]), False, can_delete_messages = False, can_invite_users = False, can_restrict_members = False) - msg.answer('Undo Promote successfully') - client.edit_message_text(msg.message.chat.id, msg.message.message_id, 'Unpromoted {}'.format(build_html_parse.parse_user(int(msg.data.split()[1]))), parse_mode = 'markdown') - except OperationTimeoutError: - msg.answer('Confirmation time out') - client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) - except OperatorError: - msg.answer('The operator should be {}.'.format(msg.data.split()[-2]), True) - except: - self.app.send_message(int(config['fuduji']['help_group']), traceback.format_exc().splitlines()[-1]) - traceback.print_exc() - -def main(): - bot = bot_controller() - bot.start() - bot.idle() - -def import_from_csv(): - import csv - with open(sys.argv[2], encoding = 'utf8') as fin: - s = csv.reader(fin, delimiter = ',') - problems = [] - for row in s: - problems.append({'Q': row[0], 'A': row[1]}) - problem_set = extern_load_problem_set() - problem_set['problem_set'] = problems - with open('problem_set.json', 'w', encoding='utf8') as fout: - json.dump(problem_set, fout, indent='\t', separators=(',', ': '), ensure_ascii=False) - -if __name__ == '__main__': - if len(sys.argv) == 3 and sys.argv[1] == 'import': - import_from_csv() - else: - main() \ No newline at end of file diff --git a/repeater.py b/repeater.py new file mode 100644 index 0000000..ea33ba3 --- /dev/null +++ b/repeater.py @@ -0,0 +1,984 @@ +# -*- coding: utf-8 -*- +# repeater.py +# Copyright (C) 2018-2020 github.com/googlehosts Group:Z +# +# This module is part of googlehosts/telegram-repeater and is released under +# the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import asyncio +import gettext +import json +import logging +import re +import time +import traceback +from configparser import ConfigParser +from typing import Mapping, Optional, Tuple, TypeVar, Union + +import aiofile +import aioredis +import pyrogram.errors +import redis +from pyrogram import (CallbackQuery, CallbackQueryHandler, ChatPermissions, + Client, Filters, InlineKeyboardButton, + InlineKeyboardMarkup, Message, MessageHandler, api) + +import utils +from customservice import CustomServiceBot, JoinGroupVerify +from utils import AuthSystem, MySQLdb +from utils import TextParser as tp +from utils import _rT, get_language + +config = ConfigParser() +config.read('config.ini') + +logger = logging.getLogger('repeater') + +translation = gettext.translation('repeater', 'translations/', + languages=[get_language()], fallback=True) + +_T = translation.gettext +_cT = TypeVar('_cT') + + +class TextParser(tp): + bot_username = '' + + def __init__(self, msg: Message): + self._msg = self.BuildMessage(msg) + self.parsed_msg = self.parse_main() + if msg.chat.id == config.getint('fuduji', 'fudu_group') and self.parsed_msg and self.parsed_msg.startswith( + '\\//'): self.parsed_msg = self.parsed_msg[1:] + if msg.chat.id == config.getint('fuduji', + 'target_group') and self.parsed_msg: self.parsed_msg = self.parsed_msg.replace( + '@{}'.format(TextParser.bot_username), '@{}'.format(config['fuduji']['replace_to_id'])) + + +async def external_load_problem_set() -> Mapping[str, _rT]: + try: + async with aiofile.AIOFile('problem_set.json', encoding='utf8') as fin: + problem_set = json.loads(await fin.read()) + if len(problem_set['problems']['problem_set']) == 0: + logger.warning('Problem set length is 0') + except: + traceback.print_exc() + logger.error('Error in reading problem set') + problem_set = {} + return problem_set + + +class WaitForDelete: + def __init__(self, client: Client, chat_id: int, message_ids: Union[int, Tuple[int, ...]]): + self.client: Client = client + self.chat_id: int = chat_id + self.message_ids: Union[int, Tuple[int, ...]] = message_ids + + async def __call__(self) -> None: + await asyncio.sleep(5) + await self.client.delete_messages(self.chat_id, self.message_ids) + + +class OperationTimeoutError(Exception): pass + + +class OperatorError(Exception): pass + + +class BotController: + class ByPassVerify(UserWarning): + pass + + def __init__(self): + self.target_group: int = config.getint('fuduji', 'target_group') + self.fudu_group: int = config.getint('fuduji', 'fudu_group') + self.bot_id: int = int(config['account']['api_key'].split(':')[0]) + self.app: Client = Client( + session_name='session', + api_id=config['account']['api_id'], + api_hash=config['account']['api_hash'], + app_version='repeater' + ) + self.botapp: Client = Client( + session_name='beyondbot', + api_id=config['account']['api_id'], + api_hash=config['account']['api_hash'], + bot_token=config['account']['api_key'], + ) + self.conn: Optional[MySQLdb] = None + self._redis: redis.Redis = None + self.auth_system: Optional[AuthSystem] = None + self.warn_evidence_history_channel: int = config.getint('fuduji', 'warn_evidence', fallback=0) + + self.join_group_verify_enable: bool = config.getboolean('join_group_verify', 'enable', fallback=True) + self.custom_service_enable: bool = config.getboolean('custom_service', 'enable', fallback=True) + + self.join_group_verify: Optional[JoinGroupVerify] = None + self.revoke_tracker_coro: Optional[utils.InviteLinkTracker] = None + self.custom_service: Optional[CustomServiceBot] = None + self.problem_set: Optional[Mapping[str, _rT]] = None + self.init_handle() + + async def init_connections(self) -> None: + self._redis = await aioredis.create_redis_pool('redis://localhost') + self.conn = await MySQLdb.create(config['database']['host'], config['database']['user'], + config['database']['passwd'], config['database']['db_name']) + self.auth_system = await AuthSystem.initialize_instance(self.conn, config.getint('account', 'owner')) + if self.join_group_verify_enable: + self.join_group_verify = await JoinGroupVerify.create(self.conn, self.botapp, self.target_group, + self.fudu_group, external_load_problem_set, + self._redis) + self.join_group_verify.init() + self.revoke_tracker_coro = self.join_group_verify.revoke_tracker_coro + if self.custom_service_enable: + self.custom_service = CustomServiceBot(config, self.conn, self.join_group_verify.send_link, self._redis) + + @classmethod + async def create(cls) -> 'BotController': + self = BotController() + await self.init_connections() + return self + + def init_handle(self) -> None: + self.app.add_handler(MessageHandler(self.handle_edit, Filters.chat(self.target_group) & ~Filters.user( + self.bot_id) & Filters.edited)) + self.app.add_handler( + MessageHandler(self.handle_new_member, Filters.chat(self.target_group) & Filters.new_chat_members)) + self.app.add_handler( + MessageHandler(self.handle_service_messages, Filters.chat(self.target_group) & Filters.service)) + self.app.add_handler(MessageHandler(self.handle_all_media, + Filters.chat(self.target_group) & ~Filters.user(self.bot_id) & ( + Filters.photo | Filters.video | Filters.document | Filters.animation | Filters.voice))) + self.app.add_handler(MessageHandler(self.handle_sticker, Filters.chat(self.target_group) & ~Filters.user( + self.bot_id) & Filters.sticker)) + self.app.add_handler(MessageHandler(self.handle_speak, Filters.chat(self.target_group) & ~Filters.user( + self.bot_id) & Filters.text)) + self.app.add_handler(MessageHandler(self.handle_incoming, Filters.incoming & Filters.chat(self.fudu_group))) + self.botapp.add_handler( + MessageHandler(self.handle_bot_send_media, Filters.chat(self.fudu_group) & Filters.command('SendMedia'))) + self.botapp.add_handler(CallbackQueryHandler(self.handle_callback)) + + async def init(self) -> None: + while not self.botapp.is_connected: + await asyncio.sleep(0) + TextParser.bot_username = (await self.botapp.get_me()).username + + async def idle(self) -> None: + try: + await self.app.idle() + except KeyboardInterrupt: + logger.info('Catched KeyboardInterrupt') + + async def start(self) -> None: + asyncio.run_coroutine_threadsafe(self.app.start(), asyncio.get_event_loop()) + asyncio.run_coroutine_threadsafe(self.botapp.start(), asyncio.get_event_loop()) + if self.custom_service_enable: + asyncio.run_coroutine_threadsafe(self.custom_service.start(), asyncio.get_event_loop()) + await self.init() + + async def stop(self) -> None: + task_pending = [] + if self.join_group_verify_enable: + self.revoke_tracker_coro.request_stop() + await self.revoke_tracker_coro.join(1.5) + if self.revoke_tracker_coro.is_alive: + logger.warning('revoke_tracker_coro still running!') + if self.custom_service_enable: + task_pending.append(asyncio.create_task(self.custom_service.stop())) + task_pending.append(asyncio.create_task(self.botapp.stop())) + task_pending.append(asyncio.create_task(self.app.stop())) + await asyncio.wait(task_pending) + task_pending.clear() + + if self.join_group_verify_enable: + await self.join_group_verify.problems.destroy() + + self._redis.close() + task_pending.append(asyncio.create_task(self.conn.close())) + task_pending.append(asyncio.create_task(self._redis.wait_closed())) + await asyncio.wait(task_pending) + + async def handle_service_messages(self, _client: Client, msg: Message) -> None: + if msg.pinned_message: + text = self.get_file_type(msg.pinned_message) + if text == 'text': + text = msg.pinned_message.text[:20] + else: + text = f'a {text}' + await self.conn.insert_ex( + (await self.botapp.send_message(self.fudu_group, f'Pined \'{text}\'', disable_web_page_preview=True, + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='UNPIN', callback_data='unpin')] + ]))).message_id, msg.message_id + ) + elif msg.new_chat_title: + await self.conn.insert_ex( + (await self.botapp.send_message(self.fudu_group, f'Set group title to {msg.new_chat_title}', + 'html', disable_web_page_preview=True)).message_id, + msg.message_id + ) + else: + logger.info('Got unexcept service message: %s', repr(msg)) + + async def generate_warn_message(self, user_id: int, reason: str) -> str: + return _T('You were warned.(Total: {})\nReason:
{}
').format( + await self.conn.query_warn_by_user(user_id), reason) + + async def process_imcoming_command(self, client: Client, msg: Message) -> None: + r = re.match(r'^/bot (on|off)$', msg.text) + if r is None: r = re.match(r'^/b?(on|off)$', msg.text) + if r: + if not self.auth_system.check_ex( + msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id): return + await self.auth_system.mute_or_unmute(r.group(1), + msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id) + await msg.delete() + + if msg.text == '/status': + user_id = msg.reply_to_message.from_user.id if msg.reply_to_message else msg.from_user.id + status = [str(user_id), ' summary:\n\n', 'A' if self.auth_system.check_ex(user_id) else 'Una', + 'uthorized user\nBot status: ', + CustomServiceBot.return_bool_emoji(not self.auth_system.check_muted(user_id))] + await WaitForDelete(client, msg.chat.id, + (msg.message_id, (await msg.reply(''.join(status), True)).message_id))() + del status + + elif msg.text.startswith('/promote'): + if len(msg.text.split()) == 1: + if msg.reply_to_message is None or not self.auth_system.check_ex(msg.reply_to_message.from_user.id): + await self.botapp.send_message(msg.chat.id, 'Please reply to an Authorized user.', + reply_to_message_id=msg.message_id) + return + user_id = msg.reply_to_message.from_user.id + else: + user_id = int(msg.text.split()[1]) + await self.botapp.send_message(msg.chat.id, + 'Please use bottom to make sure you want to add {} to Administrators'.format( + TextParser.parse_user(user_id)), + parse_mode='markdown', + reply_to_message_id=msg.message_id, + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [ + InlineKeyboardButton(text='Yes, confirm', + callback_data=f'promote {user_id}') + ], + [ + InlineKeyboardButton(text='Cancel', callback_data='cancel d') + ] + ])) + return + + elif msg.text.startswith('/su'): + if not self.auth_system.check_ex(msg.from_user.id): return + await self.botapp.promote_chat_member( + self.target_group, + int(msg.from_user.id), + True, + can_delete_messages=True, + can_pin_messages=True, + can_promote_members=True + ) + await self.botapp.send_message( + msg.chat.id, + 'Access Granted', + disable_notification=True, + reply_to_message_id=msg.message_id + ) + + elif msg.text.startswith('/title'): + if not self.auth_system.check_ex(msg.from_user.id): return + await self.botapp.set_chat_title( + self.target_group, + msg.text.split(maxsplit=2)[1] + ) + + if msg.reply_to_message: + if msg.text == '/del': + try: + await client.forward_messages(msg.chat.id, self.target_group, + await self.conn.get_reply_id_Reverse(msg)) + except: + await client.send_message(msg.chat.id, traceback.format_exc(), disable_web_page_preview=True) + try: + await self.botapp.delete_messages(self.target_group, await self.conn.get_reply_id_Reverse(msg)) + await client.delete_messages(self.fudu_group, [msg.message_id, msg.reply_to_message.message_id]) + except: + pass + + elif msg.text == '/getid': + user_id = await self.conn.get_user_id(msg) + await msg.reply( + 'user_id is `{}`'.format( + user_id['user_id'] if user_id is not None and user_id['user_id'] else \ + 'ERROR_INVALID_USER_ID' + ), + parse_mode='markdown' + ) + + elif msg.text == '/get' and await self.conn.get_reply_id_Reverse(msg): + try: + await client.forward_messages(self.fudu_group, self.target_group, + await self.conn.get_reply_id_Reverse(msg)) + except: + await client.send_message(msg.chat.id, traceback.format_exc().splitlines()[-1]) + + elif msg.text == '/getn': + r = await self.conn.get_msg_name_history_channel_msg_id(msg) + if r != 0: + await client.forward_messages(msg.chat.id, config.getint('username_tracker', 'user_name_history'), + r) + else: + await client.send_message(msg.chat.id, 'ERROR_CHANNEL_MESSAGE_NOT_FOUND', + reply_to_message_id=msg.message_id) + + elif msg.text == '/fw': + message_id = await self.conn.get_reply_id_Reverse(msg) + if message_id is None: + await msg.reply('ERROR_INVALID_MESSAGE_ID') + return + await self.conn.insert_ex( + (await self.botapp.forward_messages(self.target_group, self.target_group, message_id)).message_id, + msg.message_id) + + elif msg.text.startswith('/ban'): + user_id = await self.conn.get_user_id(msg) + if len(msg.text) == 4: + restrict_time = 0 + else: + r = re.match(r'^([1-9]\d*)([smhd])$', msg.text[5:]) + if r is not None: + restrict_time = int(r.group(1)) * {'s': 1, 'm': 60, 'h': 60 * 60, 'd': 60 * 60 * 24}.get( + r.group(2)) + else: + await self.botapp.send_message(msg.chat.id, 'Usage: `/ban` or `/ban `', + 'markdown', reply_to_message_id=msg.message_id) + return + if user_id is not None and user_id['user_id']: + if user_id['user_id'] not in self.auth_system.whitelist: + await self.botapp.send_message( + msg.chat.id, + 'What can {} only do? Press the button below.\nThis confirmation message will expire after 20 seconds.'.format( + TextParser.parse_user(user_id['user_id']) + ), + reply_to_message_id=msg.message_id, + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text='READ', + callback_data=f"res {restrict_time} read {user_id['user_id']}") + ], + [ + InlineKeyboardButton(text='SEND_MESSAGES', + callback_data=f"res {restrict_time} write {user_id['user_id']}"), + InlineKeyboardButton(text='SEND_MEDIA', + callback_data=f"res {restrict_time} media {user_id['user_id']}") + ], + [ + InlineKeyboardButton(text='SEND_STICKERS', + callback_data=f"res {restrict_time} stickers {user_id['user_id']}"), + InlineKeyboardButton(text='EMBED_LINKS', + callback_data=f"res {restrict_time} link {user_id['user_id']}") + ], + [ + InlineKeyboardButton(text='Cancel', callback_data='cancel') + ] + ] + ) + ) + else: + await self.botapp.send_message(msg.chat.id, 'ERROR_WHITELIST_USER_ID', + reply_to_message_id=msg.message_id) + else: + await self.botapp.send_message(msg.chat.id, 'ERROR_INVALID_USER_ID', + reply_to_message_id=msg.message_id) + + elif msg.text == '/kick': + user_id = await self.conn.get_user_id(msg) + if user_id is not None and user_id['user_id']: + if user_id['user_id'] not in self.auth_system.whitelist: + await self.botapp.send_message(msg.chat.id, + 'Do you really want to kick {}?\nIf you really want to kick this user, press the button below.\nThis confirmation message will expire after 15 seconds.'.format( + TextParser.parse_user(user_id['user_id']) + ), + reply_to_message_id=msg.message_id, + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text='Yes, kick it', + callback_data=f'kick {msg.from_user.id} {user_id["user_id"]}') + ], + [ + InlineKeyboardButton(text='No', + callback_data='cancel') + ], + ] + ) + ) + else: + await self.botapp.send_message(msg.chat.id, 'ERROR_WHITELIST_USER_ID', + reply_to_message_id=msg.message_id) + else: + await self.botapp.send_message(msg.chat.id, 'ERROR_INVALID_USER_ID', + reply_to_message_id=msg.message_id) + + elif msg.text.startswith('/pin'): + target_id = await self.conn.get_reply_id_Reverse(msg) + if target_id is None: + await msg.reply('ERROR_INVALID_MESSAGE_ID') + return + await self.botapp.pin_chat_message(self.target_group, target_id, not msg.text.endswith('a')) + + elif msg.text.startswith('/warn'): + user_id = await self.conn.get_user_id(msg) + if user_id is None or not user_id['user_id']: + return + user_id = user_id['user_id'] + target_id = await self.conn.get_reply_id_Reverse(msg) + reason = ' '.join(msg.text.split(' ')[1:]) + dry_run = msg.text.split()[0].endswith('d') + fwd_msg = None + if self.warn_evidence_history_channel != 0: + fwd_msg = (await self.app.forward_messages(self.warn_evidence_history_channel, self.target_group, + target_id, True)).message_id + if dry_run: + await self.botapp.send_message(self.fudu_group, await self.generate_warn_message(user_id, reason), + reply_to_message_id=msg.reply_to_message.message_id) + else: + warn_id = await self.conn.insert_new_warn(user_id, reason, fwd_msg) + warn_msg = await self.botapp.send_message(self.target_group, + await self.generate_warn_message(user_id, reason), + reply_to_message_id=target_id) + await self.botapp.send_message(self.fudu_group, _T('WARN SENT TO {}, Total warn {} time(s)').format( + TextParser.parse_user(user_id), await self.conn.query_warn_by_user(user_id)), + parse_mode='markdown', reply_to_message_id=msg.message_id, + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text=_T('RECALL'), + callback_data=f'warndel {warn_msg.message_id} {warn_id}')] + ])) + + else: # Not reply message + if msg.text == '/ban': + await client.send_message(msg.chat.id, _T( + 'Reply to the user you wish to restrict, if you want to kick this user, please use the /kick command.')) + + elif msg.text == '/join': + await self.botapp.send_message(msg.chat.id, 'Click button to join name history channel', + reply_to_message_id=msg.message_id, + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Click me', + url='https://t.me/joinchat/AAAAAFKmiao-ayZD0M7jrA')] + ])) + + elif msg.text.startswith('/grant'): + user_id = msg.text.split()[-1] + await self.botapp.send_message(msg.chat.id, + 'Do you want to grant user {}?'.format(TextParser.parse_user(user_id)), + disable_notification=True, + reply_to_message_id=msg.message_id, reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton('CHANGE INFO', f'grant {user_id} info'), + InlineKeyboardButton('PIN', f'grant {user_id} pin')], + [InlineKeyboardButton('RESTRICT', f'grant {user_id} restrict'), + InlineKeyboardButton('DELETE', f'grant {user_id} delete')], + [InlineKeyboardButton('confirm', f'grant {user_id} confirm'), + InlineKeyboardButton('[DEBUG]Clear', f'grant {user_id} clear')], + [InlineKeyboardButton('cancel', 'cancel')] + ])) + + async def func_auth_process(self, _client: Client, msg: Message) -> None: + if not self.auth_system.check_ex(msg.from_user.id): + await msg.reply('Permission denied') + return + if msg.reply_to_message.from_user: + if self.auth_system.check_ex(msg.reply_to_message.from_user.id): + await msg.reply('Authorized') + else: + await self.botapp.send_message( + msg.chat.id, + 'Do you want to authorize {} ?\nThis confirmation message will expire after 20 seconds.'.format( + TextParser.parse_user(msg.reply_to_message.from_user.id) + ), + reply_to_message_id=msg.message_id, + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text='Yes', callback_data='auth {} add'.format( + msg.reply_to_message.from_user.id)), + InlineKeyboardButton(text='No', callback_data='cancel') + ] + ] + ) + ) + else: + await msg.reply('Unexpected error.') + + async def cross_group_forward_request(self, msg: Message) -> None: + kb = [ + [InlineKeyboardButton(text='Yes, I know what I\'m doing.', callback_data='fwd original')], + [InlineKeyboardButton(text='Yes, but don\'t use forward.', callback_data='fwd text')], + [InlineKeyboardButton(text='No, please don\'t.', callback_data='cancel d')] + ] + if msg.text is None: kb.pop(1) + await self.botapp.send_message( + msg.chat.id, + 'Warning: You are requesting forwarding an authorized user\'s message to the main group, please comfirm your action.', + 'html', + reply_to_message_id=msg.message_id, + reply_markup=InlineKeyboardMarkup(inline_keyboard=kb) + ) + del kb + + async def handle_new_member(self, client: Client, msg: Message) -> None: + for new_user_id in (x.id for x in msg.new_chat_members): + # Exam check goes here + try: + if not await self.join_group_verify.query_user_passed(new_user_id): + if await self.conn.query1("SELECT * FROM `ingroup` WHERE `user_id` = %s", new_user_id) is not None: + await self.conn.execute("DELETE FROM `ingroup` WHERE `user_id` = %s", new_user_id) + raise BotController.ByPassVerify() + await self.botapp.kick_chat_member(self.target_group, new_user_id) + await self.botapp.send_message(self.fudu_group, 'Kicked challenge failure user {}'.format( + TextParser.parse_user(new_user_id)), 'markdown') + except BotController.ByPassVerify: + pass + except: + traceback.print_exc() + if await self.conn.query_user_in_banlist(new_user_id): + await self.botapp.kick_chat_member(msg.chat.id, new_user_id) + await self.conn.insert( + msg, + await client.send_message( + self.fudu_group, + '`{}` invite `{}` joined the group'.format( + TextParser.UserName(msg.from_user).full_name, + '`,`'.join( + TextParser.UserName(user).full_name for user in msg.new_chat_members + ) + ), + 'markdown' + ) \ + if msg.new_chat_members[0].id != msg.from_user.id else \ + await client.send_message( + self.fudu_group, + '`{}` joined the group'.format( + '`,`'.join( + TextParser.UserName(user).full_name for user in msg.new_chat_members + ) + ), + 'markdown' + ) + ) + + async def handle_edit(self, client: Client, msg: Message) -> None: + if msg.via_bot and msg.via_bot.id == 166035794: return + if await self.conn.get_id(msg.message_id) is None: + await asyncio.sleep(2) + if await self.conn.get_id(msg.message_id) is None: + return logger.error('Editing Failure: get_id return None') + try: + await (client.edit_message_text if msg.text else client.edit_message_caption)(self.fudu_group, + await self.conn.get_id( + msg.message_id), + TextParser(msg).get_full_message(), + 'html') + except: + traceback.print_exc() + + async def handle_sticker(self, client: Client, msg: Message) -> None: + await self.conn.insert( + msg, + await client.send_message( + self.fudu_group, + '{} {} sticker'.format( + TextParser(msg).get_full_message(), + msg.sticker.emoji + ), + 'html', + True, + True, + reply_to_message_id=await self.conn.get_reply_id(msg), + ) + ) + + async def _get_reply_id(self, msg: Message, reverse: bool = False) -> Optional[int]: + if msg.reply_to_message is None: + return None + return await self.conn.get_id(msg.reply_to_message.message_id, reverse) + + async def send_media(self, client: Client, msg: Message, send_to: int, captain: str, reverse: bool = False) -> None: + msg_type = self.get_file_type(msg) + while True: + try: + _msg = await client.send_cached_media( + send_to, + self.get_file_id(msg, msg_type), + self.get_file_ref(msg, msg_type), + captain, + 'html', + True, + await self._get_reply_id(msg, reverse) + ) + if reverse: + await self.conn.insert_ex(_msg.message_id, msg.caption.split()[1]) + else: + await self.conn.insert(msg, _msg) + break + + except pyrogram.errors.FloodWait as e: + logger.warning('Pause %d seconds because 420 flood wait', e.x) + time.sleep(e.x) + except: + traceback.print_exc() + break + + async def handle_all_media(self, client: Client, msg: Message) -> None: + await self.send_media(client, msg, self.fudu_group, TextParser(msg).get_full_message()) + + @staticmethod + def get_file_id(msg: Message, _type: str) -> str: + return getattr(msg, _type).file_id + + @staticmethod + def get_file_ref(msg: Message, _type: str) -> str: + return getattr(msg, _type).file_ref + + @staticmethod + def get_file_type(msg: Message) -> str: + return 'photo' if msg.photo else \ + 'video' if msg.video else \ + 'animation' if msg.animation else \ + 'sticker' if msg.sticker else \ + 'voice' if msg.voice else \ + 'document' if msg.document else \ + 'text' if msg.text else 'error' + + async def handle_speak(self, client: Client, msg: Message) -> None: + if msg.text.startswith('/') and re.match(r'^\/\w+(@\w*)?$', msg.text): return + await self.conn.insert( + msg, + await client.send_message( + self.fudu_group, + TextParser(msg).get_full_message(), + 'html', + not msg.web_page, + True, + reply_to_message_id=await self.conn.get_reply_id(msg) + ) + ) + + async def handle_bot_send_media(self, client: Client, msg: Message) -> None: + await self.send_media(client, msg, self.target_group, ' '.join(TextParser(msg).split_offset().split(' ')[2:]), + True) + + async def handle_incoming(self, client: Client, msg: Message) -> None: + await client.send( + api.functions.channels.ReadHistory(channel=await client.resolve_peer(msg.chat.id), max_id=msg.message_id)) + if msg.reply_to_message: + await client.send(api.functions.messages.ReadMentions(peer=await client.resolve_peer(msg.chat.id))) + if msg.text == '/auth' and msg.reply_to_message: + return await self.func_auth_process(client, msg) + + if not self.auth_system.check_ex(msg.from_user.id): return + if msg.text and re.match( + r'^\/(bot (on|off)|del|getn?|fw|ban( ([1-9]\d*)[smhd]|f)?|kick( confirm| -?\d+)?|status|b?o(n|ff)|join|promote( \d+)?|set [a-zA-Z]|pina?|su(do)?|title .*|warnd? .*|grant \d+|report)$', + msg.text + ): + return await self.process_imcoming_command(client, msg) + if msg.text and msg.text.startswith('/') and re.match(r'^\/\w+(@\w*)?$', msg.text): return + if self.auth_system.check_muted(msg.from_user.id) or (msg.text and msg.text.startswith('//')) or ( + msg.caption and msg.caption.startswith('//')): return + if msg.forward_from or msg.forward_from_chat or msg.forward_sender_name: + if msg.forward_from: + if msg.forward_from.is_self: + return + elif self.auth_system.check_ex(msg.forward_from.id): + return await self.cross_group_forward_request(msg) + await self.conn.insert_ex( + (await self.botapp.forward_messages(self.target_group, self.fudu_group, msg.message_id)).message_id, + msg.message_id) + + elif msg.text and ( + not msg.edit_date or (msg.edit_date and await self.conn.get_id(msg.message_id, True) is None)): + await self.conn.insert_ex( + (await self.botapp.send_message( + self.target_group, + TextParser(msg).split_offset(), + 'html', + not msg.web_page, + reply_to_message_id=await self.conn.get_reply_id_Reverse(msg), + )).message_id, msg.message_id + ) + + elif msg.photo or msg.video or msg.animation or msg.document: + _type = self.get_file_type(msg) + await (await client.send_cached_media( + msg.chat.id, + self.get_file_id(msg, _type), + self.get_file_ref(msg, _type), + f'/SendMedia {msg.message_id} {TextParser(msg).split_offset()}', + parse_mode='html', + disable_notification=True, + reply_to_message_id=msg.reply_to_message.message_id if msg.reply_to_message else None + )).delete() + + elif msg.edit_date: + try: + await (self.botapp.edit_message_text if msg.text else self.botapp.edit_message_caption)( + self.target_group, + await self.conn.get_id(msg.message_id, True), + TextParser(msg).split_offset(), + parse_mode='html', + disable_web_page_preview=not msg.web_page + ) + except: + traceback.print_exc() + + elif msg.sticker: + await self.conn.insert_ex( + (await self.botapp.send_sticker(self.target_group, msg.sticker.file_id, + reply_to_message_id=await self.conn.get_reply_id_Reverse( + msg))).message_id, + msg.message_id + ) + + async def handle_callback(self, client: Client, msg: CallbackQuery) -> None: + if msg.message.chat.id < 0 and msg.message.chat.id != self.fudu_group: return + args = msg.data.split() + try: + if msg.data.startswith('cancel') or msg.data == 'rm': + if msg.data.endswith('d'): + await msg.message.delete() + else: + await msg.edit_message_reply_markup() + + if self.join_group_verify_enable and \ + self.join_group_verify is not None and \ + await self.join_group_verify.click_to_join(client, msg): + return + + if msg.data.startswith('res'): + if time.time() - msg.message.date > 20: + raise OperationTimeoutError() + _, dur, _type, _user_id = args + if await client.restrict_chat_member( + self.target_group, + int(_user_id), + { + 'write': ChatPermissions(can_send_messages=True), + 'media': ChatPermissions(can_send_media_messages=True), + 'stickers': ChatPermissions(can_send_stickers=True), + 'link': ChatPermissions(can_add_web_page_previews=True), + 'read': ChatPermissions() + }.get(_type), + int(time.time()) + int(dur) + ): + await msg.answer('The user is restricted successfully.') + await client.edit_message_text(msg.message.chat.id, + msg.message.message_id, + 'Restrictions applied to {} Duration: {}'.format( + TextParser.parse_user(_user_id), + '{}s'.format(dur) if int(dur) else 'Forever'), + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup([ + [InlineKeyboardButton(text='UNBAN', + callback_data='unban {}'.format(_user_id))] + ] + ) + ) + + elif msg.data.startswith('unban'): + if await client.restrict_chat_member(self.target_group, int(args[-1]), ChatPermissions( + can_send_messages=True, + can_send_stickers=True, can_send_polls=True, can_add_web_page_previews=True, + can_send_media_messages=True, + can_pin_messages=True, can_invite_users=True, can_change_info=True + )): + await msg.answer('Unban successfully') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + + elif msg.data.startswith('auth'): + if time.time() - msg.message.date > 20: + raise OperationTimeoutError() + await self.auth_system.add_user(args[1]) + await msg.answer(f'{args[1]} added to the authorized group') + await msg.message.edit(f'{args[1]} added to the authorized group') + + elif msg.data.startswith('fwd'): + if time.time() - msg.message.date > 30: + raise OperationTimeoutError() + if 'original' in msg.data: + # Process original forward + await self.conn.insert_ex((await client.forward_messages(self.target_group, msg.message.chat.id, + msg.message.reply_to_message.message_id)).message_id, + msg.message.reply_to_message.message_id) + else: + await self.conn.insert_ex((await client.send_message(self.target_group, TextParser( + msg.message.reply_to_message).split_offset(), 'html')).message_id, + msg.message.reply_to_message.message_id) + await msg.answer('Forward successfully') + await msg.message.delete() + + elif msg.data.startswith('kick'): + if not msg.data.startswith('kickc') and msg.from_user.id != int(args[-2]): + raise OperatorError() + if 'true' not in msg.data: + if not msg.data.startswith('kickc') and time.time() - msg.message.date > 15: + raise OperationTimeoutError() + client_args = [ + msg.message.chat.id, + msg.message.message_id, + 'Press the button again to kick {}\nThis confirmation message will expire after 10 seconds.'.format( + TextParser.parse_user(args[-1]) + ), + ] + if msg.data.startswith('kickc'): + client_args.pop(1) + r = list(client_args) + r.insert(1, msg.from_user.id) + msg.data = ' '.join(map(str, r)) + del r + kwargs = { + 'parse_mode': 'markdown', + 'reply_markup': InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='Yes, please.', + callback_data=' '.join(('kick true', ' '.join(map(str, args[1:])))))], + [InlineKeyboardButton(text='Cancel', callback_data='cancel')] + ]) + } + await (client.send_message if msg.data.startswith('kickc') else client.edit_message_text)( + *client_args, **kwargs) + await msg.answer( + 'Please press again to make sure. Do you really want to kick {} ?'.format(args[-1]), True) + else: + if msg.message.edit_date: + if time.time() - msg.message.edit_date > 10: + raise OperationTimeoutError() + else: + if time.time() - msg.message.date > 10: + raise OperationTimeoutError() + await client.kick_chat_member(self.target_group, int(args[-1])) + await msg.answer('Kicked {}'.format(args[-1])) + await msg.message.edit('Kicked {}'.format(TextParser.parse_user(args[-1]))) + + elif msg.data.startswith('promote'): + if not msg.data.endswith('undo'): + if time.time() - msg.message.date > 10: + raise OperationTimeoutError() + await self.botapp.promote_chat_member( + self.target_group, int(args[1]), True, can_delete_messages=True, can_restrict_members=True, + can_invite_users=True, can_pin_messages=True, can_promote_members=True) + await msg.answer('Promote successfully') + await msg.message.edit('Promoted {}'.format(TextParser.parse_user(int(args[1]))), + parse_mode='markdown', + reply_markup=InlineKeyboardMarkup(inline_keyboard=[ + [InlineKeyboardButton(text='UNDO', + callback_data=' '.join((msg.data, 'undo')))], + [InlineKeyboardButton(text='remove button', callback_data='rm')] + ]) + ) + else: + await self.botapp.promote_chat_member(self.target_group, int(args[1]), False, + can_delete_messages=False, can_invite_users=False, + can_restrict_members=False) + await msg.answer('Undo Promote successfully') + await msg.message.edit('Undo promoted {}'.format(TextParser.parse_user(int(args[1]))), + parse_mode='markdown') + + elif msg.data.startswith('grant'): + _redis_key_str = f'promote_{msg.message.chat.id}_{args[1]}' + if args[2] == 'confirm': + select_privileges = await self._redis.get(_redis_key_str) + await self._redis.delete(_redis_key_str) + if select_privileges is None: + raise OperationTimeoutError() + grant_args = {} + for x in map(lambda x: x.strip(), select_privileges.decode().split(',')): + if x == 'info': + grant_args.update({'can_change_info': True}) + if x == 'delete': + grant_args.update({'can_delete_messages': True}) + if x == 'restrict': + grant_args.update({'can_restrict_members': True}) + if x == 'pin': + grant_args.update({'can_pin_messages': True}) + await self.botapp.promote_chat_member(self.target_group, int(args[1]), **grant_args) + await msg.message.edit('Undo grant privileges', reply_markup=InlineKeyboardMarkup( + [[InlineKeyboardButton('UNDO', f'grant {args[1]} undo')]])) + await msg.answer() + elif args[2] == 'undo': + await self.botapp.promote_chat_member(self.target_group, int(args[1]), False, + can_delete_messages=False, can_restrict_members=False) + await msg.message.edit_reply_markup() + await msg.answer() + elif args[2] == 'clear': + self._redis.delete(_redis_key_str) + await msg.answer() + else: + if time.time() - msg.message.date > 40: + raise OperationTimeoutError() + select_privileges = self._redis.get(_redis_key_str) + if select_privileges is None: + select_privileges = [args[2]] + self._redis.set(_redis_key_str, select_privileges[0]) + self._redis.expire(_redis_key_str, 60) + else: + select_privileges = list(map(lambda x: x.strip(), select_privileges.decode().split(','))) + if args[2] in select_privileges: + if len(select_privileges) == 1: + return await msg.answer('You should choose at least one privilege.', True) + select_privileges.remove(args[2]) + else: + select_privileges.append(args[2]) + await self._redis.set(_redis_key_str, ','.join(select_privileges)) + await msg.message.edit( + 'Do you want to grant user {}?\n\nSelect privileges:\n{}'.format(TextParser.parse_user(args[1]), + '\n'.join(select_privileges)), + reply_markup=msg.message.reply_markup) + + elif msg.data == 'unpin': + await self.botapp.unpin_chat_message(self.target_group) + await msg.message.edit_reply_markup() + await msg.answer() + + elif msg.data.startswith('warndel'): + await self.botapp.delete_messages(self.target_group, int(args[1])) + await self.conn.delete_warn_by_id(args[2]) + await msg.message.edit_reply_markup() + await msg.answer() + + except OperationTimeoutError: + await msg.answer('Confirmation time out') + await client.edit_message_reply_markup(msg.message.chat.id, msg.message.message_id) + except OperatorError: + await msg.answer('The operator should be {}.'.format(args[-2]), True) + except: + await self.app.send_message(config.getint('custom_service', 'help_group'), + traceback.format_exc().splitlines()[-1]) + traceback.print_exc() + + +async def main(): + logging.getLogger("pyrogram").setLevel(logging.WARNING) + logging.basicConfig(level=logging.DEBUG, + format='%(asctime)s - %(levelname)s - %(funcName)s - %(lineno)d - %(message)s') + bot = await BotController.create() + await bot.start() + await bot.idle() + await bot.stop() + + +if __name__ == '__main__': + asyncio.get_event_loop().run_until_complete(main()) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..3af31e0 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +pyrogram===0.17.1-async +aiomysql==0.0.20 +aioredis==1.3.1 +tgcrypto==1.2.0 +aiofile==1.5.2 \ No newline at end of file diff --git a/tg_tools.py b/tg_tools.py deleted file mode 100644 index 7312383..0000000 --- a/tg_tools.py +++ /dev/null @@ -1,253 +0,0 @@ -# -*- coding: utf-8 -*- -# tg_tools.py -# Copyright (C) 2018-2019 github.com/googlehosts Group:Z -# -# This module is part of googlehosts/telegram-repeater and is released under -# the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License for more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with this program. If not, see . -from pyrogram import Message, User, MessageEntity, InlineKeyboardMarkup, InlineKeyboardButton, Client -from queue import Queue -from threading import Thread, Event, Lock -import time -import configparser -import pymysql -import traceback - -class build_html_parse(object): - class gen_msg(object): - def __init__(self, msg: Message): - self.text = (msg.text if msg.text else msg.caption if msg.caption else '').encode('utf-16-le') - self.entities = msg.entities if msg.text else msg.caption_entities - self.user_name, self.user_id = build_html_parse.user_name(msg.from_user).get_name_id() - try: - self.forward_from = msg.forward_from_chat.title if msg.forward_from_chat else ('DELETED' if msg.forward_from.is_deleted else (msg.forward_from.first_name + (' {}'.format(msg.forward_from.last_name) if msg.forward_from.last_name else ''))) if msg.forward_from else '' - except TypeError: - print(msg) - self.forward_from = 'Error: unable to get the name of the account you wish to forward from' - self.forward_fom_id = msg.forward_from_chat.id if msg.forward_from_chat else msg.forward_from.id if msg.forward_from else None - class user_name(object): - def __init__(self, user: User): - self.first_name = user.first_name - self.last_name = user.last_name if user.last_name else '' - self.full_name = user.first_name if self.last_name == '' else ' '.join((self.first_name, self.last_name)) - self.id = user.id - self.user = user - def get_name_id(self): - return self.full_name, self.id - def __str__(self): - return self.full_name - _dict = { - 'italic': ('i', 'i'), - 'bold': ('b', 'b'), - 'code': ('code', 'code'), - 'pre': ('pre', 'pre'), - 'text_link': ('a href="{}"', 'a') - } - def __init__(self, msg: Message): - raise NotImplementedError('This function should be overrided') - self._msg = self.gen_msg(None) - self.parsed_msg = '' - @staticmethod - def escape(text: str): - return text - @staticmethod - def parse_tag(_entry: MessageEntity): - r = build_html_parse._dict[_entry.type] - return ''.join(('<', r[0].format(_entry.url), '>\\n')).split('\\n') - def _split_loc_func(self): - self._split_loc_ex = [(_entry.offset * 2, (_entry.length + _entry.offset) * 2, self.parse_tag(_entry)) for _entry in self._msg.entities if _entry.type in ('italic', 'bold', 'code', 'pre', 'text_link')] - self._split_loc = [item for loc in [[_split[0], _split[1]] for _split in self._split_loc_ex] for item in loc] - self._tag = [_split[2] for _split in self._split_loc_ex] - del self._split_loc_ex - def parse_main_ex(self): - if self._msg.entities is None: return self.escape(self._msg.text.decode('utf-16-le')) - self._split_loc_func() - if not len(self._split_loc): return self.escape(self._msg.text.decode('utf-16-le')) - self._split_loc.insert(0, 0) - self._split_loc.append(len(self._msg.text)) - msg_list = [] - for index in range(1, len(self._split_loc)): - self._msg.text[self._split_loc[index - 1]:self._split_loc[index]].decode('utf-16-le') - if (index + 1) % 2: - msg_list.append(''.join((self._tag[index // 2 - 1][0], self.escape(self._msg.text[self._split_loc[index - 1]:self._split_loc[index]].decode('utf-16-le')), self._tag[index // 2 - 1][1]))) - else: - msg_list.append(self.escape(self._msg.text[self._split_loc[index - 1]:self._split_loc[index]].decode('utf-16-le'))) - return ''.join(msg_list) - def parse_main(self): - return self.parse_main_ex() - def split_offset(self): - return self.parsed_msg - def call(self): - return ''.join(('', - self._msg.user_name[:30], - ' (↩️ {})'.format(self._msg.forward_from[:30]) if self._msg.forward_from != '' else '', - ': ', - self.parsed_msg - )).replace('Now loading', '') - @staticmethod - def parse_user(user_id: int, user_name: str or None = None): - if user_name is None: - user_name = str(user_id) - return ''.join(('[', user_name, '](tg://user?id=', str(user_id), ')')) - @staticmethod - def parse_user_ex(user_id: int, user_name: str or None = None): - if user_name is None: - user_name = str(user_id) - return ''.join(('', user_name, '')) - @staticmethod - def markdown_replace(name: str): - for x in ('['): - name = name.replace(x, ''.join(('\\', x))) - return name - -class mysqldb(object): - def __init__(self, host: str, user: str, password: str, db: str, charset: str = 'utf8'): - self.host = host - self.user = user - self.password = password - self.db = db - self.charset = charset - self.last_execute = time.time() - self.init_connection() - self.lock = Lock() - self.query_lock = Lock() - def init_connection(self): - self.mysql_connection = pymysql.connect(host = self.host, - user = self.user, - password = self.password, - db = self.db, - charset = self.charset, - cursorclass = pymysql.cursors.DictCursor - ) - self.cursor = self.mysql_connection.cursor() - def commit(self): - with self.lock: - self.cursor.close() - self.mysql_connection.commit() - self.cursor = self.mysql_connection.cursor() - def query1(self, sql: str, args: tuple = ()): - with self.query_lock: - self.execute(sql, args) - return self.cursor.fetchone() - def query3(self, sql: str, args: tuple = ()): - with self.query_lock: - self.execute(sql, args) - return self.cursor.fetchmany(3) - def execute(self, sql: str, args: tuple = (), *, exception: pymysql.Error or None = None): - with self.lock: - self.cursor.execute(sql, args) - self.last_execute = time.time() - def ping(self): - self.mysql_connection.ping() - def close(self): - with self.lock: - self.mysql_connection.commit() - self.cursor.close() - self.mysql_connection.close() - def keep_alive(self, interval: float = 300.0): - while True: - if time.time() - self.last_execute > interval: - self.ping() - self.last_execute = time.time() - time.sleep(interval - time.time() + self.last_execute + 5) - -class revoke_thread(Thread): - def __init__(self, queue: Queue, client: Client, generate_keyboard: callable): - Thread.__init__(self, daemon=True) - self.queue = queue - self.client = client - self.generate_keyboard = generate_keyboard - self.start() - def run(self): - while not self.queue.empty(): - self.client.edit_message_reply_markup(*self.queue.get_nowait(), reply_markup=self.generate_keyboard()) - del self.queue - -class invite_link_tracker(Thread): - class user_tracker(object): - def __init__(self, message_id: int, timestamp: float): - self.message_id = message_id - self.timestamp = timestamp - def __init__(self, client: Client, problem_set: dict, chat_id: int): - Thread.__init__(self, daemon = True) - self.client = client - self.chat_id = chat_id - self.user_dict = {} - self.revoke_time = problem_set['revoke_time'] + 10 - self.join_group_msg = problem_set['success_msg'] - self.tricket_msg = problem_set['ticket_bot']['text'] - self.last_revoke_time = 0.0 - self.current_link = '' - self.stop_event = Event() - self.start() - def do_revoke(self): - self.current_link = self.client.export_chat_invite_link(self.chat_id) - self.revoke_users() - self.last_revoke_time = time.time() - def revoke_users(self): - current_time = time.time() - pending_delete = [] - need_update_user = Queue() - for user_id, user_tracker_ in self.user_dict.items(): - if current_time - user_tracker_.timestamp > self.revoke_time: - pending_delete.append(user_id) - else: - need_update_user.put_nowait((user_id, user_tracker_.message_id)) - for user_id in pending_delete: - self.user_dict.pop(user_id, None) - if not need_update_user.empty(): - revoke_thread(need_update_user, self.client, self.generate_keyboard).join() - del pending_delete, need_update_user, current_time - def get(self): - return self.current_link - def set_stop(self): - self.stop_event.set() - def generate_keyboard(self): - return InlineKeyboardMarkup( - inline_keyboard = [ - [ - InlineKeyboardButton(text = 'Join group', url = self.current_link) - ], - ] - ) - def send_link(self, chat_id: int, from_ticket: bool = False): - self.user_dict.update( - { - chat_id: invite_link_tracker.user_tracker( - self.client.send_message( - chat_id, - self.join_group_msg if from_ticket else self.tricket_msg, - 'html', - reply_markup = self.generate_keyboard() - ).message_id, - time.time() - ) - } - ) - def run(self): - # Wait start: - while not self.client.is_started: - time.sleep(0.01) - # Do revoke first. (init process) - self.do_revoke() - while not self.stop_event.is_set(): - try: - if len(self.user_dict) > 0: - if time.time() - self.last_revoke_time > 30: - self.do_revoke() - except: - traceback.print_exc() - else: - self.stop_event.wait(1) \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..a4a6772 --- /dev/null +++ b/utils.py @@ -0,0 +1,513 @@ +# -*- coding: utf-8 -*- +# utils.py +# Copyright (C) 2018-2020 github.com/googlehosts Group:Z +# +# This module is part of googlehosts/telegram-repeater and is released under +# the AGPL v3 License: https://www.gnu.org/licenses/agpl-3.0.txt +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +import ast +import asyncio +import concurrent.futures +import logging +import random +import string +import time +import traceback +from configparser import ConfigParser +from dataclasses import dataclass +from typing import (Dict, List, Mapping, Optional, Sequence, SupportsBytes, + Tuple, TypeVar, Union) + +import aiomysql +from pyrogram import (Client, InlineKeyboardButton, InlineKeyboardMarkup, + Message, MessageEntity, User) + +logger = logging.getLogger(__name__) + +_rT = TypeVar('_rT', str, Optional[int], SupportsBytes) +_anyT = TypeVar('_anyT') +_kT = TypeVar('_kT', str, int) + + +class TextParser: + class BuildMessage: + def __init__(self, msg: Message): + self.text: bytes = (msg.text if msg.text else msg.caption if msg.caption else '').encode('utf-16-le') + self.chat_id: int = msg.chat.id + self.entities: List[MessageEntity] = msg.entities if msg.text else msg.caption_entities + self.user_name, self.user_id = TextParser.UserName(msg.from_user).get_name_id() + self.message_id: int = msg.message_id + try: + self.forward_from: str = msg.forward_from_chat.title if msg.forward_from_chat else \ + ('DELETED' if msg.forward_from.is_deleted else (msg.forward_from.first_name + (' {}'.format( + msg.forward_from.last_name) if msg.forward_from.last_name else ''))) if msg.forward_from else msg.forward_sender_name if msg.forward_sender_name else '' + except TypeError: + print(msg) + self.forward_from = 'Error: unable to get the name of the account you wish to forward from' + self.forward_fom_id: Optional[ + int] = msg.forward_from_chat.id if msg.forward_from_chat else msg.forward_from.id if msg.forward_from else None + + class UserName: + def __init__(self, user: User): + self.first_name: str = user.first_name + self.last_name: str = user.last_name if user.last_name else '' + self.full_name: str = user.first_name if self.last_name == '' else ' '.join( + (self.first_name, self.last_name)) + self.id: int = user.id + self.user: User = user + + def get_name_id(self) -> Tuple[str, int]: + return self.full_name, self.id + + def __str__(self) -> str: + return self.full_name + + _dict = { + 'italic': ('i', 'i'), + 'bold': ('b', 'b'), + 'code': ('code', 'code'), + 'pre': ('pre', 'pre'), + 'text_link': ('a href="{}"', 'a'), + 'strike': ('del', 'del'), + 'underline': ('u', 'u'), + 'text_mention': ('a href=tg://user?id={}', 'a') + } + + filter_keyword = tuple(key for key, _ in _dict.items()) + + def __init__(self): + self._msg: Message = None + self.parsed_msg: str = '' + + def parse_html_msg(self) -> str: + result = [] + tag_stack = [] + if self._msg.entities is None: + return self._msg.text.decode('utf-16-le') + start_pos = set(_entity.offset * 2 for _entity in self._msg.entities if _entity.type in self.filter_keyword) + if not len(start_pos): + return self._msg.text.decode('utf-16-le') + _close_tag_pos = -1 + _close_tag = '' + _last_cut = 0 + for _pos in range(len(self._msg.text) + 1): + while _close_tag_pos == _pos: + result.append(self._msg.text[_last_cut:_pos]) + _last_cut = _pos + result.append(f''.encode('utf-16-le')) + if not len(tag_stack): + break + _close_tag, _close_tag_pos = tag_stack.pop() + if _pos in start_pos: + result.append(self._msg.text[_last_cut:_pos]) + _last_cut = _pos + for _entity in self._msg.entities: + if _entity.offset * 2 == _pos: + format_value = _entity.url + if format_value is None and _entity.user: + format_value = _entity.user.id + result.append(f'<{self._dict[_entity["type"]][0]}>'.format(format_value).encode('utf-16-le')) + tag_stack.append((self._dict[_entity.type][1], (_entity.offset + _entity.length) * 2)) + if _close_tag_pos <= _pos: + _close_tag, _close_tag_pos = tag_stack.pop() + result.append(self._msg.text[_last_cut:]) + return b''.join(result).decode('utf-16-le') + + def parse_main(self) -> str: + return self.parse_html_msg() + + def split_offset(self) -> str: + return self.parsed_msg + + def get_full_message(self) -> str: + return ''.join(('', + self._msg.user_name[:30], + ' (\u21a9 {})'.format(self._msg.forward_from[:30]) if self._msg.forward_from != '' else '', + '', + ': ', + self.parsed_msg + )) + + @staticmethod + def parse_user(user_id: int, user_name: Optional[str] = None) -> str: + if user_name is None: + user_name = str(user_id) + return f'[{user_name}](tg://user?id={user_id})' + + @staticmethod + def parse_user_ex(user_id: int, user_name: Optional[str] = None) -> str: + if user_name is None: + user_name = str(user_id) + return f'{user_name}' + + @staticmethod + def markdown_replace(name: str) -> str: + for x in ('['): + name = name.replace(x, ''.join(('\\', x))) + return name + + +class MySQLdb: + + def __init__( + self, + host: str, + user: str, + password: str, + db: str, + charset: str = 'utf8mb4', + cursorclass: aiomysql.Cursor = aiomysql.DictCursor + ): + self.logger: logging.Logger = logging.getLogger(__name__) + self.logger.setLevel(logging.DEBUG) + self.host: str = host + self.user: str = user + self.password: str = password + self.db: str = db + self.charset: str = charset + self.cursorclass: aiomysql.Cursor = cursorclass + self.execute_lock: asyncio.Lock = asyncio.Lock() + self.mysql_connection = None + + async def create_connect(self) -> None: + self.mysql_connection = await aiomysql.create_pool( + host=self.host, + user=self.user, + password=self.password, + db=self.db, + charset=self.charset, + cursorclass=self.cursorclass, + ) + + @classmethod + async def create(cls, + host: str, + user: str, + password: str, + db: str, + charset: str = 'utf8mb4', + cursorclass: aiomysql.Cursor = aiomysql.DictCursor, + ) -> 'MySQLdb': + self = MySQLdb(host, user, password, db, charset, cursorclass) + await self.create_connect() + return self + + async def promise_query1(self, sql: str, args: Union[Sequence[_anyT], _anyT] = ()) -> Mapping[_kT, _rT]: + obj = await self.query1(sql, args) + if obj is None: + raise RuntimeError() + return obj + + async def query(self, sql: str, args: Union[Sequence[_anyT], _anyT] = ()) -> Tuple[Mapping[_kT, _rT], ...]: + async with self.mysql_connection.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute(sql, args) + return await cur.fetchall() + + async def query1(self, sql: str, args: Union[Sequence[_anyT], _anyT] = ()) -> Optional[Mapping[_kT, _rT]]: + async with self.mysql_connection.acquire() as conn: + async with conn.cursor() as cur: + await cur.execute(sql, args) + return await cur.fetchone() + + async def execute(self, sql: str, args: Union[Sequence[_anyT], Sequence[Sequence[_anyT]], _anyT] = (), + many: bool = False) -> None: + async with self.mysql_connection.acquire() as conn: + async with conn.cursor() as cur: + await (cur.executemany if many else cur.execute)(sql, args) + await conn.commit() + + async def close(self) -> None: + self.mysql_connection.close() + await self.mysql_connection.wait_closed() + + async def insert_ex(self, id1: int, id2: int, user_id: Optional[int] = None) -> None: + await self.execute( + 'INSERT INTO `msg_id` (`msg_id`, `target_id`, `timestamp`, `user_id`) VALUES (%s, %s, CURRENT_TIMESTAMP(), %s)', + (id1, id2, user_id)) + + async def insert(self, msg: Message, msg_2: Message) -> None: + try: + await self.insert_ex(msg.message_id, msg_2.message_id, msg.from_user.id) + except: + traceback.print_exc() + await self.insert_ex(msg.message_id, msg_2.message_id) + + async def get_user_id(self, msg: Union[Message, int]) -> Optional[Mapping[_kT, _rT]]: + return await self.query1( + 'SELECT `user_id` FROM `msg_id` WHERE `msg_id` = (SELECT `msg_id` FROM `msg_id` WHERE `target_id` = %s)', + (msg if isinstance(msg, int) else msg.reply_to_message.message_id)) + + async def get_id(self, msg_id: int, reverse: bool = False) -> Optional[int]: + r = await self.query1('{} = %s'.format('SELECT `{}` FROM `msg_id` WHERE `{}`'.format( + *(('target_id', 'msg_id') if not reverse else ('msg_id', 'target_id')))), msg_id) + return r['target_id' if not reverse else 'msg_id'] if r else None + + async def get_reply_id(self, msg: Message) -> Optional[int]: + return await self.get_id(msg.reply_to_message.message_id) if msg.reply_to_message else None + + async def get_reply_id_Reverse(self, msg: Message) -> Optional[int]: + return await self.get_id(msg.reply_to_message.message_id, True) if msg.reply_to_message else None + + async def get_msg_name_history_channel_msg_id(self, msg: Message) -> int: + return (await self.query1( + 'SELECT `channel_msg_id` FROM `username` WHERE `user_id` = (SELECT `user_id` FROM `msg_id` WHERE `target_id` = %s)', + msg.reply_to_message.message_id))['channel_msg_id'] + + async def insert_new_warn(self, user_id: int, msg: str, msg_id: Optional[int]) -> int: + await self.execute("INSERT INTO `reasons` (`user_id`, `text`, `msg_id`) VALUE (%s, %s, %s)", + (user_id, msg, msg_id)) + return (await self.query1("SELECT LAST_INSERT_ID()"))['LAST_INSERT_ID()'] + + async def delete_warn_by_id(self, warn_id: int) -> None: + await self.execute("DELETE FROM `reasons` WHERE `user_id` = %s", warn_id) + + async def query_warn_by_user(self, user_id: int) -> int: + return (await self.query1("SELECT COUNT(*) FROM `reasons` WHERE `user_id` = %s", user_id))['COUNT(*)'] + + async def query_warn_reason_by_id(self, reason_id: int) -> str: + return (await self.promise_query1("SELECT `text` FROM `reasons` WHERE `id` = %s", reason_id))['text'] + + async def query_user_in_banlist(self, user_id: int) -> bool: + return await self.query1("SELECT * FROM `banlist` WHERE `id` = %s", user_id) is not None + + +class InviteLinkTracker: + @dataclass + class _UserTracker: + message_id: int + timestamp: float + + def __init__(self, client: Client, problem_set: dict, chat_id: int): + self.client: Client = client + self.chat_id: int = chat_id + self.user_dict: Dict[int, InviteLinkTracker._UserTracker] = {} + self.revoke_time: int = problem_set['configs']['revoke_time'] + 10 + self.join_group_msg: str = problem_set['messages']['success_msg'] + self.tricket_msg: str = problem_set['messages']['join_group_message'] + self.last_revoke_time: float = 0.0 + self.current_link: str = '' + self.stop_event: asyncio.Event = asyncio.Event() + self.future: Optional[concurrent.futures.Future] = None + + def start(self) -> concurrent.futures.Future: + if self.future is not None: + return self.future + self.future = asyncio.run_coroutine_threadsafe(self._boost_run(), asyncio.get_event_loop()) + return self.future + + async def do_revoke(self) -> None: + self.current_link = await self.client.export_chat_invite_link(self.chat_id) + await self.revoke_users() + self.last_revoke_time = time.time() + + async def revoke_users(self) -> None: + current_time = time.time() + pending_delete = [] + need_update_user = asyncio.Queue() + for user_id, user_tracker in self.user_dict.items(): + if current_time - user_tracker.timestamp > self.revoke_time: + pending_delete.append(user_id) + else: + need_update_user.put_nowait((user_id, user_tracker.message_id)) + for user_id in pending_delete: + self.user_dict.pop(user_id, None) + while not need_update_user.empty(): + await self.client.edit_message_reply_markup(*need_update_user.get_nowait(), + reply_markup=self.generate_keyboard()) + del pending_delete, need_update_user, current_time + + def get(self) -> str: + return self.current_link + + async def join(self, timeout: float = 0) -> None: + if self.future is None: + return + if timeout > 0: + while not self.future.done(): + for _ in range(int(timeout // .05)): + if self.future.done(): + return + await asyncio.sleep(.05) + else: + await asyncio.sleep(0) + + @property + def is_alive(self) -> bool: + return self.future is not None and not self.future.done() + + def request_stop(self) -> None: + self.stop_event.set() + + def generate_keyboard(self) -> InlineKeyboardMarkup: + return InlineKeyboardMarkup( + inline_keyboard=[ + [ + InlineKeyboardButton(text='Join group', url=self.current_link) + ] + ] + ) + + async def send_link(self, chat_id: int, from_ticket: bool = False) -> None: + self.user_dict.update( + { + chat_id: InviteLinkTracker._UserTracker( + # NOTE: KNOWN ISSUE, IF NEVER CONTACT FROM THIS BOT + (await self.client.send_message( + chat_id, + self.join_group_msg if from_ticket else self.tricket_msg, + 'html', + reply_markup=self.generate_keyboard() + )).message_id, + time.time() + ) + } + ) + + async def _boost_run(self) -> None: + while not self.client.is_connected: + await asyncio.sleep(0) + await self.do_revoke() + while not self.stop_event.is_set(): + try: + if self.user_dict: + if time.time() - self.last_revoke_time > 30: + await self.do_revoke() + except: + traceback.print_exc() + else: + if not self.stop_event.is_set(): + await asyncio.sleep(1) + + +def get_random_string(length: int = 8) -> str: + return ''.join(random.choices(string.ascii_lowercase, k=length)) + + +class _AuthSystem: + + def __init__(self, conn: MySQLdb): + self.conn = conn + self.authed_user: List[int] = [] + self.non_ignore_user: List[int] = [] + self.whitelist: List[int] = [] + + async def init(self, owner: Optional[int] = None) -> None: + sqlObj = await self.conn.query("SELECT * FROM `auth_user`") + self.authed_user = [row['id'] for row in sqlObj if row['authorized'] == 'Y'] + self.non_ignore_user = [row['id'] for row in sqlObj if row['muted'] == 'N'] + self.whitelist = [row['id'] for row in sqlObj if row['whitelist'] == 'Y'] + if owner is not None and owner not in self.authed_user: + self.authed_user.append(owner) + + @classmethod + async def create(cls, conn: MySQLdb, owner: Optional[int] = None) -> '_AuthSystem': + self = _AuthSystem(conn) + await self.init(owner) + return self + + def check_ex(self, user_id: int) -> bool: + return user_id in self.authed_user + + async def add_user(self, user_id: int) -> None: + self.authed_user.append(user_id) + self.authed_user = list(set(self.authed_user)) + if self.query_user(user_id) is not None: + await self.update_user(user_id, 'authorized', 'Y') + else: + await self.conn.execute("INSERT INTO `auth_user` (`id`, `authorized`) VALUE (%s, 'Y')", user_id) + + async def update_user(self, user_id: int, column_name: str, value: str) -> None: + await self.conn.execute("UPDATE `auth_user` SET `{}` = %s WHERE `id` = %s".format(column_name), + (value, user_id)) + + async def query_user(self, user_id: int) -> Optional[Mapping[_kT, _rT]]: + return await self.conn.query1("SELECT * FROM `auth_user` WHERE `id` = %s", user_id) + + async def del_user(self, user_id: int) -> None: + self.authed_user.remove(user_id) + await self.update_user(user_id, 'authorized', 'N') + + def check_muted(self, user_id: int) -> bool: + return user_id not in self.non_ignore_user + + async def unmute_user(self, user_id: int): + self.non_ignore_user.append(user_id) + self.non_ignore_user = list(set(self.non_ignore_user)) + await self.update_user(user_id, 'muted', 'N') + + async def mute_user(self, user_id: int) -> None: + self.non_ignore_user.remove(user_id) + await self.update_user(user_id, 'muted', 'Y') + + def check(self, user_id: int) -> bool: + return self.check_ex(user_id) and not self.check_muted(user_id) + + def check_full(self, user_id: int) -> bool: + return self.check_ex(user_id) or user_id in self.whitelist + + async def mute_or_unmute(self, r: str, chat_id: int) -> None: + if not self.check_ex(chat_id): return + try: + await (self.mute_user if r == 'off' else self.unmute_user)(chat_id) + except ValueError: + pass + + +class AuthSystem(_AuthSystem): + class_self = None + + @staticmethod + def get_instance(): + if AuthSystem.class_self is None: + raise RuntimeError('Instance not initialize') + return AuthSystem.class_self + + @staticmethod + async def initialize_instance(conn: MySQLdb, owner: int = None) -> 'AuthSystem': + AuthSystem.class_self = await AuthSystem.create(conn, owner) + return AuthSystem.class_self + + @classmethod + async def create(cls, conn: MySQLdb, owner: Optional[int] = None) -> 'AuthSystem': + self = AuthSystem(conn) + await self.init(owner) + return self + + @staticmethod + async def config2mysqldb(config: ConfigParser, conn: MySQLdb) -> None: + await conn.execute("TRUNCATE TABLE `auth_user`") + authed_user = ast.literal_eval(config['fuduji']['auth_user']) + ignore_user = ast.literal_eval(config['fuduji']['ignore_user']) + whitelist = ast.literal_eval(config['fuduji']['whitelist']) if config.has_option('fuduji', 'whitelist') else [] + await conn.execute("INSERT INTO `auth_user` (`id`, `authorized`) VALUES (%s, 'Y')", ((x,) for x in authed_user), + True) + for x in ignore_user: + await conn.execute("UPDATE `auth_user` SET `muted` = 'Y' WHERE `id` = %s", x) + for x in whitelist: + if await conn.query1("SELECT * FROM `auth_user` WHERE `id` = %s", x) is not None: + await conn.execute("UPDATE `auth_user` SET `whitelist` = 'Y' WHERE `id` = %s", x) + else: + await conn.execute("INSERT INTO `auth_user` (`id`, `whitelist`) VALUE (%s, 'Y')", x) + + +def get_language() -> str: + config = ConfigParser() + config.read('config.ini') + return config.get('i18n', 'language', fallback='en_US')