diff --git a/pagermaid/__init__.py b/pagermaid/__init__.py index 6e70a8f..f2f1df0 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.26" +pgm_version = "1.2.30" CMD_LIST = {} module_dir = __path__[0] working_dir = getcwd() diff --git a/pagermaid/__main__.py b/pagermaid/__main__.py index c846152..a29bf99 100644 --- a/pagermaid/__main__.py +++ b/pagermaid/__main__.py @@ -6,6 +6,7 @@ from pyrogram import idle from pyrogram.errors import AuthKeyUnregistered from pagermaid import bot, logs, working_dir +from pagermaid.common.plugin import plugin_manager from pagermaid.hook import Hook from pagermaid.modules import module_list, plugin_list from pagermaid.single_utils import safe_remove @@ -41,6 +42,7 @@ async def main(): except BaseException as exception: logs.info(f"{lang('module')} {plugin_name} {lang('error')}: {exception}") plugin_list.remove(plugin_name) + plugin_manager.load_local_plugins() await process_exit(start=True, _client=bot) logs.info(lang('start')) diff --git a/pagermaid/common/alias.py b/pagermaid/common/alias.py new file mode 100644 index 0000000..6ed8b5d --- /dev/null +++ b/pagermaid/common/alias.py @@ -0,0 +1,66 @@ +from os import sep +from json import dump as json_dump +from typing import List, Dict + +from pydantic import BaseModel + +from pagermaid.common.reload import reload_all +from pagermaid.config import Config + + +class Alias(BaseModel): + command: str + alias: str + + +class AliasManager: + def __init__(self): + self.alias_list = [] + for key, value in Config.alias_dict.items(): + temp = Alias(command=key, alias=value) + self.alias_list.append(temp) + + def get_all_alias(self): + return self.alias_list + + def get_all_alias_dict(self): + return [i.dict() for i in self.alias_list] + + def get_all_alias_text(self) -> str: + texts = [] + texts.extend( + f'`{i.command}` > `{i.alias}`' + for i in self.alias_list + ) + return '\n'.join(texts) + + @staticmethod + def save(): + with open(f"data{sep}alias.json", 'w', encoding="utf-8") as f: + json_dump(Config.alias_dict, f) + + @staticmethod + def delete_alias(source_command: str): + del Config.alias_dict[source_command] + AliasManager.save() + + @staticmethod + def add_alias(source_command: str, to_command: str): + Config.alias_dict[source_command] = to_command + AliasManager.save() + + @staticmethod + async def save_from_web(data: List[Dict]): + for i in data: + temp = Alias(**i) + Config.alias_dict[temp.command] = temp.alias + AliasManager.save() + await reload_all() + + def test_alias(self, message: str) -> str: + r = message.split(" ") + for i in self.alias_list: + if i.command == r[0]: + r[0] = i.alias + break + return " ".join(r) diff --git a/pagermaid/common/cache.py b/pagermaid/common/cache.py new file mode 100644 index 0000000..ccada32 --- /dev/null +++ b/pagermaid/common/cache.py @@ -0,0 +1,35 @@ +import datetime +import functools +import inspect +from typing import Any, Dict, Optional + +from pydantic import BaseModel + + +class Cache(BaseModel): + value: Any + time: Optional[datetime.datetime] + + +def cache(ttl=datetime.timedelta(minutes=15)): + def wrap(func): + cache_data: Dict[str, Cache] = {} + + @functools.wraps(func) + async def wrapped(*args, **kw): + nonlocal cache_data + bound = inspect.signature(func).bind(*args, **kw) + bound.apply_defaults() + ins_key = '|'.join([f'{k}_{v}' for k, v in bound.arguments.items()]) + data: Cache = cache_data.get(ins_key, Cache(value=None, time=None)) + now = datetime.datetime.now() + if (not data.time) or ((now - data.time) > ttl): + try: + data.value = await func(*args, **kw) + data.time = datetime.datetime.now() + cache_data[ins_key] = data + except Exception as e: + raise e + return data.value + return wrapped + return wrap diff --git a/pagermaid/common/ignore.py b/pagermaid/common/ignore.py new file mode 100644 index 0000000..0af97d8 --- /dev/null +++ b/pagermaid/common/ignore.py @@ -0,0 +1,22 @@ +import json + +from pyrogram.enums import ChatType + +from pagermaid import bot +from pagermaid.sub_utils import Sub + +ignore_groups_manager = Sub("ignore_groups") + + +async def get_group_list(): + try: + return [ + json.loads(str(dialog.chat)) + for dialog in await bot.get_dialogs_list() + if ( + dialog.chat + and dialog.chat.type in [ChatType.SUPERGROUP, ChatType.GROUP] + ) + ] + except BaseException: + return [] diff --git a/pagermaid/common/plugin.py b/pagermaid/common/plugin.py new file mode 100644 index 0000000..5424a2e --- /dev/null +++ b/pagermaid/common/plugin.py @@ -0,0 +1,181 @@ +import contextlib +import json +import os +from pathlib import Path +from typing import Optional, List + +from pydantic import BaseModel + +from pagermaid import Config +from pagermaid.common.cache import cache +from pagermaid.utils import client + +plugins_path = Path('plugins') + + +class LocalPlugin(BaseModel): + name: str + status: bool + installed: bool = False + version: Optional[float] + + @property + def normal_path(self) -> Path: + return plugins_path / f"{self.name}.py" + + @property + def disabled_path(self) -> Path: + return plugins_path / f"{self.name}.py.disabled" + + def remove(self): + with contextlib.suppress(FileNotFoundError): + os.remove(self.normal_path) + with contextlib.suppress(FileNotFoundError): + os.remove(self.disabled_path) + + def enable(self) -> bool: + try: + os.rename(self.disabled_path, self.normal_path) + return True + except Exception: + return False + + def disable(self) -> bool: + try: + os.rename(self.normal_path, self.disabled_path) + return True + except Exception: + return False + + +class RemotePlugin(LocalPlugin): + section: str + maintainer: str + size: str + supported: bool + des: str + ... + + async def install(self) -> bool: + html = await client.get(f'{Config.GIT_SOURCE}{self.name}/main.py') + if html.status_code == 200: + self.remove() + with open(plugins_path / f"{self.name}.py", mode="wb") as f: + f.write(html.text.encode('utf-8')) + return True + return False + + +class PluginManager: + def __init__(self): + self.version_map = {} + self.remote_version_map = {} + self.plugins: List[LocalPlugin] = [] + self.remote_plugins: List[RemotePlugin] = [] + + def load_local_version_map(self): + if not os.path.exists(plugins_path / "version.json"): + return + with open(plugins_path / "version.json", 'r', encoding="utf-8") as f: + self.version_map = json.load(f) + + def save_local_version_map(self): + with open(plugins_path / "version.json", 'w', encoding="utf-8") as f: + json.dump(self.version_map, f, indent=4) + + def get_local_version(self, name: str) -> Optional[float]: + return self.version_map.get(name) + + def set_local_version(self, name: str, version: float) -> None: + self.version_map[name] = version + self.save_local_version_map() + + def get_plugin_install_status(self, name: str) -> bool: + return name in self.version_map + + @staticmethod + def get_plugin_load_status(name: str) -> bool: + return bool(os.path.exists(plugins_path / f"{name}.py")) + + def remove_plugin(self, name: str) -> bool: + if plugin := self.get_local_plugin(name): + plugin.remove() + if name in self.version_map: + self.version_map.pop(name) + self.save_local_version_map() + return True + return False + + def enable_plugin(self, name: str) -> bool: + return plugin.enable() if (plugin := self.get_local_plugin(name)) else False + + def disable_plugin(self, name: str) -> bool: + return plugin.disable() if (plugin := self.get_local_plugin(name)) else False + + def load_local_plugins(self) -> List[LocalPlugin]: + self.load_local_version_map() + self.plugins = [] + for plugin in os.listdir('plugins'): + if plugin.endswith('.py') or plugin.endswith('.py.disabled'): + plugin = plugin[:-12] if plugin.endswith('.py.disabled') else plugin[:-3] + self.plugins.append( + LocalPlugin( + name=plugin, + installed=self.get_plugin_install_status(plugin), + status=self.get_plugin_load_status(plugin), + version=self.get_local_version(plugin) + ) + ) + return self.plugins + + def get_local_plugin(self, name: str) -> LocalPlugin: + return next(filter(lambda x: x.name == name, self.plugins), None) + + @cache() + async def load_remote_plugins(self) -> List[RemotePlugin]: + plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") + plugin_list = plugin_list.json()["list"] + plugins = [ + RemotePlugin( + **plugin, + status=self.get_plugin_load_status(plugin["name"]) + ) for plugin in plugin_list + ] + self.remote_plugins = plugins + self.remote_version_map = {} + for plugin in plugins: + self.remote_version_map[plugin.name] = plugin.version + return plugins + + def get_remote_plugin(self, name: str) -> RemotePlugin: + return next(filter(lambda x: x.name == name, self.remote_plugins), None) + + def plugin_need_update(self, name: str) -> bool: + if local_version := self.get_local_version(name): + if local_version == 0.0: + return False + if remote_version := self.remote_version_map.get(name): + return local_version < remote_version + return False + + async def install_remote_plugin(self, name: str) -> bool: + if plugin := self.get_remote_plugin(name): + if await plugin.install(): + self.set_local_version(name, plugin.version) + return True + return False + + async def update_remote_plugin(self, name: str) -> bool: + if self.plugin_need_update(name): + return await self.install_remote_plugin(name) + return False + + async def update_all_remote_plugin(self) -> List[RemotePlugin]: + updated_plugins = [] + for i in self.remote_plugins: + if await self.update_remote_plugin(i.name): + updated_plugins.append(i) + return updated_plugins + + +plugin_manager = PluginManager() diff --git a/pagermaid/common/reload.py b/pagermaid/common/reload.py new file mode 100644 index 0000000..6917d04 --- /dev/null +++ b/pagermaid/common/reload.py @@ -0,0 +1,46 @@ +import contextlib +import importlib +import os + +import pagermaid.config +import pagermaid.modules +from pagermaid import read_context, bot, help_messages, all_permissions, hook_functions, logs +from pagermaid.common.plugin import plugin_manager +from pagermaid.hook import Hook +from pagermaid.utils import lang + + +async def reload_all(): + read_context.clear() + bot.dispatcher.remove_all_handlers() + bot.job.remove_all_jobs() + with contextlib.suppress(RuntimeError): + bot.cancel_all_listener() + loaded_plugins = list(pagermaid.modules.plugin_list) + loaded_plugins.extend(iter(pagermaid.modules.module_list)) + # init + importlib.reload(pagermaid.modules) + importlib.reload(pagermaid.config) + help_messages.clear() + all_permissions.clear() + for functions in hook_functions.values(): + functions.clear() # noqa: clear all hooks + + for module_name in pagermaid.modules.module_list: + try: + module = importlib.import_module(f"pagermaid.modules.{module_name}") + if module_name in loaded_plugins: + importlib.reload(module) + except BaseException as exception: + logs.info(f"{lang('module')} {module_name} {lang('error')}: {type(exception)}: {exception}") + for plugin_name in pagermaid.modules.plugin_list.copy(): + try: + plugin = importlib.import_module(f"plugins.{plugin_name}") + if plugin_name in loaded_plugins and os.path.exists(plugin.__file__): + importlib.reload(plugin) + except BaseException as exception: + logs.info(f"{lang('module')} {plugin_name} {lang('error')}: {exception}") + pagermaid.modules.plugin_list.remove(plugin_name) + plugin_manager.load_local_plugins() + plugin_manager.save_local_version_map() + await Hook.load_success_exec() diff --git a/pagermaid/common/status.py b/pagermaid/common/status.py new file mode 100644 index 0000000..3ff91e5 --- /dev/null +++ b/pagermaid/common/status.py @@ -0,0 +1,54 @@ +import asyncio +from datetime import datetime, timezone + +import psutil +from pydantic import BaseModel +from pagermaid import start_time, Config, pgm_version + + +class Status(BaseModel): + version: str + run_time: str + cpu_percent: str + ram_percent: str + swap_percent: str + + +async def human_time_duration(seconds) -> str: + parts = {} + time_units = ( + ('%m', 60 * 60 * 24 * 30), + ('%d', 60 * 60 * 24), + ('%H', 60 * 60), + ('%M', 60), + ('%S', 1) + ) + for unit, div in time_units: + amount, seconds = divmod(int(seconds), div) + parts[unit] = str(amount) + time_form = Config.START_FORM + for key, value in parts.items(): + time_form = time_form.replace(key, value) + return time_form + + +async def get_bot_uptime() -> str: + current_time = datetime.now(timezone.utc) + uptime_sec = (current_time - start_time).total_seconds() + return await human_time_duration(int(uptime_sec)) + + +async def get_status() -> Status: + uptime = await get_bot_uptime() + psutil.cpu_percent() + await asyncio.sleep(0.1) + cpu_percent = psutil.cpu_percent() + ram_stat = psutil.virtual_memory() + swap_stat = psutil.swap_memory() + return Status( + version=pgm_version, + run_time=uptime, + cpu_percent=f'{cpu_percent}%', + ram_percent=f'{ram_stat.percent}%', + swap_percent=f'{swap_stat.percent}%', + ) diff --git a/pagermaid/common/system.py b/pagermaid/common/system.py new file mode 100644 index 0000000..dee7b21 --- /dev/null +++ b/pagermaid/common/system.py @@ -0,0 +1,45 @@ +import io +import sys +import traceback + +from pagermaid import bot + + +async def run_eval(cmd: str, message=None, only_result: bool = False) -> str: + old_stderr = sys.stderr + old_stdout = sys.stdout + redirected_output = sys.stdout = io.StringIO() + redirected_error = sys.stderr = io.StringIO() + stdout, stderr, exc = None, None, None + try: + await aexec(cmd, message, bot) + except Exception: # noqa + exc = traceback.format_exc() + stdout = redirected_output.getvalue() + stderr = redirected_error.getvalue() + sys.stdout = old_stdout + sys.stderr = old_stderr + if exc: + evaluation = exc + elif stderr: + evaluation = stderr + elif stdout: + evaluation = stdout + else: + evaluation = "Success" + return evaluation if only_result else f"**>>>** `{cmd}` \n`{evaluation}`" + + +async def aexec(code, event, client): + exec( + ( + ( + ("async def __aexec(e, client): " + "\n msg = message = e") + + "\n reply = message.reply_to_message if message else None" + ) + + "\n chat = e.chat if e else None" + ) + + "".join(f"\n {x}" for x in code.split("\n")) + ) + + return await locals()["__aexec"](event, client) diff --git a/pagermaid/common/update.py b/pagermaid/common/update.py new file mode 100644 index 0000000..7441472 --- /dev/null +++ b/pagermaid/common/update.py @@ -0,0 +1,12 @@ +from sys import executable + +from pagermaid.utils import execute + + +async def update(force: bool = False): + await execute('git fetch --all') + if force: + await execute('git reset --hard origin/master') + await execute('git pull --all') + await execute(f"{executable} -m pip install --upgrade -r requirements.txt") + await execute(f"{executable} -m pip install -r requirements.txt") diff --git a/pagermaid/config.py b/pagermaid/config.py index 5e59a0d..c85246f 100644 --- a/pagermaid/config.py +++ b/pagermaid/config.py @@ -98,6 +98,11 @@ class Config: alias_dict = load_json(f) except Exception as e: alias_dict = {} + web_interface = config.get("web_interface", {}) + WEB_ENABLE = strtobool(os.environ.get("WEB_ENABLE", web_interface.get("enable", "False"))) + WEB_SECRET_KEY = os.environ.get("WEB_SECRET_KEY", web_interface.get("secret_key", "secret_key")) + WEB_HOST = os.environ.get("WEB_HOST", web_interface.get("host", "127.0.0.1")) + WEB_PORT = int(os.environ.get("WEB_PORT", web_interface.get("port", 3333))) except ValueError as e: print(e) sys.exit(1) diff --git a/pagermaid/listener.py b/pagermaid/listener.py index 0cbf5ef..e988054 100644 --- a/pagermaid/listener.py +++ b/pagermaid/listener.py @@ -15,6 +15,7 @@ from pyrogram.errors.exceptions.bad_request_400 import ( from pyrogram.handlers import MessageHandler, EditedMessageHandler from pagermaid import help_messages, logs, Config, bot, read_context, all_permissions +from pagermaid.common.ignore import ignore_groups_manager from pagermaid.group_manager import Permission from pagermaid.inject import inject from pagermaid.single_utils import Message, AlreadyInConversationError, TimeoutConversationError, ListenerCanceled @@ -114,6 +115,14 @@ def listener(**args): async def handler(client: Client, message: Message): try: + # ignore + try: + if ignore_groups_manager.check_id(message.chat.id): + raise ContinuePropagation + except ContinuePropagation: + raise ContinuePropagation + except BaseException: + pass try: parameter = message.matches[0].group(2).split(" ") if parameter == [""]: @@ -229,6 +238,14 @@ def raw_listener(filter_s): def decorator(function): async def handler(client, message): + # ignore + try: + if ignore_groups_manager.check_id(message.chat.id): + raise ContinuePropagation + except ContinuePropagation: + raise ContinuePropagation + except BaseException: + pass # solve same process async with _lock: if (message.chat.id, message.id) in read_context: diff --git a/pagermaid/modules/help.py b/pagermaid/modules/help.py index bc7ff94..221914d 100644 --- a/pagermaid/modules/help.py +++ b/pagermaid/modules/help.py @@ -6,8 +6,9 @@ from os import listdir, sep from pyrogram.enums import ParseMode from pagermaid import help_messages, Config +from pagermaid.common.alias import AliasManager from pagermaid.group_manager import enforce_permission -from pagermaid.modules.reload import reload_all +from pagermaid.common.reload import reload_all from pagermaid.utils import lang, Message, from_self, from_msg_get_sudo_uid from pagermaid.listener import listener @@ -120,31 +121,18 @@ async def lang_change(message: Message): description=lang('alias_des'), parameters='{list|del|set} ') async def alias_commands(message: Message): - source_commands = [] - to_commands = [] - texts = [] - for key, value in Config.alias_dict.items(): - source_commands.append(key) - to_commands.append(value) + alias_manager = AliasManager() if len(message.parameter) == 0: await message.edit(lang('arg_error')) - return elif len(message.parameter) == 1: - if source_commands: - texts.extend( - f'`{source_commands[i]}` > `{to_commands[i]}`' - for i in range(len(source_commands)) - ) - - await message.edit(lang('alias_list') + '\n\n' + '\n'.join(texts)) + if alias_manager.alias_list: + await message.edit(lang('alias_list') + '\n\n' + alias_manager.get_all_alias_text()) else: await message.edit(lang('alias_no')) elif len(message.parameter) == 2: source_command = message.parameter[1] try: - del Config.alias_dict[source_command] - with open(f"data{sep}alias.json", 'w', encoding="utf-8") as f: - json_dump(Config.alias_dict, f) + alias_manager.delete_alias(source_command) await message.edit(lang('alias_success')) await reload_all() except KeyError: @@ -156,8 +144,6 @@ async def alias_commands(message: Message): if to_command in help_messages: await message.edit(lang('alias_exist')) return - Config.alias_dict[source_command] = to_command - with open(f"data{sep}alias.json", 'w', encoding="utf-8") as f: - json_dump(Config.alias_dict, f) + alias_manager.add_alias(source_command, to_command) await message.edit(lang('alias_success')) await reload_all() diff --git a/pagermaid/modules/plugin.py b/pagermaid/modules/plugin.py index f7188c2..16f40a9 100644 --- a/pagermaid/modules/plugin.py +++ b/pagermaid/modules/plugin.py @@ -2,20 +2,18 @@ import contextlib import json -import importlib - -from re import search, I -from os import remove, rename, chdir, path, sep -from os.path import exists -from shutil import copyfile, move from glob import glob +from os import remove, chdir, path, sep +from os.path import exists +from re import search, I +from shutil import copyfile, move -from pagermaid import log, working_dir, Config, scheduler +from pagermaid import log, working_dir, Config +from pagermaid.common.plugin import plugin_manager +from pagermaid.common.reload import reload_all from pagermaid.listener import listener -from pagermaid.single_utils import safe_remove -from pagermaid.utils import upload_attachment, lang, Message, client from pagermaid.modules import plugin_list as active_plugins, __list_plugins -from pagermaid.modules.reload import reload_all +from pagermaid.utils import upload_attachment, lang, Message, client def remove_plugin(name): @@ -33,14 +31,6 @@ def move_plugin(file_path): move(file_path, plugin_directory) -async def download(name): - html = await client.get(f'{Config.GIT_SOURCE}{name}/main.py') - assert html.status_code == 200 - with open(f'plugins{sep}{name}.py', mode='wb') as f: - f.write(html.text.encode('utf-8')) - return f'plugins{sep}{name}.py' - - def update_version(plugin_name, version): plugin_directory = f"{working_dir}{sep}plugins{sep}" with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: @@ -82,43 +72,27 @@ async def plugin(message: Message): await log(f"{lang('apt_install_success')} {path.basename(file_path)[:-3]}.") await reload_all() elif len(message.parameter) >= 2: + await plugin_manager.load_remote_plugins() process_list = message.parameter message = await message.edit(lang('apt_processing')) del process_list[0] success_list = [] failed_list = [] no_need_list = [] - plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") - plugin_list = plugin_list.json()["list"] for i in process_list: - if exists(f"{plugin_directory}version.json"): - with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: - version_json = json.load(f) - try: - plugin_version = version_json[i] - except: # noqa - plugin_version = 0 + if plugin_manager.get_remote_plugin(i): + local_temp = plugin_manager.get_local_plugin(i) + if local_temp and not plugin_manager.plugin_need_update(i): + no_need_list.append(i) + else: + try: + if await plugin_manager.install_remote_plugin(i): + success_list.append(i) + else: + failed_list.append(i) + except Exception: + failed_list.append(i) else: - temp_dict = {} - with open(f"{plugin_directory}version.json", 'w') as f: - json.dump(temp_dict, f) - plugin_version = 0 - temp = True - for x in plugin_list: - if x["name"] == i: - if (float(x["version"]) - float(plugin_version)) <= 0: - no_need_list.append(i) - else: - remove_plugin(i) - try: - await download(i) - except AssertionError: - break - update_version(i, x["version"]) - success_list.append(i) - temp = False - break - if temp: failed_list.append(i) text = f"{lang('apt_name')}\n\n" if len(success_list) > 0: @@ -136,15 +110,7 @@ async def plugin(message: Message): await message.edit(lang('arg_error')) elif message.parameter[0] == "remove": if len(message.parameter) == 2: - if exists(f"{plugin_directory}{message.parameter[1]}.py") or \ - exists(f"{plugin_directory}{message.parameter[1]}.py.disabled"): - remove_plugin(message.parameter[1]) - if exists(f"{plugin_directory}version.json"): - with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: - version_json = json.load(f) - version_json[message.parameter[1]] = "0.0" - with open(f"{plugin_directory}version.json", 'w') as f: - json.dump(version_json, f) + if plugin_manager.remove_plugin(message.parameter[1]): await message.edit(f"{lang('apt_remove_success')} {message.parameter[1]}") await log(f"{lang('apt_remove')} {message.parameter[1]}.") await reload_all() @@ -192,9 +158,7 @@ async def plugin(message: Message): await message.edit(lang('arg_error')) elif message.parameter[0] == "enable": if len(message.parameter) == 2: - if exists(f"{plugin_directory}{message.parameter[1]}.py.disabled"): - rename(f"{plugin_directory}{message.parameter[1]}.py.disabled", - f"{plugin_directory}{message.parameter[1]}.py") + if plugin_manager.enable_plugin(message.parameter[1]): await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " f"{lang('apt_enable')}") await log(f"{lang('apt_enable')} {message.parameter[1]}.") @@ -205,9 +169,7 @@ async def plugin(message: Message): await message.edit(lang('arg_error')) elif message.parameter[0] == "disable": if len(message.parameter) == 2: - if exists(f"{plugin_directory}{message.parameter[1]}.py") is True: - rename(f"{plugin_directory}{message.parameter[1]}.py", - f"{plugin_directory}{message.parameter[1]}.py.disabled") + if plugin_manager.disable_plugin(message.parameter[1]): await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " f"{lang('apt_disable')}") await log(f"{lang('apt_disable')} {message.parameter[1]}.") @@ -240,55 +202,20 @@ async def plugin(message: Message): else: await message.edit(lang('arg_error')) elif message.parameter[0] == "update": - un_need_update = lang('apt_no_update') - need_update = f"\n{lang('apt_updated')}:" - need_update_list = [] if not exists(f"{plugin_directory}version.json"): await message.edit(lang('apt_why_not_install_a_plugin')) return - with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: - version_json = json.load(f) - plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") - plugin_online = plugin_list.json()["list"] - for key, value in version_json.items(): - if value == "0.0": - continue - for i in plugin_online: - if key == i["name"]: - if (float(i["version"]) - float(value)) <= 0: - un_need_update += "\n`" + key + "`:Ver " + value - else: - need_update_list.extend([key]) - need_update += "\n" + key + ":Ver " + value + " > Ver " + i['version'] - continue - if un_need_update == f"{lang('apt_no_update')}:": - un_need_update = "" - if need_update == f"\n{lang('apt_updated')}:": - need_update = "" - if not un_need_update and not need_update: - await message.edit(lang("apt_why_not_install_a_plugin")) + await plugin_manager.load_remote_plugins() + updated_plugins = await plugin_manager.update_all_remote_plugin() + if len(updated_plugins) == 0: + await message.edit(f"{lang('apt_name')}\n\n" + + lang("apt_loading_from_online_but_nothing_need_to_update")) else: - if len(need_update_list) == 0: - await message.edit(f"{lang('apt_name')}\n\n" + - lang("apt_loading_from_online_but_nothing_need_to_update")) - else: - message = await message.edit(lang("apt_loading_from_online_and_updating")) - plugin_directory = f"{working_dir}{sep}plugins{sep}" - for i in need_update_list: - remove_plugin(i) - try: - await download(i) - except AssertionError: - continue - with open(f"{plugin_directory}version.json", "r", encoding="utf-8") as f: - version_json = json.load(f) - for m in plugin_online: - if m["name"] == i: - version_json[i] = m["version"] - with open(f"{plugin_directory}version.json", "w") as f: - json.dump(version_json, f) - await message.edit(f"{lang('apt_name')}\n\n" + lang("apt_reading_list") + need_update) - await reload_all() + message = await message.edit(lang("apt_loading_from_online_and_updating")) + await message.edit( + f"{lang('apt_name')}\n\n" + lang("apt_reading_list") + "\n".join(updated_plugins) + ) + await reload_all() elif message.parameter[0] == "search": if len(message.parameter) == 1: await message.edit(lang("apt_search_no_name")) @@ -354,9 +281,5 @@ async def plugin(message: Message): await message.edit(lang("apt_why_not_install_a_plugin")) else: await message.edit(",apt install " + " ".join(list_plugin)) - elif message.parameter[0] == "reload": - # bot.dispatcher.remove_all_handlers() # noqa - scheduler.remove_all_jobs() - importlib.reload(importlib.import_module("plugins.sign")) else: await message.edit(lang("arg_error")) diff --git a/pagermaid/modules/reload.py b/pagermaid/modules/reload.py index 858c03f..057a587 100644 --- a/pagermaid/modules/reload.py +++ b/pagermaid/modules/reload.py @@ -1,51 +1,11 @@ -import contextlib -import importlib -import os - -import pagermaid.config -import pagermaid.modules -from pagermaid import logs, help_messages, all_permissions, hook_functions, read_context +from pagermaid import read_context +from pagermaid.common.reload import reload_all from pagermaid.enums import Message -from pagermaid.hook import Hook from pagermaid.listener import listener -from pagermaid.services import bot, scheduler +from pagermaid.services import scheduler from pagermaid.utils import lang -async def reload_all(): - read_context.clear() - bot.dispatcher.remove_all_handlers() - bot.job.remove_all_jobs() - with contextlib.suppress(RuntimeError): - bot.cancel_all_listener() - loaded_plugins = list(pagermaid.modules.plugin_list) - loaded_plugins.extend(iter(pagermaid.modules.module_list)) - # init - importlib.reload(pagermaid.modules) - importlib.reload(pagermaid.config) - help_messages.clear() - all_permissions.clear() - for functions in hook_functions.values(): - functions.clear() # noqa: clear all hooks - - for module_name in pagermaid.modules.module_list: - try: - module = importlib.import_module(f"pagermaid.modules.{module_name}") - if module_name in loaded_plugins: - importlib.reload(module) - except BaseException as exception: - logs.info(f"{lang('module')} {module_name} {lang('error')}: {type(exception)}: {exception}") - for plugin_name in pagermaid.modules.plugin_list.copy(): - try: - plugin = importlib.import_module(f"plugins.{plugin_name}") - if plugin_name in loaded_plugins and os.path.exists(plugin.__file__): - importlib.reload(plugin) - except BaseException as exception: - logs.info(f"{lang('module')} {plugin_name} {lang('error')}: {exception}") - pagermaid.modules.plugin_list.remove(plugin_name) - await Hook.load_success_exec() - - @listener(is_plugin=False, command="reload", need_admin=True, description=lang('reload_des')) diff --git a/pagermaid/modules/status.py b/pagermaid/modules/status.py index 62b15e2..3dec06d 100644 --- a/pagermaid/modules/status.py +++ b/pagermaid/modules/status.py @@ -19,6 +19,7 @@ from shutil import disk_usage from subprocess import Popen, PIPE from pagermaid import start_time, Config, pgm_version +from pagermaid.common.status import get_bot_uptime from pagermaid.enums import Client, Message from pagermaid.listener import listener from pagermaid.utils import lang, execute @@ -50,27 +51,7 @@ async def status(message: Message): # database # database = lang('status_online') if redis_status() else lang('status_offline') # uptime https://gist.github.com/borgstrom/936ca741e885a1438c374824efb038b3 - time_units = ( - ('%m', 60 * 60 * 24 * 30), - ('%d', 60 * 60 * 24), - ('%H', 60 * 60), - ('%M', 60), - ('%S', 1) - ) - - async def human_time_duration(seconds): - parts = {} - for unit, div in time_units: - amount, seconds = divmod(int(seconds), div) - parts[unit] = str(amount) - time_form = Config.START_FORM - for key, value in parts.items(): - time_form = time_form.replace(key, value) - return time_form - - current_time = datetime.now(timezone.utc) - uptime_sec = (current_time - start_time).total_seconds() - uptime = await human_time_duration(int(uptime_sec)) + uptime = await get_bot_uptime() text = (f"**{lang('status_hint')}** \n" f"{lang('status_name')}: `{uname().node}` \n" f"{lang('status_platform')}: `{platform}` \n" @@ -88,7 +69,7 @@ async def status(message: Message): async def stats(client: Client, message: Message): msg = await message.edit(lang("stats_loading")) a, u, g, s, c, b = 0, 0, 0, 0, 0, 0 - async for dialog in client.get_dialogs(): + for dialog in await client.get_dialogs_list(): chat_type = dialog.chat.type if chat_type == ChatType.BOT: b += 1 diff --git a/pagermaid/modules/system.py b/pagermaid/modules/system.py index 9940f80..7b21758 100644 --- a/pagermaid/modules/system.py +++ b/pagermaid/modules/system.py @@ -1,7 +1,3 @@ -import io -import sys -import traceback - from os.path import exists, sep from sys import exit from platform import node @@ -9,9 +5,9 @@ from getpass import getuser from pyrogram.enums import ParseMode +from pagermaid.common.system import run_eval from pagermaid.listener import listener from pagermaid.enums import Message -from pagermaid.services import bot from pagermaid.utils import attach_log, execute, lang, upload_attachment @@ -72,31 +68,10 @@ async def sh_eval(message: Message): cmd = message.text.split(" ", maxsplit=1)[1] except (IndexError, AssertionError): return await message.edit(lang('eval_need_dev')) - old_stderr = sys.stderr - old_stdout = sys.stdout - redirected_output = sys.stdout = io.StringIO() - redirected_error = sys.stderr = io.StringIO() - stdout, stderr, exc = None, None, None - try: - await aexec(cmd, message, bot) - except Exception: # noqa - exc = traceback.format_exc() - stdout = redirected_output.getvalue() - stderr = redirected_error.getvalue() - sys.stdout = old_stdout - sys.stderr = old_stderr - if exc: - evaluation = exc - elif stderr: - evaluation = stderr - elif stdout: - evaluation = stdout - else: - evaluation = "Success" - final_output = f"**>>>** `{cmd}` \n`{evaluation}`" + final_output = await run_eval(cmd, message) if len(final_output) > 4096: message = await message.edit(f"**>>>** `{cmd}`", parse_mode=ParseMode.MARKDOWN) - await attach_log(evaluation, message.chat.id, "output.log", message.id) + await attach_log(final_output, message.chat.id, "output.log", message.id) else: await message.edit(final_output) @@ -114,18 +89,3 @@ async def send_log(message: Message): thumb=f"pagermaid{sep}assets{sep}logo.jpg", caption=lang("send_log_caption")) await message.safe_delete() - - -async def aexec(code, event, client): - exec( - ( - ( - ("async def __aexec(e, client): " + "\n msg = message = e") - + "\n reply = message.reply_to_message" - ) - + "\n chat = e.chat" - ) - + "".join(f"\n {x}" for x in code.split("\n")) - ) - - return await locals()["__aexec"](event, client) diff --git a/pagermaid/modules/update.py b/pagermaid/modules/update.py index 0868829..b0eaddc 100644 --- a/pagermaid/modules/update.py +++ b/pagermaid/modules/update.py @@ -1,7 +1,8 @@ -from sys import executable, exit +from sys import exit +from pagermaid.common.update import update as update_function from pagermaid.listener import listener -from pagermaid.utils import lang, execute, Message, alias_command +from pagermaid.utils import lang, Message, alias_command @listener(is_plugin=False, outgoing=True, command=alias_command("update"), @@ -9,11 +10,6 @@ from pagermaid.utils import lang, execute, Message, alias_command description=lang('update_des'), parameters="") async def update(message: Message): - await execute('git fetch --all') - if len(message.parameter) > 0: - await execute('git reset --hard origin/master') - await execute('git pull --all') - await execute(f"{executable} -m pip install --upgrade -r requirements.txt") - await execute(f"{executable} -m pip install -r requirements.txt") + await update_function(len(message.parameter) > 0) await message.edit(lang('update_success')) exit(0) diff --git a/pagermaid/modules/web.py b/pagermaid/modules/web.py new file mode 100644 index 0000000..177817b --- /dev/null +++ b/pagermaid/modules/web.py @@ -0,0 +1,15 @@ +from pagermaid.config import Config +from pagermaid.hook import Hook +from pagermaid.services import bot + + +@Hook.on_startup() +async def init_web(): + if not Config.WEB_ENABLE: + return + import uvicorn + from pagermaid.web import app, init_web + + init_web() + server = uvicorn.Server(config=uvicorn.Config(app, host=Config.WEB_HOST, port=Config.WEB_PORT)) + bot.loop.create_task(server.serve()) diff --git a/pagermaid/single_utils.py b/pagermaid/single_utils.py index 90f6877..02dbdce 100644 --- a/pagermaid/single_utils.py +++ b/pagermaid/single_utils.py @@ -6,7 +6,7 @@ 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 +from pyrogram.types import Chat as OldChat, Message as OldMessage, Dialog from pyromod.utils.conversation import Conversation from pyromod.utils.errors import AlreadyInConversationError, TimeoutConversationError, ListenerCanceled @@ -70,6 +70,9 @@ class Client(OldClient): once_timeout: int = 60, filters=None) -> Optional[Conversation]: """ Initialize a conversation with the given chat_id. """ + async def get_dialogs_list(self) -> List[Dialog]: + """ Get a list of all dialogs. """ + class Chat(OldChat): is_forum: Optional[bool] = None diff --git a/pagermaid/utils.py b/pagermaid/utils.py index d9d545c..af590b3 100644 --- a/pagermaid/utils.py +++ b/pagermaid/utils.py @@ -77,6 +77,7 @@ async def execute(command, pass_error=True): """ Executes command and returns output, with the option of enabling stderr. """ executor = await create_subprocess_shell( command, + loop=bot.loop, stdout=PIPE, stderr=PIPE, stdin=PIPE diff --git a/pagermaid/web/__init__.py b/pagermaid/web/__init__.py new file mode 100644 index 0000000..96c6ec3 --- /dev/null +++ b/pagermaid/web/__init__.py @@ -0,0 +1,50 @@ +from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from starlette.responses import RedirectResponse + +from .api import base_api_router +from .pages import admin_app, login_page + +requestAdaptor = ''' +requestAdaptor(api) { + api.headers["token"] = localStorage.getItem("token"); + return api; +}, +''' +responseAdaptor = ''' +responseAdaptor(api, payload, query, request, response) { + if (response.data.detail == '登录验证失败或已失效,请重新登录') { + window.location.href = '/login' + window.localStorage.clear() + window.sessionStorage.clear() + window.alert('登录验证失败或已失效,请重新登录') + } + return payload +}, +''' +icon_path = 'https://xtaolabs.com/pagermaid-logo.png' +app: FastAPI = FastAPI() + + +def init_web(): + app.include_router(base_api_router) + + @app.get('/', response_class=RedirectResponse) + async def index(): + return '/admin' + + @app.get('/admin', response_class=HTMLResponse) + async def admin(): + return admin_app.render( + site_title='PagerMaid-Pyro 后台管理', + site_icon=icon_path, + requestAdaptor=requestAdaptor, + responseAdaptor=responseAdaptor + ) + + @app.get('/login', response_class=HTMLResponse) + async def login(): + return login_page.render( + site_title='登录 | PagerMaid-Pyro 后台管理', + site_icon=icon_path, + ) diff --git a/pagermaid/web/api/__init__.py b/pagermaid/web/api/__init__.py new file mode 100644 index 0000000..1168a39 --- /dev/null +++ b/pagermaid/web/api/__init__.py @@ -0,0 +1,18 @@ +from fastapi import APIRouter + +from pagermaid.web.api.utils import authentication +from pagermaid.web.api.bot_info import route as bot_info_route +from pagermaid.web.api.command_alias import route as command_alias_route +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 + +base_api_router = APIRouter(prefix='/pagermaid/api') + +base_api_router.include_router(plugin_route) +base_api_router.include_router(bot_info_route) +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) diff --git a/pagermaid/web/api/bot_info.py b/pagermaid/web/api/bot_info.py new file mode 100644 index 0000000..edf4a09 --- /dev/null +++ b/pagermaid/web/api/bot_info.py @@ -0,0 +1,24 @@ +import os +import signal + +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from pagermaid.web.api.utils import authentication +from pagermaid.common.update import update + +route = APIRouter() + + +@route.post('/bot_update', response_class=JSONResponse, dependencies=[authentication()]) +async def bot_update(): + await update() + return { + "status": 0, + "msg": "更新成功,请重启 PagerMaid-Pyro 以应用更新。" + } + + +@route.post('/bot_restart', response_class=JSONResponse, dependencies=[authentication()]) +async def bot_restart(): + os.kill(os.getppid(), signal.SIGINT) diff --git a/pagermaid/web/api/command_alias.py b/pagermaid/web/api/command_alias.py new file mode 100644 index 0000000..6dd1e2f --- /dev/null +++ b/pagermaid/web/api/command_alias.py @@ -0,0 +1,47 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from pagermaid.common.alias import AliasManager +from pagermaid.web.api.utils import authentication + +route = APIRouter() + + +@route.get('/command_alias', response_class=JSONResponse, dependencies=[authentication()]) +async def get_command_alias(): + alias = AliasManager() + return { + 'status': 0, + 'msg': 'ok', + 'data': { + 'items': alias.get_all_alias_dict(), + } + } + + +@route.post('/command_alias', response_class=JSONResponse, dependencies=[authentication()]) +async def add_command_alias(data: dict): + data = data['items'] + try: + await AliasManager.save_from_web(data) + return { + 'status': 0, + 'msg': '命令别名保存成功' + } + except Exception: + return { + 'status': 1, + 'msg': '命令别名保存失败' + } + + +@route.get('/test_command_alias', response_class=JSONResponse, dependencies=[authentication()]) +async def test_command_alias(message: str): + alias = AliasManager() + return { + 'status': 0, + 'msg': '测试成功', + 'data': { + 'new_msg': alias.test_alias(message), + } + } diff --git a/pagermaid/web/api/ignore_groups.py b/pagermaid/web/api/ignore_groups.py new file mode 100644 index 0000000..84df038 --- /dev/null +++ b/pagermaid/web/api/ignore_groups.py @@ -0,0 +1,45 @@ +from fastapi import APIRouter +from starlette.responses import JSONResponse + +from pagermaid.common.ignore import ignore_groups_manager, get_group_list +from pagermaid.web.api import authentication + +route = APIRouter() + + +@route.get("/get_ignore_group_list", response_class=JSONResponse, dependencies=[authentication()]) +async def get_ignore_group_list(): + try: + groups = [] + for data in await get_group_list(): + data["status"] = ignore_groups_manager.check_id(data["id"]) + groups.append(data) + return { + 'status': 0, + 'msg': 'ok', + 'data': { + 'groups': groups + } + } + except BaseException: + return { + 'status': -100, + 'msg': '获取群组列表失败' + } + + +@route.post('/set_ignore_group_status', response_class=JSONResponse, dependencies=[authentication()]) +async def set_ignore_group_status(data: dict): + cid: int = data.get('id') + status: bool = data.get('status') + if status: + ignore_groups_manager.add_id(cid) + else: + ignore_groups_manager.del_id(cid) + return {'status': 0, 'msg': f'成功{"忽略" if status else "取消忽略"} {cid}'} + + +@route.post('/clear_ignore_group', response_class=JSONResponse, dependencies=[authentication()]) +async def clear_ignore_group(): + ignore_groups_manager.clear_subs() + return {'status': 0, 'msg': '成功清空忽略列表'} diff --git a/pagermaid/web/api/login.py b/pagermaid/web/api/login.py new file mode 100644 index 0000000..03691fe --- /dev/null +++ b/pagermaid/web/api/login.py @@ -0,0 +1,30 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse +from pydantic import BaseModel + +from pagermaid.web.api.utils import create_token +from pagermaid.config import Config + + +class UserModel(BaseModel): + password: str + + +route = APIRouter() + + +@route.post('/login', response_class=JSONResponse) +async def login(user: UserModel): + if user.password != Config.WEB_SECRET_KEY: + return { + "status": -100, + "msg": "登录失败,请重新输入密钥" + } + token = create_token() + return { + "status": 0, + "msg": "登录成功", + "data": { + "token": token + } + } diff --git a/pagermaid/web/api/plugin.py b/pagermaid/web/api/plugin.py new file mode 100644 index 0000000..8eb7a28 --- /dev/null +++ b/pagermaid/web/api/plugin.py @@ -0,0 +1,65 @@ +from fastapi import APIRouter +from fastapi.responses import JSONResponse + +from pagermaid.common.plugin import plugin_manager +from pagermaid.common.reload import reload_all +from pagermaid.web.api.utils import authentication + +route = APIRouter() + + +@route.get('/get_local_plugins', response_class=JSONResponse, dependencies=[authentication()]) +async def get_local_plugins(): + plugins = [i.dict() for i in plugin_manager.plugins] + plugins.sort(key=lambda x: x['name']) + return { + 'status': 0, + 'msg': 'ok', + 'data': { + 'rows': plugins, + 'total': len(plugins) + } + } + + +@route.post('/set_local_plugin_status', response_class=JSONResponse, dependencies=[authentication()]) +async def set_local_plugin_status(data: dict): + module_name: str = data.get('plugin') + status: bool = data.get('status') + if not (plugin := plugin_manager.get_local_plugin(module_name)): + return {'status': 1, 'msg': f'插件 {module_name} 不存在'} + if status: + plugin.enable() + else: + plugin.disable() + await reload_all() + return {'status': 0, 'msg': f'成功{"开启" if status else "关闭"} {module_name}'} + + +@route.get('/get_remote_plugins', response_class=JSONResponse, dependencies=[authentication()]) +async def get_remote_plugins(): + await plugin_manager.load_remote_plugins() + plugins = [i.dict() for i in plugin_manager.remote_plugins] + plugins.sort(key=lambda x: x['name']) + return { + 'status': 0, + 'msg': 'ok', + 'data': { + 'rows': plugins, + 'total': len(plugins) + } + } + + +@route.post('/set_remote_plugin_status', response_class=JSONResponse, dependencies=[authentication()]) +async def set_remote_plugin_status(data: dict): + module_name: str = data.get('plugin') + status: bool = data.get('status') + if not plugin_manager.get_remote_plugin(module_name): + return {'status': 1, 'msg': f'插件 {module_name} 不存在'} + if status: + await plugin_manager.install_remote_plugin(module_name) + else: + plugin_manager.remove_plugin(module_name) + await reload_all() + return {'status': 0, 'msg': f'成功{"开启" if status else "关闭"} {module_name}'} diff --git a/pagermaid/web/api/status.py b/pagermaid/web/api/status.py new file mode 100644 index 0000000..e2ab6a2 --- /dev/null +++ b/pagermaid/web/api/status.py @@ -0,0 +1,69 @@ +import asyncio +from typing import Union, Optional + +from fastapi import APIRouter, Header +from fastapi.responses import JSONResponse, StreamingResponse + +from pagermaid.common.status import get_status +from pagermaid.common.system import run_eval +from pagermaid.config import Config +from pagermaid.utils import execute +from pagermaid.web.api.utils import authentication + +route = APIRouter() + + +@route.get('/log') +async def get_log(token: Optional[str] = Header(...), num: Union[int, str] = 100): + if token != Config.WEB_SECRET_KEY: + return "非法请求" + try: + num = int(num) + except ValueError: + num = 100 + + async def streaming_logs(): + with open("pagermaid.log.txt", "r", encoding="utf-8") as f: + for log in f.readlines()[-num:]: + yield log + await asyncio.sleep(0.02) + + return StreamingResponse(streaming_logs()) + + +@route.get('/run_eval') +async def run_cmd(token: Optional[str] = Header(...), cmd: str = ''): + if token != Config.WEB_SECRET_KEY: + return "非法请求" + + async def run_cmd_func(): + result = (await run_eval(cmd, only_result=True)).split("\n") + for i in result: + yield i + "\n" + await asyncio.sleep(0.02) + + return StreamingResponse(run_cmd_func()) if cmd else "无效命令" + + +@route.get('/run_sh') +async def run_sh(token: Optional[str] = Header(...), cmd: str = ''): + if token != Config.WEB_SECRET_KEY: + return "非法请求" + + async def run_sh_func(): + result = (await execute(cmd)).split("\n") + for i in result: + yield i + "\n" + await asyncio.sleep(0.02) + + return StreamingResponse(run_sh_func()) if cmd else "无效命令" + + +@route.get('/status', response_class=JSONResponse, dependencies=[authentication()]) +async def status(): + return (await get_status()).dict() + + +@route.get('/status', response_class=JSONResponse, dependencies=[authentication()]) +async def status(): + return (await get_status()).dict() diff --git a/pagermaid/web/api/utils.py b/pagermaid/web/api/utils.py new file mode 100644 index 0000000..c347758 --- /dev/null +++ b/pagermaid/web/api/utils.py @@ -0,0 +1,27 @@ +import datetime +from typing import Optional + +from fastapi import Header, HTTPException, Depends +from jose import jwt + +from pagermaid.config import Config + +ALGORITHM = 'HS256' +TOKEN_EXPIRE_MINUTES = 30 + + +def authentication(): + def inner(token: Optional[str] = Header(...)): + try: + jwt.decode(token, Config.WEB_SECRET_KEY, algorithms=ALGORITHM) + except (jwt.JWTError, jwt.ExpiredSignatureError, AttributeError): + raise HTTPException(status_code=400, detail='登录验证失败或已失效,请重新登录') + + return Depends(inner) + + +def create_token(): + data = { + "exp": datetime.datetime.now(datetime.timezone.utc) + datetime.timedelta(minutes=TOKEN_EXPIRE_MINUTES), + } + return jwt.encode(data, Config.WEB_SECRET_KEY, algorithm=ALGORITHM) diff --git a/pagermaid/web/html/__init__.py b/pagermaid/web/html/__init__.py new file mode 100644 index 0000000..a513237 --- /dev/null +++ b/pagermaid/web/html/__init__.py @@ -0,0 +1,24 @@ +from pathlib import Path + +html_base_path = Path(__file__).parent + + +def get_html(path: Path) -> str: + """获取 HTML 模板。""" + with open(path, 'r', encoding='utf-8') as f: + return f.read() + + +def get_logo() -> str: + """获取 logo。""" + return get_html(html_base_path / 'logo.html') + + +def get_github_logo() -> str: + """获取 github logo。""" + return get_html(html_base_path / 'github_logo.html') + + +def get_footer() -> str: + """获取 footer。""" + return get_html(html_base_path / 'footer.html') diff --git a/pagermaid/web/html/footer.html b/pagermaid/web/html/footer.html new file mode 100644 index 0000000..246f1ad --- /dev/null +++ b/pagermaid/web/html/footer.html @@ -0,0 +1,9 @@ +
+ Copyright © 2023 + + PagerMaid-Pyro + X + + amis v2.2.0 + +
\ No newline at end of file diff --git a/pagermaid/web/html/github_logo.html b/pagermaid/web/html/github_logo.html new file mode 100644 index 0000000..7d7340b --- /dev/null +++ b/pagermaid/web/html/github_logo.html @@ -0,0 +1,8 @@ +
+
+
+ + + +
+
\ No newline at end of file diff --git a/pagermaid/web/html/logo.html b/pagermaid/web/html/logo.html new file mode 100644 index 0000000..5497775 --- /dev/null +++ b/pagermaid/web/html/logo.html @@ -0,0 +1,14 @@ +

+ + pagermaid + +

+

PagerMaid-Pyro 控制台

+
+ + Github仓库   ·   + 文档 +
+
\ No newline at end of file diff --git a/pagermaid/web/pages/__init__.py b/pagermaid/web/pages/__init__.py new file mode 100644 index 0000000..cc8c9fe --- /dev/null +++ b/pagermaid/web/pages/__init__.py @@ -0,0 +1,2 @@ +from .login import login_page +from .main import admin_app, blank_page diff --git a/pagermaid/web/pages/command_alias.py b/pagermaid/web/pages/command_alias.py new file mode 100644 index 0000000..be9c868 --- /dev/null +++ b/pagermaid/web/pages/command_alias.py @@ -0,0 +1,50 @@ +from amis import Form, InputSubForm, InputText, Static, Alert, PageSchema, Page + +main_form = Form( + title='命令别名', + initApi='get:/pagermaid/api/command_alias', + api='post:/pagermaid/api/command_alias', + submitText='保存', + body=[ + InputSubForm( + name='items', + label='已设置的命令别名', + multiple=True, + btnLabel='${alias} >> ${command}', + draggable=True, + addable=True, + removable=True, + addButtonText='添加命令别名', + showErrorMsg=False, + form=Form( + title='命令别名', + body=[ + InputText(name='alias', label='命令别名', required=True), + InputText(name='command', label='原命令', required=True), + ] + ) + ) + ] +) + +test_form = Form( + title='测试', + api='get:/pagermaid/api/test_command_alias?message=${message}', + submitText='测试', + body=[ + InputText(name='message', label='测试消息(无需输入逗号前缀)', required=True), + Static(className='text-red-600', name='new_msg', label='命令别名修改后消息', + visibleOn="typeof data.new_msg !== 'undefined'") + ] +) + +tips = Alert(level='info') + +page = PageSchema( + url='/bot_config/command_alias', + icon='fa fa-link', label='命令别名', + schema=Page( + title='', + body=[tips, main_form, test_form] + ) +) diff --git a/pagermaid/web/pages/home_page.py b/pagermaid/web/pages/home_page.py new file mode 100644 index 0000000..97fafe0 --- /dev/null +++ b/pagermaid/web/pages/home_page.py @@ -0,0 +1,178 @@ +from amis import Page, PageSchema, Html, Property, Service, Flex, ActionType, LevelEnum, Divider, Log, Alert, Form, \ + Dialog, Select, Group, InputText, DisplayModeEnum, Horizontal + +from pagermaid.config import Config +from pagermaid.web.html import get_logo + +logo = Html(html=get_logo()) +select_log_num = Select( + label='日志数量', + name='log_num', + value=100, + options=[ + { + 'label': 100, + 'value': 100 + }, + { + 'label': 200, + 'value': 200 + }, + { + 'label': 300, + 'value': 300 + }, + { + 'label': 400, + 'value': 400 + }, + { + 'label': 500, + 'value': 500 + } + ] +) + +log_page = Log( + autoScroll=True, + placeholder='暂无日志数据...', + operation=['stop', 'showLineNumber', 'filter'], + source={ + 'method': 'get', + 'url': '/pagermaid/api/log?num=${log_num | raw}', + 'headers': { + 'token': Config.WEB_SECRET_KEY + } + } +) + +cmd_input = Form( + mode=DisplayModeEnum.horizontal, + horizontal=Horizontal(left=0), + wrapWithPanel=False, + body=[ + InputText(name='command', required=True, clearable=True, addOn=ActionType.Dialog( + label='执行', + level=LevelEnum.primary, + dialog=Dialog( + title='命令执行结果', + size='xl', + body=Log( + autoScroll=True, + placeholder='执行命令中,请稍候...', + operation=['stop', 'showLineNumber', 'filter'], + source={ + 'method': 'get', + 'url': '/pagermaid/api/run_sh?cmd=${command | raw}', + 'headers': { + 'token': Config.WEB_SECRET_KEY + } + }), + ) + )) + ] +) +eval_input = Form( + mode=DisplayModeEnum.horizontal, + horizontal=Horizontal(left=0), + wrapWithPanel=False, + body=[ + InputText(name='command', required=True, clearable=True, addOn=ActionType.Dialog( + label='执行', + level=LevelEnum.primary, + dialog=Dialog( + title='命令执行结果', + size='xl', + body=Log( + autoScroll=True, + placeholder='执行命令中,请稍候...', + operation=['stop', 'showLineNumber', 'filter'], + source={ + 'method': 'get', + 'url': '/pagermaid/api/run_eval?cmd=${command | raw}', + 'headers': { + 'token': Config.WEB_SECRET_KEY + } + }), + ) + )) + ] +) + +operation_button = Flex(justify='center', items=[ + ActionType.Ajax( + label='更新', + api='/pagermaid/api/bot_update', + confirmText='该操作会更新 PagerMaid-Pyro ,请在更新完成后手动重启,请确认执行该操作', + level=LevelEnum.info + ), + ActionType.Ajax( + label='重启', + className='m-l', + api='/pagermaid/api/bot_restart', + confirmText='该操作会重启 PagerMaid-Pyro ,请耐心等待重启', + level=LevelEnum.danger + ), + ActionType.Dialog( + label='日志', + className='m-l', + level=LevelEnum.primary, + dialog=Dialog(title='查看日志', + size='xl', + actions=[], + body=[ + Alert(level=LevelEnum.info, + body='查看最近最多500条日志,不会自动刷新,需要手动点击两次"暂停键"来进行刷新。'), + Form( + body=[Group(body=[select_log_num]), log_page] + )]) + ), + ActionType.Dialog( + label='shell', + className='m-l', + level=LevelEnum.warning, + dialog=Dialog(title='shell', + size='lg', + actions=[], + body=[cmd_input]) + ), + ActionType.Dialog( + label='eval', + className='m-l', + level=LevelEnum.warning, + dialog=Dialog(title='eval', + size='lg', + actions=[], + body=[eval_input]) + ) +]) + +status = Service( + api='/pagermaid/api/status', + body=Property( + title='运行信息', + column=2, + items=[ + Property.Item( + label='Bot 运行时间', + content='${run_time}' + ), + Property.Item( + label='CPU占用率', + content='${cpu_percent}' + ), + Property.Item( + label='RAM占用率', + content='${ram_percent}' + ), + Property.Item( + label='SWAP占用率', + content='${swap_percent}', + span=2 + ), + ] + ) +) + +page_detail = Page(title='', body=[logo, operation_button, Divider(), status]) +page = PageSchema(url='/home', label='首页', icon='fa fa-home', isDefaultPage=True, schema=page_detail) diff --git a/pagermaid/web/pages/ignore_groups.py b/pagermaid/web/pages/ignore_groups.py new file mode 100644 index 0000000..d2fbfd4 --- /dev/null +++ b/pagermaid/web/pages/ignore_groups.py @@ -0,0 +1,63 @@ +from amis import InputText, Switch, Card, Tpl, CardsCRUD, PageSchema, Page, Button, Select + +card = Card( + header=Card.Header( + title='$title', + description='$id', + avatarText='$title', + avatarTextClassName='overflow-hidden' + ), + actions=[], + toolbar=[ + Switch( + name='enable', + value='${status}', + onText='已忽略', + offText='未忽略', + onEvent={ + 'change': { + 'actions': { + 'actionType': 'ajax', + 'args': { + 'api': { + 'url': '/pagermaid/api/set_ignore_group_status', + 'method': 'post' + }, + 'messages': { + 'success': '成功${IF(event.data.value, "忽略", "取消忽略")}了 ${title}', + 'failed': '操作失败' + }, + 'status': '${event.data.value}', + 'id': '${id}' + } + } + } + } + ) + ] +) +cards_curd = CardsCRUD( + mode='cards', + title='', + syncLocation=False, + api='/pagermaid/api/get_ignore_group_list', + loadDataOnce=True, + source='${groups | filter:title:match:keywords_name}', + filter={ + 'body': [ + InputText(name='keywords_name', label='群组名') + ] + }, + perPage=12, + autoJumpToTopOnPagerChange=True, + placeholder='群组列表为空', + footerToolbar=['switch-per-page', 'pagination'], + columnsCount=3, + card=card +) +page = PageSchema( + url='/bot_config/ignore_groups', + icon='fa fa-ban', + label='忽略群组', + schema=Page(title='忽略群组', subTitle="忽略后,Bot 不再响应指定群组的消息(群组列表将会缓存一小时)", body=cards_curd) +) diff --git a/pagermaid/web/pages/login.py b/pagermaid/web/pages/login.py new file mode 100644 index 0000000..857d5dd --- /dev/null +++ b/pagermaid/web/pages/login.py @@ -0,0 +1,32 @@ +from amis import Form, InputPassword, DisplayModeEnum, Horizontal, Remark, Html, Page, AmisAPI, Wrapper + +from pagermaid.web.html import get_logo + +logo = Html(html=get_logo()) +login_api = AmisAPI( + url='/pagermaid/api/login', + method='post', + adaptor=''' + if (payload.status == 0) { + localStorage.setItem("token", payload.data.token); + } + return payload; + ''' +) + +login_form = Form( + api=login_api, + title='', + body=[ + InputPassword( + name='password', + label='密码', + labelRemark=Remark(shape='circle', content='登录密码') + ), + ], + mode=DisplayModeEnum.horizontal, + horizontal=Horizontal(left=3, right=9, offset=5), + redirect='/admin', +) +body = Wrapper(className='w-2/5 mx-auto my-0 m:w-full', body=login_form) +login_page = Page(title='', body=[logo, body]) diff --git a/pagermaid/web/pages/main.py b/pagermaid/web/pages/main.py new file mode 100644 index 0000000..1adf313 --- /dev/null +++ b/pagermaid/web/pages/main.py @@ -0,0 +1,32 @@ +from amis import App, PageSchema, Tpl, Page, Flex + +from pagermaid.web.html import get_footer, get_github_logo +from pagermaid.web.pages.command_alias import page as command_alias_page +from pagermaid.web.pages.ignore_groups import page as ignore_groups_page +from pagermaid.web.pages.home_page import page as home_page +from pagermaid.web.pages.plugin_local_manage import page as plugin_local_manage_page +from pagermaid.web.pages.plugin_remote_manage import page as plugin_remote_manage_page + +github_logo = Tpl( + className='w-full', + tpl=get_github_logo(), +) +header = Flex(className='w-full', justify='flex-end', alignItems='flex-end', items=[github_logo]) +admin_app = App( + brandName='pagermaid', + logo='https://xtaolabs.com/pagermaid-logo.png', + header=header, + pages=[ + { + 'children': [ + home_page, + PageSchema(label='Bot 设置', icon='fa fa-wrench', + children=[command_alias_page, ignore_groups_page]), + PageSchema(label='插件管理', icon='fa fa-cube', + children=[plugin_local_manage_page, plugin_remote_manage_page]), + ] + } + ], + footer=get_footer(), +) +blank_page = Page(title='PagerMaid-Pyro 404', body='404') diff --git a/pagermaid/web/pages/plugin_local_manage.py b/pagermaid/web/pages/plugin_local_manage.py new file mode 100644 index 0000000..aabbe5a --- /dev/null +++ b/pagermaid/web/pages/plugin_local_manage.py @@ -0,0 +1,64 @@ +from amis import InputText, Switch, Card, CardsCRUD, PageSchema, Page + +card = Card( + header=Card.Header( + title='$name', + avatarText='$name', + avatarTextClassName='overflow-hidden' + ), + actions=[], + toolbar=[ + Switch( + name='enable', + value='${status}', + onText='启用', + offText='禁用', + onEvent={ + 'change': { + 'actions': [ + { + 'actionType': 'ajax', + 'args': { + 'api': { + 'url': '/pagermaid/api/set_local_plugin_status', + 'method': 'post' + }, + 'messages': { + 'success': '成功${IF(event.data.value, "开启", "禁用")}了 ${name}', + 'failed': '操作失败' + }, + 'status': '${event.data.value}', + 'plugin': '${name}' + } + }, + ] + } + } + ) + ] +) +cards_curd = CardsCRUD( + mode='cards', + title='', + syncLocation=False, + api='/pagermaid/api/get_local_plugins', + loadDataOnce=True, + source='${rows | filter:name:match:keywords_name}', + filter={ + 'body': [ + InputText(name='keywords_name', label='插件名') + ] + }, + perPage=12, + autoJumpToTopOnPagerChange=True, + placeholder='暂无插件信息', + footerToolbar=['switch-per-page', 'pagination'], + columnsCount=3, + card=card +) +page = PageSchema( + url='/plugins/local', + icon='fa fa-database', + label='本地插件管理', + schema=Page(title='本地插件管理', body=cards_curd) +) diff --git a/pagermaid/web/pages/plugin_remote_manage.py b/pagermaid/web/pages/plugin_remote_manage.py new file mode 100644 index 0000000..c702744 --- /dev/null +++ b/pagermaid/web/pages/plugin_remote_manage.py @@ -0,0 +1,64 @@ +from amis import InputText, Switch, Card, Tpl, CardsCRUD, PageSchema, Page, Button + +card = Card( + header=Card.Header( + title='$name', + description='$des', + avatarText='$name', + avatarTextClassName='overflow-hidden' + ), + actions=[], + toolbar=[ + Switch( + name='enable', + value='${status}', + onText='已安装', + offText='未安装', + onEvent={ + 'change': { + 'actions': { + 'actionType': 'ajax', + 'args': { + 'api': { + 'url': '/pagermaid/api/set_remote_plugin_status', + 'method': 'post' + }, + 'messages': { + 'success': '成功${IF(event.data.value, "安装", "卸载")}了 ${name}', + 'failed': '操作失败' + }, + 'status': '${event.data.value}', + 'plugin': '${name}' + } + } + } + } + ) + ] +) +cards_curd = CardsCRUD( + mode='cards', + title='', + syncLocation=False, + api='/pagermaid/api/get_remote_plugins', + loadDataOnce=True, + source='${rows | filter:name:match:keywords_name | filter:des:match:keywords_description}', + filter={ + 'body': [ + InputText(name='keywords_name', label='插件名'), + InputText(name='keywords_description', label='插件描述') + ] + }, + perPage=12, + autoJumpToTopOnPagerChange=True, + placeholder='暂无插件信息', + footerToolbar=['switch-per-page', 'pagination'], + columnsCount=3, + card=card +) +page = PageSchema( + url='/plugins/remote', + icon='fa fa-cloud-download', + label='插件仓库', + schema=Page(title='插件仓库', body=cards_curd) +) diff --git a/pyromod/listen/listen.py b/pyromod/listen/listen.py index 8abef9a..8f41e70 100644 --- a/pyromod/listen/listen.py +++ b/pyromod/listen/listen.py @@ -29,6 +29,7 @@ from pyrogram.enums import ChatType from pagermaid.single_utils import get_sudo_list, Message from pagermaid.scheduler import add_delete_message_job +from ..methods.get_dialogs_list import get_dialogs_list as get_dialogs_list_func from ..utils import patch, patchable from ..utils.conversation import Conversation @@ -123,6 +124,10 @@ class Client: ) return await self.oldread_chat_history(chat_id, max_id) # noqa + @patchable + async def get_dialogs_list(self: "Client"): + return await get_dialogs_list_func(self) + @patch(pyrogram.handlers.message_handler.MessageHandler) class MessageHandler: diff --git a/pyromod/methods/get_dialogs_list.py b/pyromod/methods/get_dialogs_list.py new file mode 100644 index 0000000..1890add --- /dev/null +++ b/pyromod/methods/get_dialogs_list.py @@ -0,0 +1,16 @@ +from datetime import timedelta + +from pyrogram import Client +from pyrogram.enums import ChatType + +from pagermaid.common.cache import cache + + +@cache(ttl=timedelta(hours=1)) +async def get_dialogs_list(client: Client): + dialogs = [] + async for dialog in client.get_dialogs(): + dialogs.append(dialog) + if dialog.chat.type == ChatType.SUPERGROUP: + break + return dialogs diff --git a/requirements.txt b/requirements.txt index 72e974c..f86db1f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,10 +5,16 @@ pytz>=2021.3 PyYAML>=6.0 coloredlogs>=15.0.1 psutil>=5.8.0 -httpx -apscheduler -sqlitedict +httpx~=0.23.3 +apscheduler~=3.9.1.post1 +sqlitedict~=2.1.0 casbin==1.17.5 -sentry-sdk==1.13.0 +sentry-sdk==1.14.0 PyQRCode>=1.2.1 PyPng +fastapi~=0.89.1 +amis-python +python-jose +uvicorn +pydantic~=1.10.4 +starlette~=0.22.0