diff --git a/config.gen.yml b/config.gen.yml index 4759325..e0f3384 100644 --- a/config.gen.yml +++ b/config.gen.yml @@ -11,6 +11,7 @@ api_id: "ID_HERE" api_hash: "HASH_HERE" qrcode_login: "False" +web_login: "False" # Either debug logging is enabled or not debug: "False" diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py index b513f85..e1c6348 100644 --- a/pagermaid/__init__.py +++ b/pagermaid/__init__.py @@ -21,8 +21,8 @@ from pagermaid.scheduler import scheduler import pyromod.listen from pyrogram import Client -pgm_version = "1.3.4" -pgm_version_code = 1304 +pgm_version = "1.4.0" +pgm_version_code = 1400 CMD_LIST = {} module_dir = __path__[0] working_dir = getcwd() diff --git a/pagermaid/__main__.py b/pagermaid/__main__.py index d983674..e5a7ef3 100644 --- a/pagermaid/__main__.py +++ b/pagermaid/__main__.py @@ -1,15 +1,16 @@ import asyncio from os import sep from signal import signal as signal_fn, SIGINT, SIGTERM, SIGABRT -from sys import path, platform +from sys import path, platform, exit from pyrogram.errors import AuthKeyUnregistered -from pagermaid import bot, logs, working_dir +from pagermaid import bot, logs, working_dir, Config from pagermaid.common.reload import load_all from pagermaid.single_utils import safe_remove from pagermaid.utils import lang, process_exit from pagermaid.web import web +from pagermaid.web.api.web_login import web_login from pyromod.methods.sign_in_qrcode import start_client path.insert(1, f"{working_dir}{sep}plugins") @@ -50,11 +51,29 @@ async def console_bot(): await process_exit(start=True, _client=bot) +async def web_bot(): + try: + await web_login.init() + except AuthKeyUnregistered: + safe_remove("pagermaid.session") + exit() + if bot.me is not None: + me = await bot.get_me() + if me.is_bot: + safe_remove("pagermaid.session") + exit() + else: + logs.info("Please use web to login, path: web_login .") + + async def main(): logs.info(lang("platform") + platform + lang("platform_load")) await web.start() - await console_bot() - logs.info(lang("start")) + if not (Config.WEB_ENABLE and Config.WEB_LOGIN): + await console_bot() + logs.info(lang("start")) + else: + await web_bot() try: await idle() diff --git a/pagermaid/config.py b/pagermaid/config.py index f7a5a2e..6ddca7a 100644 --- a/pagermaid/config.py +++ b/pagermaid/config.py @@ -48,6 +48,9 @@ class Config: QRCODE_LOGIN = strtobool( os.environ.get("QRCODE_LOGIN", config.get("qrcode_login", "false")) ) + WEB_LOGIN = strtobool( + os.environ.get("WEB_LOGIN", config.get("web_login", "false")) + ) STRING_SESSION = os.environ.get("STRING_SESSION") DEBUG = strtobool(os.environ.get("PGM_DEBUG", config["debug"])) ERROR_REPORT = strtobool( diff --git a/pagermaid/enums/__init__.py b/pagermaid/enums/__init__.py index 82dd64c..2e45042 100644 --- a/pagermaid/enums/__init__.py +++ b/pagermaid/enums/__init__.py @@ -5,3 +5,13 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler from sqlitedict import SqliteDict from httpx import AsyncClient from logging import Logger + +__all__ = [ + "Client", + "Message", + "Sub", + "AsyncIOScheduler", + "SqliteDict", + "AsyncClient", + "Logger" +] diff --git a/pagermaid/listener.py b/pagermaid/listener.py index 765bb59..ea1c4ff 100644 --- a/pagermaid/listener.py +++ b/pagermaid/listener.py @@ -34,6 +34,7 @@ from pagermaid.utils import ( process_exit, ) from pagermaid.hook import Hook +from pagermaid.web import web _lock = asyncio.Lock() @@ -197,7 +198,7 @@ def listener(**args): except SystemExit: await process_exit(start=False, _client=client, message=message) await Hook.shutdown() - sys.exit(0) + web.stop() except BaseException: exc_info = sys.exc_info()[1] exc_format = format_exc() diff --git a/pagermaid/modules/help.py b/pagermaid/modules/help.py index c8f7406..a92766e 100644 --- a/pagermaid/modules/help.py +++ b/pagermaid/modules/help.py @@ -1,7 +1,5 @@ """ The help module. """ - -from json import dump as json_dump -from os import listdir, sep +from os import listdir from pyrogram.enums import ParseMode diff --git a/pagermaid/modules/plugin.py b/pagermaid/modules/plugin.py index d9ce523..662ae92 100644 --- a/pagermaid/modules/plugin.py +++ b/pagermaid/modules/plugin.py @@ -140,7 +140,7 @@ async def plugin(message: Message): for target_plugin in active_plugins: inactive_plugins.remove(target_plugin) chdir(f"plugins{sep}") - for target_plugin in glob(f"*.py.disabled"): + for target_plugin in glob("*.py.disabled"): disabled_plugins += [f"{target_plugin[:-12]}"] chdir(f"..{sep}") active_plugins_string = "" diff --git a/pagermaid/modules/sentry.py b/pagermaid/modules/sentry.py index 429e91b..85f01ae 100644 --- a/pagermaid/modules/sentry.py +++ b/pagermaid/modules/sentry.py @@ -1,6 +1,7 @@ import sentry_sdk from subprocess import run, PIPE +import sys from time import time from pyrogram.errors import Unauthorized, UsernameInvalid @@ -18,7 +19,7 @@ def sentry_before_send(event, hint): if exc_info and isinstance(exc_info[1], (Unauthorized, UsernameInvalid)): # The user has been deleted/deactivated or session revoked safe_remove("pagermaid.session") - exit(1) + sys.exit(1) if time() <= sentry_sdk_report_time + 30: sentry_sdk_report_time = time() return None @@ -29,7 +30,7 @@ def sentry_before_send(event, hint): sentry_sdk_report_time = time() sentry_sdk_git_hash = ( - run("git rev-parse HEAD", stdout=PIPE, shell=True).stdout.decode().strip() + run("git rev-parse HEAD", stdout=PIPE, shell=True, check=True).stdout.decode(check=True).strip(check=True) ) sentry_sdk.init( Config.SENTRY_API, diff --git a/pagermaid/modules/status.py b/pagermaid/modules/status.py index edb7758..897940f 100644 --- a/pagermaid/modules/status.py +++ b/pagermaid/modules/status.py @@ -2,7 +2,7 @@ import re -from datetime import datetime, timezone +from datetime import datetime from platform import uname, python_version from sys import platform @@ -18,7 +18,7 @@ from psutil import boot_time, virtual_memory, disk_partitions from shutil import disk_usage from subprocess import Popen, PIPE -from pagermaid import start_time, Config, pgm_version +from pagermaid import Config, pgm_version from pagermaid.common.status import get_bot_uptime from pagermaid.enums import Client, Message from pagermaid.listener import listener diff --git a/pagermaid/modules/system.py b/pagermaid/modules/system.py index 78da180..0053095 100644 --- a/pagermaid/modules/system.py +++ b/pagermaid/modules/system.py @@ -1,3 +1,4 @@ +import sys from getpass import getuser from os.path import exists, sep from platform import node @@ -8,7 +9,6 @@ from pagermaid.common.system import run_eval from pagermaid.enums import Message from pagermaid.listener import listener from pagermaid.utils import attach_log, execute, lang, upload_attachment -from pagermaid.web import web @listener( @@ -51,7 +51,7 @@ async def restart(message: Message): """To re-execute PagerMaid.""" if not message.text[0].isalpha(): await message.edit(lang("restart_log")) - web.stop() + sys.exit(0) @listener( @@ -65,7 +65,8 @@ async def sh_eval(message: Message): """Run python commands from Telegram.""" dev_mode = exists(f"data{sep}dev") try: - assert dev_mode + if not dev_mode: + raise AssertionError cmd = message.text.split(" ", maxsplit=1)[1] except (IndexError, AssertionError): return await message.edit(lang("eval_need_dev")) diff --git a/pagermaid/services/__init__.py b/pagermaid/services/__init__.py index 35810b1..64d3b16 100644 --- a/pagermaid/services/__init__.py +++ b/pagermaid/services/__init__.py @@ -4,6 +4,14 @@ from pagermaid.single_utils import sqlite from pagermaid.scheduler import scheduler from pagermaid.utils import client +__all__ = [ + "bot", + "logs", + "sqlite", + "scheduler", + "client", +] + def get(name: str): data = { @@ -13,4 +21,4 @@ def get(name: str): "AsyncIOScheduler": scheduler, "AsyncClient": client, } - return data.get(name, None) + return data.get(name) diff --git a/pagermaid/single_utils.py b/pagermaid/single_utils.py index 2d4d50d..87c2b88 100644 --- a/pagermaid/single_utils.py +++ b/pagermaid/single_utils.py @@ -3,7 +3,6 @@ from os import sep, remove, mkdir from os.path import exists from typing import List, Optional, Union from apscheduler.schedulers.asyncio import AsyncIOScheduler -from httpx import AsyncClient from pyrogram import Client as OldClient from pyrogram.types import Chat as OldChat, Message as OldMessage, Dialog @@ -14,9 +13,13 @@ from pyromod.utils.errors import ( TimeoutConversationError, ListenerCanceled, ) - from sqlitedict import SqliteDict +__all__ = [ + "AlreadyInConversationError", + "TimeoutConversationError", + "ListenerCanceled", +] # init folders if not exists("data"): mkdir("data") diff --git a/pagermaid/utils.py b/pagermaid/utils.py index e725ba7..d8c9364 100644 --- a/pagermaid/utils.py +++ b/pagermaid/utils.py @@ -10,7 +10,7 @@ from sys import executable from asyncio import create_subprocess_shell, sleep from asyncio.subprocess import PIPE -from pyrogram import filters, enums +from pyrogram import filters from pagermaid.config import Config from pagermaid import bot from pagermaid.group_manager import enforce_permission diff --git a/pagermaid/web/__init__.py b/pagermaid/web/__init__.py index e641af5..079f14f 100644 --- a/pagermaid/web/__init__.py +++ b/pagermaid/web/__init__.py @@ -7,7 +7,7 @@ from starlette.responses import RedirectResponse from pagermaid import logs from pagermaid.config import Config -from pagermaid.web.api import base_api_router +from pagermaid.web.api import base_api_router, base_html_router from pagermaid.web.pages import admin_app, login_page requestAdaptor = """ @@ -39,6 +39,7 @@ class Web: def init_web(self): self.app.include_router(base_api_router) + self.app.include_router(base_html_router) self.app.add_middleware( CORSMiddleware, diff --git a/pagermaid/web/api/__init__.py b/pagermaid/web/api/__init__.py index 46b3e99..cb57d01 100644 --- a/pagermaid/web/api/__init__.py +++ b/pagermaid/web/api/__init__.py @@ -7,8 +7,12 @@ from pagermaid.web.api.ignore_groups import route as ignore_groups_route from pagermaid.web.api.login import route as login_route from pagermaid.web.api.plugin import route as plugin_route from pagermaid.web.api.status import route as status_route +from pagermaid.web.api.web_login import route as web_login_route, html_route as web_login_html_route + +__all__ = ["authentication", "base_api_router", "base_html_router"] base_api_router = APIRouter(prefix="/pagermaid/api") +base_html_router = APIRouter() base_api_router.include_router(plugin_route) base_api_router.include_router(bot_info_route) @@ -16,3 +20,5 @@ base_api_router.include_router(status_route) base_api_router.include_router(login_route) base_api_router.include_router(command_alias_route) base_api_router.include_router(ignore_groups_route) +base_api_router.include_router(web_login_route) +base_html_router.include_router(web_login_html_route) diff --git a/pagermaid/web/api/web_login.py b/pagermaid/web/api/web_login.py new file mode 100644 index 0000000..2b4121e --- /dev/null +++ b/pagermaid/web/api/web_login.py @@ -0,0 +1,125 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from pydantic import BaseModel +from pyrogram.errors import BadRequest, AuthTokenExpired +from pyrogram.raw.functions.account import InitTakeoutSession +from pyrogram.raw.functions.updates import GetState +from starlette.responses import HTMLResponse + +from pagermaid import bot, logs +from pagermaid.common.reload import load_all +from pagermaid.utils import lang, process_exit +from pagermaid.web.api import authentication +from pagermaid.web.html import get_web_login_html +from pyromod.methods.sign_in_qrcode import authorize_by_qrcode_web +from pyromod.utils.errors import QRCodeWebCodeError, QRCodeWebNeedPWDError +import sys + + +class UserModel(BaseModel): + password: str + + +class WebLogin(): + def __init__(self): + self.is_authorized = False + self.need_password = False + self.password_hint = "" + + async def connect(self): + if not bot.is_connected: + self.is_authorized = await bot.connect() + + @staticmethod + async def initialize(): + if bot.is_connected and not bot.is_initialized: + await bot.initialize() + + @staticmethod + def has_login(): + return bot.me is not None + + @staticmethod + async def init_bot(): + logs.info(f"{lang('save_id')} {bot.me.first_name}({bot.me.id})") + await load_all() + await process_exit(start=True, _client=bot) + logs.info(lang("start")) + + async def init(self): + await self.connect() + if not self.is_authorized: + return + if not await bot.storage.is_bot() and bot.takeout: + bot.takeout_id = ( + await bot.invoke(InitTakeoutSession()) + ).id + + await bot.invoke(GetState()) + bot.me = await bot.get_me() + if bot.me.is_bot: + sys.exit(0) + await self.initialize() + await self.init_bot() + + +route = APIRouter() +html_route = APIRouter() +web_login = WebLogin() +web_login_html = get_web_login_html() + + +@route.get("/web_login", response_class=JSONResponse, dependencies=[authentication()]) +async def web_login_qrcode(): + if web_login.has_login(): + return {"status": 0, "msg": "已登录"} + try: + await web_login.connect() + if not web_login.is_authorized: + await authorize_by_qrcode_web(bot) + web_login.is_authorized = True + await web_login.init() + return {"status": 0, "msg": "登录成功"} + except QRCodeWebCodeError as e: + web_login.need_password = False + return {"status": 1, "msg": "未扫码", "content": e.code} + except QRCodeWebNeedPWDError as e: + web_login.need_password = True + web_login.password_hint = e.hint or "" + return {"status": 2, "msg": "需要密码", "content": web_login.password_hint} + except AuthTokenExpired: + return {"status": 3, "msg": "登录状态过期,请重新扫码登录"} + except BadRequest as e: + return {"status": 3, "msg": e.MESSAGE} + except Exception as e: + return {"status": 3, "msg": f"{type(e)}"} + + +@route.post("/web_login", response_class=JSONResponse, dependencies=[authentication()]) +async def web_login_password(user: UserModel): + if web_login.has_login(): + return {"status": 0, "msg": "已登录"} + if not web_login.need_password: + return {"status": 0, "msg": "无需密码"} + try: + await authorize_by_qrcode_web(bot, user.password) + web_login.is_authorized = True + await web_login.init() + return {"status": 0, "msg": "登录成功"} + except QRCodeWebCodeError as e: + return {"status": 1, "msg": "需要重新扫码", "content": e.code} + except QRCodeWebNeedPWDError as e: + web_login.need_password = True + return {"status": 2, "msg": "密码错误", "content": e.hint or ""} + except AuthTokenExpired: + web_login.need_password = False + return {"status": 3, "msg": "登录状态过期,请重新扫码登录"} + except BadRequest as e: + return {"status": 3, "msg": e.MESSAGE} + except Exception as e: + return {"status": 3, "msg": f"{type(e)}"} + + +@html_route.get("/web_login", response_class=HTMLResponse) +async def get_web_login(): + return web_login_html diff --git a/pagermaid/web/html/__init__.py b/pagermaid/web/html/__init__.py index e770269..77ba242 100644 --- a/pagermaid/web/html/__init__.py +++ b/pagermaid/web/html/__init__.py @@ -22,3 +22,8 @@ def get_github_logo() -> str: def get_footer() -> str: """获取 footer。""" return get_html(html_base_path / "footer.html") + + +def get_web_login_html() -> str: + """获取 web login。""" + return get_html(html_base_path / "web_login.html") diff --git a/pagermaid/web/html/web_login.html b/pagermaid/web/html/web_login.html new file mode 100644 index 0000000..6767295 --- /dev/null +++ b/pagermaid/web/html/web_login.html @@ -0,0 +1,202 @@ + + +
+ + + + + + + + + +Scan the QR code above to log in.
+