🔖 Update to v1.2.30

support web
This commit is contained in:
xtaodada 2023-02-01 00:24:56 +08:00
parent 9211a7d843
commit 29ae48d1ae
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
45 changed files with 1506 additions and 254 deletions

View File

@ -12,7 +12,7 @@ from pagermaid.scheduler import scheduler
import pyromod.listen import pyromod.listen
from pyrogram import Client from pyrogram import Client
pgm_version = "1.2.26" pgm_version = "1.2.30"
CMD_LIST = {} CMD_LIST = {}
module_dir = __path__[0] module_dir = __path__[0]
working_dir = getcwd() working_dir = getcwd()

View File

@ -6,6 +6,7 @@ from pyrogram import idle
from pyrogram.errors import AuthKeyUnregistered from pyrogram.errors import AuthKeyUnregistered
from pagermaid import bot, logs, working_dir from pagermaid import bot, logs, working_dir
from pagermaid.common.plugin import plugin_manager
from pagermaid.hook import Hook from pagermaid.hook import Hook
from pagermaid.modules import module_list, plugin_list from pagermaid.modules import module_list, plugin_list
from pagermaid.single_utils import safe_remove from pagermaid.single_utils import safe_remove
@ -41,6 +42,7 @@ async def main():
except BaseException as exception: except BaseException as exception:
logs.info(f"{lang('module')} {plugin_name} {lang('error')}: {exception}") logs.info(f"{lang('module')} {plugin_name} {lang('error')}: {exception}")
plugin_list.remove(plugin_name) plugin_list.remove(plugin_name)
plugin_manager.load_local_plugins()
await process_exit(start=True, _client=bot) await process_exit(start=True, _client=bot)
logs.info(lang('start')) logs.info(lang('start'))

66
pagermaid/common/alias.py Normal file
View File

@ -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)

35
pagermaid/common/cache.py Normal file
View File

@ -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

View File

@ -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 []

181
pagermaid/common/plugin.py Normal file
View File

@ -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()

View File

@ -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()

View File

@ -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}%',
)

View File

@ -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)

View File

@ -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")

View File

@ -98,6 +98,11 @@ class Config:
alias_dict = load_json(f) alias_dict = load_json(f)
except Exception as e: except Exception as e:
alias_dict = {} 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: except ValueError as e:
print(e) print(e)
sys.exit(1) sys.exit(1)

View File

@ -15,6 +15,7 @@ from pyrogram.errors.exceptions.bad_request_400 import (
from pyrogram.handlers import MessageHandler, EditedMessageHandler from pyrogram.handlers import MessageHandler, EditedMessageHandler
from pagermaid import help_messages, logs, Config, bot, read_context, all_permissions 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.group_manager import Permission
from pagermaid.inject import inject from pagermaid.inject import inject
from pagermaid.single_utils import Message, AlreadyInConversationError, TimeoutConversationError, ListenerCanceled from pagermaid.single_utils import Message, AlreadyInConversationError, TimeoutConversationError, ListenerCanceled
@ -114,6 +115,14 @@ def listener(**args):
async def handler(client: Client, message: Message): async def handler(client: Client, message: Message):
try: try:
# ignore
try:
if ignore_groups_manager.check_id(message.chat.id):
raise ContinuePropagation
except ContinuePropagation:
raise ContinuePropagation
except BaseException:
pass
try: try:
parameter = message.matches[0].group(2).split(" ") parameter = message.matches[0].group(2).split(" ")
if parameter == [""]: if parameter == [""]:
@ -229,6 +238,14 @@ def raw_listener(filter_s):
def decorator(function): def decorator(function):
async def handler(client, message): 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 # solve same process
async with _lock: async with _lock:
if (message.chat.id, message.id) in read_context: if (message.chat.id, message.id) in read_context:

View File

@ -6,8 +6,9 @@ from os import listdir, sep
from pyrogram.enums import ParseMode from pyrogram.enums import ParseMode
from pagermaid import help_messages, Config from pagermaid import help_messages, Config
from pagermaid.common.alias import AliasManager
from pagermaid.group_manager import enforce_permission 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.utils import lang, Message, from_self, from_msg_get_sudo_uid
from pagermaid.listener import listener from pagermaid.listener import listener
@ -120,31 +121,18 @@ async def lang_change(message: Message):
description=lang('alias_des'), description=lang('alias_des'),
parameters='{list|del|set} <source> <to>') parameters='{list|del|set} <source> <to>')
async def alias_commands(message: Message): async def alias_commands(message: Message):
source_commands = [] alias_manager = AliasManager()
to_commands = []
texts = []
for key, value in Config.alias_dict.items():
source_commands.append(key)
to_commands.append(value)
if len(message.parameter) == 0: if len(message.parameter) == 0:
await message.edit(lang('arg_error')) await message.edit(lang('arg_error'))
return
elif len(message.parameter) == 1: elif len(message.parameter) == 1:
if source_commands: if alias_manager.alias_list:
texts.extend( await message.edit(lang('alias_list') + '\n\n' + alias_manager.get_all_alias_text())
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))
else: else:
await message.edit(lang('alias_no')) await message.edit(lang('alias_no'))
elif len(message.parameter) == 2: elif len(message.parameter) == 2:
source_command = message.parameter[1] source_command = message.parameter[1]
try: try:
del Config.alias_dict[source_command] alias_manager.delete_alias(source_command)
with open(f"data{sep}alias.json", 'w', encoding="utf-8") as f:
json_dump(Config.alias_dict, f)
await message.edit(lang('alias_success')) await message.edit(lang('alias_success'))
await reload_all() await reload_all()
except KeyError: except KeyError:
@ -156,8 +144,6 @@ async def alias_commands(message: Message):
if to_command in help_messages: if to_command in help_messages:
await message.edit(lang('alias_exist')) await message.edit(lang('alias_exist'))
return return
Config.alias_dict[source_command] = to_command alias_manager.add_alias(source_command, to_command)
with open(f"data{sep}alias.json", 'w', encoding="utf-8") as f:
json_dump(Config.alias_dict, f)
await message.edit(lang('alias_success')) await message.edit(lang('alias_success'))
await reload_all() await reload_all()

View File

@ -2,20 +2,18 @@
import contextlib import contextlib
import json 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 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.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 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): def remove_plugin(name):
@ -33,14 +31,6 @@ def move_plugin(file_path):
move(file_path, plugin_directory) 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): def update_version(plugin_name, version):
plugin_directory = f"{working_dir}{sep}plugins{sep}" plugin_directory = f"{working_dir}{sep}plugins{sep}"
with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: 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 log(f"{lang('apt_install_success')} {path.basename(file_path)[:-3]}.")
await reload_all() await reload_all()
elif len(message.parameter) >= 2: elif len(message.parameter) >= 2:
await plugin_manager.load_remote_plugins()
process_list = message.parameter process_list = message.parameter
message = await message.edit(lang('apt_processing')) message = await message.edit(lang('apt_processing'))
del process_list[0] del process_list[0]
success_list = [] success_list = []
failed_list = [] failed_list = []
no_need_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: for i in process_list:
if exists(f"{plugin_directory}version.json"): if plugin_manager.get_remote_plugin(i):
with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: local_temp = plugin_manager.get_local_plugin(i)
version_json = json.load(f) if local_temp and not plugin_manager.plugin_need_update(i):
try: no_need_list.append(i)
plugin_version = version_json[i] else:
except: # noqa try:
plugin_version = 0 if await plugin_manager.install_remote_plugin(i):
success_list.append(i)
else:
failed_list.append(i)
except Exception:
failed_list.append(i)
else: 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) failed_list.append(i)
text = f"<b>{lang('apt_name')}</b>\n\n" text = f"<b>{lang('apt_name')}</b>\n\n"
if len(success_list) > 0: if len(success_list) > 0:
@ -136,15 +110,7 @@ async def plugin(message: Message):
await message.edit(lang('arg_error')) await message.edit(lang('arg_error'))
elif message.parameter[0] == "remove": elif message.parameter[0] == "remove":
if len(message.parameter) == 2: if len(message.parameter) == 2:
if exists(f"{plugin_directory}{message.parameter[1]}.py") or \ if plugin_manager.remove_plugin(message.parameter[1]):
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)
await message.edit(f"{lang('apt_remove_success')} {message.parameter[1]}") await message.edit(f"{lang('apt_remove_success')} {message.parameter[1]}")
await log(f"{lang('apt_remove')} {message.parameter[1]}.") await log(f"{lang('apt_remove')} {message.parameter[1]}.")
await reload_all() await reload_all()
@ -192,9 +158,7 @@ async def plugin(message: Message):
await message.edit(lang('arg_error')) await message.edit(lang('arg_error'))
elif message.parameter[0] == "enable": elif message.parameter[0] == "enable":
if len(message.parameter) == 2: if len(message.parameter) == 2:
if exists(f"{plugin_directory}{message.parameter[1]}.py.disabled"): if plugin_manager.enable_plugin(message.parameter[1]):
rename(f"{plugin_directory}{message.parameter[1]}.py.disabled",
f"{plugin_directory}{message.parameter[1]}.py")
await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} "
f"{lang('apt_enable')}") f"{lang('apt_enable')}")
await log(f"{lang('apt_enable')} {message.parameter[1]}.") await log(f"{lang('apt_enable')} {message.parameter[1]}.")
@ -205,9 +169,7 @@ async def plugin(message: Message):
await message.edit(lang('arg_error')) await message.edit(lang('arg_error'))
elif message.parameter[0] == "disable": elif message.parameter[0] == "disable":
if len(message.parameter) == 2: if len(message.parameter) == 2:
if exists(f"{plugin_directory}{message.parameter[1]}.py") is True: if plugin_manager.disable_plugin(message.parameter[1]):
rename(f"{plugin_directory}{message.parameter[1]}.py",
f"{plugin_directory}{message.parameter[1]}.py.disabled")
await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} " await message.edit(f"{lang('apt_plugin')} {message.parameter[1]} "
f"{lang('apt_disable')}") f"{lang('apt_disable')}")
await log(f"{lang('apt_disable')} {message.parameter[1]}.") await log(f"{lang('apt_disable')} {message.parameter[1]}.")
@ -240,55 +202,20 @@ async def plugin(message: Message):
else: else:
await message.edit(lang('arg_error')) await message.edit(lang('arg_error'))
elif message.parameter[0] == "update": 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"): if not exists(f"{plugin_directory}version.json"):
await message.edit(lang('apt_why_not_install_a_plugin')) await message.edit(lang('apt_why_not_install_a_plugin'))
return return
with open(f"{plugin_directory}version.json", 'r', encoding="utf-8") as f: await plugin_manager.load_remote_plugins()
version_json = json.load(f) updated_plugins = await plugin_manager.update_all_remote_plugin()
plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") if len(updated_plugins) == 0:
plugin_online = plugin_list.json()["list"] await message.edit(f"<b>{lang('apt_name')}</b>\n\n" +
for key, value in version_json.items(): lang("apt_loading_from_online_but_nothing_need_to_update"))
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<code>" + key + "</code>: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"))
else: else:
if len(need_update_list) == 0: message = await message.edit(lang("apt_loading_from_online_and_updating"))
await message.edit(f"<b>{lang('apt_name')}</b>\n\n" + await message.edit(
lang("apt_loading_from_online_but_nothing_need_to_update")) f"<b>{lang('apt_name')}</b>\n\n" + lang("apt_reading_list") + "\n".join(updated_plugins)
else: )
message = await message.edit(lang("apt_loading_from_online_and_updating")) await reload_all()
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"<b>{lang('apt_name')}</b>\n\n" + lang("apt_reading_list") + need_update)
await reload_all()
elif message.parameter[0] == "search": elif message.parameter[0] == "search":
if len(message.parameter) == 1: if len(message.parameter) == 1:
await message.edit(lang("apt_search_no_name")) 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")) await message.edit(lang("apt_why_not_install_a_plugin"))
else: else:
await message.edit(",apt install " + " ".join(list_plugin)) 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: else:
await message.edit(lang("arg_error")) await message.edit(lang("arg_error"))

View File

@ -1,51 +1,11 @@
import contextlib from pagermaid import read_context
import importlib from pagermaid.common.reload import reload_all
import os
import pagermaid.config
import pagermaid.modules
from pagermaid import logs, help_messages, all_permissions, hook_functions, read_context
from pagermaid.enums import Message from pagermaid.enums import Message
from pagermaid.hook import Hook
from pagermaid.listener import listener from pagermaid.listener import listener
from pagermaid.services import bot, scheduler from pagermaid.services import scheduler
from pagermaid.utils import lang 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", @listener(is_plugin=False, command="reload",
need_admin=True, need_admin=True,
description=lang('reload_des')) description=lang('reload_des'))

View File

@ -19,6 +19,7 @@ from shutil import disk_usage
from subprocess import Popen, PIPE from subprocess import Popen, PIPE
from pagermaid import start_time, Config, pgm_version from pagermaid import start_time, Config, pgm_version
from pagermaid.common.status import get_bot_uptime
from pagermaid.enums import Client, Message from pagermaid.enums import Client, Message
from pagermaid.listener import listener from pagermaid.listener import listener
from pagermaid.utils import lang, execute from pagermaid.utils import lang, execute
@ -50,27 +51,7 @@ async def status(message: Message):
# database # database
# database = lang('status_online') if redis_status() else lang('status_offline') # database = lang('status_online') if redis_status() else lang('status_offline')
# uptime https://gist.github.com/borgstrom/936ca741e885a1438c374824efb038b3 # uptime https://gist.github.com/borgstrom/936ca741e885a1438c374824efb038b3
time_units = ( uptime = await get_bot_uptime()
('%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))
text = (f"**{lang('status_hint')}** \n" text = (f"**{lang('status_hint')}** \n"
f"{lang('status_name')}: `{uname().node}` \n" f"{lang('status_name')}: `{uname().node}` \n"
f"{lang('status_platform')}: `{platform}` \n" f"{lang('status_platform')}: `{platform}` \n"
@ -88,7 +69,7 @@ async def status(message: Message):
async def stats(client: Client, message: Message): async def stats(client: Client, message: Message):
msg = await message.edit(lang("stats_loading")) msg = await message.edit(lang("stats_loading"))
a, u, g, s, c, b = 0, 0, 0, 0, 0, 0 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 chat_type = dialog.chat.type
if chat_type == ChatType.BOT: if chat_type == ChatType.BOT:
b += 1 b += 1

View File

@ -1,7 +1,3 @@
import io
import sys
import traceback
from os.path import exists, sep from os.path import exists, sep
from sys import exit from sys import exit
from platform import node from platform import node
@ -9,9 +5,9 @@ from getpass import getuser
from pyrogram.enums import ParseMode from pyrogram.enums import ParseMode
from pagermaid.common.system import run_eval
from pagermaid.listener import listener from pagermaid.listener import listener
from pagermaid.enums import Message from pagermaid.enums import Message
from pagermaid.services import bot
from pagermaid.utils import attach_log, execute, lang, upload_attachment 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] cmd = message.text.split(" ", maxsplit=1)[1]
except (IndexError, AssertionError): except (IndexError, AssertionError):
return await message.edit(lang('eval_need_dev')) return await message.edit(lang('eval_need_dev'))
old_stderr = sys.stderr final_output = await run_eval(cmd, message)
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}`"
if len(final_output) > 4096: if len(final_output) > 4096:
message = await message.edit(f"**>>>** `{cmd}`", parse_mode=ParseMode.MARKDOWN) 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: else:
await message.edit(final_output) await message.edit(final_output)
@ -114,18 +89,3 @@ async def send_log(message: Message):
thumb=f"pagermaid{sep}assets{sep}logo.jpg", thumb=f"pagermaid{sep}assets{sep}logo.jpg",
caption=lang("send_log_caption")) caption=lang("send_log_caption"))
await message.safe_delete() 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)

View File

@ -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.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"), @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'), description=lang('update_des'),
parameters="<true/debug>") parameters="<true/debug>")
async def update(message: Message): async def update(message: Message):
await execute('git fetch --all') await update_function(len(message.parameter) > 0)
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 message.edit(lang('update_success')) await message.edit(lang('update_success'))
exit(0) exit(0)

15
pagermaid/modules/web.py Normal file
View File

@ -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())

View File

@ -6,7 +6,7 @@ from apscheduler.schedulers.asyncio import AsyncIOScheduler
from httpx import AsyncClient from httpx import AsyncClient
from pyrogram import Client as OldClient 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.conversation import Conversation
from pyromod.utils.errors import AlreadyInConversationError, TimeoutConversationError, ListenerCanceled from pyromod.utils.errors import AlreadyInConversationError, TimeoutConversationError, ListenerCanceled
@ -70,6 +70,9 @@ class Client(OldClient):
once_timeout: int = 60, filters=None) -> Optional[Conversation]: once_timeout: int = 60, filters=None) -> Optional[Conversation]:
""" Initialize a conversation with the given chat_id. """ """ 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): class Chat(OldChat):
is_forum: Optional[bool] = None is_forum: Optional[bool] = None

View File

@ -77,6 +77,7 @@ async def execute(command, pass_error=True):
""" Executes command and returns output, with the option of enabling stderr. """ """ Executes command and returns output, with the option of enabling stderr. """
executor = await create_subprocess_shell( executor = await create_subprocess_shell(
command, command,
loop=bot.loop,
stdout=PIPE, stdout=PIPE,
stderr=PIPE, stderr=PIPE,
stdin=PIPE stdin=PIPE

50
pagermaid/web/__init__.py Normal file
View File

@ -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,
)

View File

@ -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)

View File

@ -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)

View File

@ -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),
}
}

View File

@ -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': '成功清空忽略列表'}

View File

@ -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
}
}

View File

@ -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}'}

View File

@ -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()

View File

@ -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)

View File

@ -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')

View File

@ -0,0 +1,9 @@
<div class="p-2 text-center bg-blue-100">
Copyright © 2023
<a href="https://github.com/TeamPGM/PagerMaid-Pyro" target="_blank" class="link-secondary">
PagerMaid-Pyro
</a> X
<a target="_blank" href="https://github.com/baidu/amis" class="link-secondary" rel="noopener">
amis v2.2.0
</a>
</div>

View File

@ -0,0 +1,8 @@
<div class="flex justify-between">
<div></div>
<div>
<a href="https://github.com/TeamPGM/PagerMaid-Pyro" target="_blank" title="Copyright">
<i class="fa fa-github fa-2x"></i>
</a>
</div>
</div>

View File

@ -0,0 +1,14 @@
<p align="center">
<a href="https://github.com/TeamPGM/PagerMaid-Pyro">
<img src="https://xtaolabs.com/pagermaid-logo.png"
width="256" height="256" alt="pagermaid">
</a>
</p>
<h1 align="center">PagerMaid-Pyro 控制台</h1>
<div align="center">
<a href="https://github.com/TeamPGM/PagerMaid-Pyro" target="_blank">
Github仓库</a> &nbsp; · &nbsp;
<a href="https://xtaolabs.com"
target="_blank">文档</a>
</div>
<br>

View File

@ -0,0 +1,2 @@
from .login import login_page
from .main import admin_app, blank_page

View File

@ -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]
)
)

View File

@ -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)

View File

@ -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)
)

View File

@ -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])

View File

@ -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')

View File

@ -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)
)

View File

@ -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)
)

View File

@ -29,6 +29,7 @@ from pyrogram.enums import ChatType
from pagermaid.single_utils import get_sudo_list, Message from pagermaid.single_utils import get_sudo_list, Message
from pagermaid.scheduler import add_delete_message_job 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 import patch, patchable
from ..utils.conversation import Conversation from ..utils.conversation import Conversation
@ -123,6 +124,10 @@ class Client:
) )
return await self.oldread_chat_history(chat_id, max_id) # noqa 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) @patch(pyrogram.handlers.message_handler.MessageHandler)
class MessageHandler: class MessageHandler:

View File

@ -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

View File

@ -5,10 +5,16 @@ pytz>=2021.3
PyYAML>=6.0 PyYAML>=6.0
coloredlogs>=15.0.1 coloredlogs>=15.0.1
psutil>=5.8.0 psutil>=5.8.0
httpx httpx~=0.23.3
apscheduler apscheduler~=3.9.1.post1
sqlitedict sqlitedict~=2.1.0
casbin==1.17.5 casbin==1.17.5
sentry-sdk==1.13.0 sentry-sdk==1.14.0
PyQRCode>=1.2.1 PyQRCode>=1.2.1
PyPng PyPng
fastapi~=0.89.1
amis-python
python-jose
uvicorn
pydantic~=1.10.4
starlette~=0.22.0