diff --git a/.gitignore b/.gitignore index 8efc557..fb7add7 100644 --- a/.gitignore +++ b/.gitignore @@ -131,6 +131,7 @@ unknown_errors.txt pagermaid*.txt exception*.txt output.log +qrcode.png # Spyder project settings .spyderproject diff --git a/config.gen.yml b/config.gen.yml index 0aa826d..da069cd 100644 --- a/config.gen.yml +++ b/config.gen.yml @@ -10,6 +10,7 @@ # API Credentials of your telegram application created at https://my.telegram.org/apps api_id: "ID_HERE" api_hash: "HASH_HERE" +qrcode_login: "False" # Either debug logging is enabled or not debug: "False" diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py index ff4f3d7..5357fc8 100644 --- a/pagermaid/__init__.py +++ b/pagermaid/__init__.py @@ -12,7 +12,7 @@ from pagermaid.scheduler import scheduler import pyromod.listen from pyrogram import Client -pgm_version = "1.2.24" +pgm_version = "1.2.25" CMD_LIST = {} module_dir = __path__[0] working_dir = getcwd() diff --git a/pagermaid/__main__.py b/pagermaid/__main__.py index ca6218b..c846152 100644 --- a/pagermaid/__main__.py +++ b/pagermaid/__main__.py @@ -10,6 +10,7 @@ from pagermaid.hook import Hook from pagermaid.modules import module_list, plugin_list from pagermaid.single_utils import safe_remove from pagermaid.utils import lang, process_exit +from pyromod.methods.sign_in_qrcode import start_client path.insert(1, f"{working_dir}{sep}plugins") @@ -18,7 +19,7 @@ async def main(): logs.info(lang('platform') + platform + lang('platform_load')) try: - await bot.start() + await start_client(bot) except AuthKeyUnregistered: safe_remove("pagermaid.session") exit() diff --git a/pagermaid/config.py b/pagermaid/config.py index e29f852..5e59a0d 100644 --- a/pagermaid/config.py +++ b/pagermaid/config.py @@ -43,6 +43,7 @@ class Config: # TGX API_ID = DEFAULT_API_ID API_HASH = DEFAULT_API_HASH + QRCODE_LOGIN = strtobool(os.environ.get("QRCODE_LOGIN", config.get("qrcode_login", "false"))) STRING_SESSION = os.environ.get("STRING_SESSION") DEBUG = strtobool(os.environ.get("PGM_DEBUG", config["debug"])) ERROR_REPORT = strtobool(os.environ.get("PGM_ERROR_REPORT", config["error_report"]), True) diff --git a/pyromod/helpers/__init__.py b/pyromod/helpers/__init__.py deleted file mode 100644 index 03dda1a..0000000 --- a/pyromod/helpers/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -pyromod - A monkeypatcher add-on for Pyrogram -Copyright (C) 2020 Cezar H. - -This file is part of pyromod. - -pyromod is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyromod 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyromod. If not, see . -""" -from .helpers import ikb, bki, ntb, btn, kb, kbtn, array_chunk, force_reply diff --git a/pyromod/helpers/helpers.py b/pyromod/helpers/helpers.py deleted file mode 100644 index b9cc3a5..0000000 --- a/pyromod/helpers/helpers.py +++ /dev/null @@ -1,75 +0,0 @@ -from pyrogram.types import InlineKeyboardButton, InlineKeyboardMarkup, KeyboardButton, ReplyKeyboardMarkup, ForceReply - - -def ikb(rows=None): - if rows is None: - rows = [] - lines = [] - for row in rows: - line = [] - for button in row: - button = btn(*button) # InlineKeyboardButton - line.append(button) - lines.append(line) - return InlineKeyboardMarkup(inline_keyboard=lines) - # return {'inline_keyboard': lines} - - -def btn(text, value, type='callback_data'): - return InlineKeyboardButton(text, **{type: value}) - # return {'text': text, type: value} - - -# The inverse of above -def bki(keyboard): - lines = [] - for row in keyboard.inline_keyboard: - line = [] - for button in row: - button = ntb(button) # btn() format - line.append(button) - lines.append(line) - return lines - # return ikb() format - - -def ntb(button): - for btn_type in ['callback_data', 'url', 'switch_inline_query', 'switch_inline_query_current_chat', - 'callback_game']: - value = getattr(button, btn_type) - if value: - break - button = [button.text, value] - if btn_type != 'callback_data': - button.append(btn_type) - return button - # return {'text': text, type: value} - - -def kb(rows=None, **kwargs): - if rows is None: - rows = [] - lines = [] - for row in rows: - line = [] - for button in row: - button_type = type(button) - if button_type == str: - button = KeyboardButton(button) - elif button_type == dict: - button = KeyboardButton(**button) - - line.append(button) - lines.append(line) - return ReplyKeyboardMarkup(keyboard=lines, **kwargs) - - -kbtn = KeyboardButton - - -def force_reply(selective=True): - return ForceReply(selective=selective) - - -def array_chunk(input_, size): - return [input_[i:i + size] for i in range(0, len(input_), size)] diff --git a/pyromod/methods/__init__.py b/pyromod/methods/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyromod/methods/sign_in_qrcode.py b/pyromod/methods/sign_in_qrcode.py new file mode 100644 index 0000000..531eb05 --- /dev/null +++ b/pyromod/methods/sign_in_qrcode.py @@ -0,0 +1,127 @@ +import asyncio +import base64 +from typing import Optional + +import pyrogram +from pyqrcode import QRCode +from pyrogram import Client +from pyrogram.errors import SessionPasswordNeeded, BadRequest +from pyrogram.session import Auth, Session +from pyrogram.utils import ainput + +from pagermaid import Config + + +async def sign_in_qrcode( + client: Client, +) -> Optional[str]: + req = await client.invoke( + pyrogram.raw.functions.auth.ExportLoginToken( + api_id=client.api_id, + api_hash=client.api_hash, + except_ids=[], + ) + ) + + if isinstance(req, pyrogram.raw.types.auth.LoginToken): + token = base64.b64encode(req.token) + return f"tg://login?token={token.decode('utf-8')}" + elif isinstance(req, pyrogram.raw.types.auth.LoginTokenMigrateTo): + await client.session.stop() + await client.storage.dc_id(req.dc_id) + await client.storage.auth_key( + await Auth(client, await client.storage.dc_id(), await client.storage.test_mode()).create() + ) + client.session = Session( + client, await client.storage.dc_id(), + await client.storage.auth_key(), await client.storage.test_mode() + ) + await client.session.start() + req = await client.invoke(pyrogram.raw.functions.auth.ImportLoginToken(token=req.token)) + await client.storage.user_id(req.user.id) + await client.storage.is_bot(False) + return pyrogram.types.User._parse(client, req.user) + elif isinstance(req, pyrogram.raw.types.auth.LoginTokenSuccess): + await client.storage.user_id(req.authorization.user.id) + await client.storage.is_bot(False) + return pyrogram.types.User._parse(client, req.authorization.user) + + +async def authorize_by_qrcode( + client: Client, +): + print(f"Welcome to Pyrogram (version {pyrogram.__version__})") + print(f"Pyrogram is free software and comes with ABSOLUTELY NO WARRANTY. Licensed\n" + f"under the terms of the {pyrogram.__license__}.\n") + + while True: + qrcode = None + try: + qrcode = await sign_in_qrcode(client) + except BadRequest as e: + print(e.MESSAGE) + except SessionPasswordNeeded as e: + print(e.MESSAGE) + while True: + print(f"Password hint: {await client.get_password_hint()}") + + if not client.password: + client.password = await ainput("Enter password (empty to recover): ", hide=client.hide_password) + + try: + if client.password: + return await client.check_password(client.password) + confirm = await ainput("Confirm password recovery (y/n): ") + + if confirm == "y": + email_pattern = await client.send_recovery_code() + print(f"The recovery code has been sent to {email_pattern}") + + while True: + recovery_code = await ainput("Enter recovery code: ") + + try: + return await client.recover_password(recovery_code) + except BadRequest as e: + print(e.MESSAGE) + else: + client.password = None + except BadRequest as e: + print(e.MESSAGE) + client.password = None + if isinstance(qrcode, str): + qr_obj = QRCode(qrcode) + try: + qr_obj.png("qrcode.png", scale=6) + except Exception: + print("Save qrcode.png failed.") + print(qr_obj.terminal()) + print(f"Scan the QR code above, the qrcode.png file or visit {qrcode} to log in.\n") + print("QR code will expire in 20 seconds. If you have scanned it, please wait...") + await asyncio.sleep(20) + elif isinstance(qrcode, pyrogram.types.User): + return qrcode + + +async def start_client(client: Client): + is_authorized = await client.connect() + + try: + if not is_authorized: + if Config.QRCODE_LOGIN: + await authorize_by_qrcode(client) + else: + await client.authorize() + + if not await client.storage.is_bot() and client.takeout: + client.takeout_id = (await client.invoke(pyrogram.raw.functions.account.InitTakeoutSession())).id + + await client.invoke(pyrogram.raw.functions.updates.GetState()) + except (Exception, KeyboardInterrupt): + await client.disconnect() + raise + else: + client.me = await client.get_me() + await client.initialize() + + return client diff --git a/pyromod/nav/__init__.py b/pyromod/nav/__init__.py deleted file mode 100644 index 2d451b8..0000000 --- a/pyromod/nav/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" -pyromod - A monkeypatcher add-on for Pyrogram -Copyright (C) 2020 Cezar H. - -This file is part of pyromod. - -pyromod is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyromod 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyromod. If not, see . -""" -from .pagination import Pagination diff --git a/pyromod/nav/pagination.py b/pyromod/nav/pagination.py deleted file mode 100644 index f113e7e..0000000 --- a/pyromod/nav/pagination.py +++ /dev/null @@ -1,91 +0,0 @@ -""" -pyromod - A monkeypatcher add-on for Pyrogram -Copyright (C) 2020 Cezar H. - -This file is part of pyromod. - -pyromod is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -pyromod 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 General Public License for more details. - -You should have received a copy of the GNU General Public License -along with pyromod. If not, see . -""" -import math -from ..helpers import array_chunk - - -class Pagination: - def __init__(self, objects, page_data=None, item_data=None, item_title=None): - default_page_callback = (lambda x: str(x)) - default_item_callback = (lambda i, pg: f'[{pg}] {i}') - self.objects = objects - self.page_data = page_data or default_page_callback - self.item_data = item_data or default_item_callback - self.item_title = item_title or default_item_callback - - def create(self, page, lines=5, columns=1): - quant_per_page = lines * columns - page = 1 if page <= 0 else page - offset = (page - 1) * quant_per_page - stop = offset + quant_per_page - cutted = self.objects[offset:stop] - - total = len(self.objects) - pages_range = [*range(1, math.ceil(total / quant_per_page) + 1)] # each item is a page - last_page = len(pages_range) - - nav = [] - if page <= 3: - for n in [1, 2, 3]: - if n not in pages_range: - continue - text = f"· {n} ·" if n == page else n - nav.append((text, self.page_data(n))) - if last_page >= 4: - nav.append( - ('4 ›' if last_page > 5 else 4, self.page_data(4)) - ) - if last_page > 4: - nav.append( - (f'{last_page} »' if last_page > 5 else last_page, self.page_data(last_page)) - ) - elif page >= last_page - 2: - nav.extend( - [ - ('« 1' if last_page > 5 else 1, self.page_data(1)), - ( - f'‹ {last_page - 3}' if last_page > 5 else last_page - 3, - self.page_data(last_page - 3), - ), - ] - ) - - for n in range(last_page - 2, last_page + 1): - text = f"· {n} ·" if n == page else n - nav.append((text, self.page_data(n))) - else: - nav = [ - ('« 1', self.page_data(1)), - (f'‹ {page - 1}', self.page_data(page - 1)), - (f'· {page} ·', "noop"), - (f'{page + 1} ›', self.page_data(page + 1)), - (f'{last_page} »', self.page_data(last_page)), - ] - - buttons = [ - (self.item_title(item, page), self.item_data(item, page)) - for item in cutted - ] - - kb_lines = array_chunk(buttons, columns) - if last_page > 1: - kb_lines.append(nav) - - return kb_lines diff --git a/requirements.txt b/requirements.txt index 0b96a4a..72e974c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,3 +10,5 @@ apscheduler sqlitedict casbin==1.17.5 sentry-sdk==1.13.0 +PyQRCode>=1.2.1 +PyPng diff --git a/utils/docker-config.sh b/utils/docker-config.sh index 5da01da..df78d3f 100644 --- a/utils/docker-config.sh +++ b/utils/docker-config.sh @@ -19,7 +19,10 @@ configure () { printf "请输入应用程序 api_hash(不懂请直接回车):" read -r api_hash <&1 sed -i "s/HASH_HERE/$api_hash/" $config_file - printf "请输入应用程序语言(默认:zh-cn):" + read -p "二维码扫码登录?(避免无法收到验证码) [Y/n]" choi + if [ "$choi" == "y" ] || [ "$choi" == "Y" ]; then + sed -i "s/qrcode_login: \"False\"/qrcode_login: \"True\"/" $config_file + fi read -r application_language <&1 if [ -z "$application_language" ] then