🔖 Update to v1.4.1

 Support custom apt source
This commit is contained in:
omg-xtao 2023-07-01 16:42:23 +08:00 committed by GitHub
parent a0dedaea52
commit 0c87310f77
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 280 additions and 133 deletions

View File

@ -344,6 +344,15 @@ apt_plugin_maintainer: Author
apt_plugin_size: size apt_plugin_size: size
apt_plugin_support: support period apt_plugin_support: support period
apt_plugin_des_short: description apt_plugin_des_short: description
apt_source_des: Add or delete or view the source of the plugin repository.
apt_source_parameters: "[add/del] [source url]"
apt_source_not_found: Something went wrong ~ The specified source does not exist.
apt_source_header: "Apt Source List:"
apt_source_add_success: Add success
apt_source_del_success: Del success
apt_source_add_failed: Add failed, this source already exists
apt_source_add_invalid: Add failed, invalid source
apt_source_del_failed: Del failed, this source does not exist
#prune #prune
## prune ## prune
prune_des: Replying to a message with this command will delete all messages between the latest message and the message. Limit, 1000 messages based on message ID, more than 1000 messages may trigger the limit of deleting messages too quickly. (Non-group administrators only delete their own messages) prune_des: Replying to a message with this command will delete all messages between the latest message and the message. Limit, 1000 messages based on message ID, more than 1000 messages may trigger the limit of deleting messages too quickly. (Non-group administrators only delete their own messages)

View File

@ -344,6 +344,15 @@ apt_plugin_maintainer: 作者
apt_plugin_size: 大小 apt_plugin_size: 大小
apt_plugin_support: 支持周期 apt_plugin_support: 支持周期
apt_plugin_des_short: 说明 apt_plugin_des_short: 说明
apt_source_des: "添加/删除/查看 自定义插件源"
apt_source_parameters: "[add/del] [自定义软件源链接]"
apt_source_not_found: 当前没有自定义插件源
apt_source_header: "当前自定义插件源:"
apt_source_add_success: 添加成功
apt_source_del_success: 删除成功
apt_source_add_failed: 添加失败,此源已存在
apt_source_add_invalid: 添加失败,此源无效
apt_source_del_failed: 删除失败,此源不存在
#prune #prune
## prune ## prune
prune_des: 以此命令回复某条消息,将删除最新一条消息至该条消息之间的所有消息。限制:基于消息 ID 的 1000 条消息,大于 1000 条可能会触发删除消息过快限制。(非群组管理员只删除自己的消息) prune_des: 以此命令回复某条消息,将删除最新一条消息至该条消息之间的所有消息。限制:基于消息 ID 的 1000 条消息,大于 1000 条可能会触发删除消息过快限制。(非群组管理员只删除自己的消息)

View File

@ -21,8 +21,8 @@ from pagermaid.scheduler import scheduler
import pyromod.listen import pyromod.listen
from pyrogram import Client from pyrogram import Client
pgm_version = "1.4.0" pgm_version = "1.4.1"
pgm_version_code = 1400 pgm_version_code = 1401
CMD_LIST = {} CMD_LIST = {}
module_dir = __path__[0] module_dir = __path__[0]
working_dir = getcwd() working_dir = getcwd()

View File

@ -2,13 +2,16 @@ import contextlib
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Optional, List from typing import Optional, List, Tuple, Dict
from pydantic import BaseModel from pydantic import BaseModel, ValidationError
from pagermaid import Config from pagermaid import Config, logs
from pagermaid.enums import Message
from pagermaid.common.cache import cache from pagermaid.common.cache import cache
from pagermaid.modules import plugin_list as active_plugins
from pagermaid.utils import client from pagermaid.utils import client
from pagermaid.services import sqlite
plugins_path = Path("plugins") plugins_path = Path("plugins")
@ -27,6 +30,11 @@ class LocalPlugin(BaseModel):
def disabled_path(self) -> Path: def disabled_path(self) -> Path:
return plugins_path / f"{self.name}.py.disabled" return plugins_path / f"{self.name}.py.disabled"
@property
def load_status(self) -> bool:
"""插件加载状态"""
return self.name in active_plugins
def remove(self): def remove(self):
with contextlib.suppress(FileNotFoundError): with contextlib.suppress(FileNotFoundError):
os.remove(self.normal_path) os.remove(self.normal_path)
@ -53,7 +61,8 @@ class RemotePlugin(LocalPlugin):
maintainer: str maintainer: str
size: str size: str
supported: bool supported: bool
des: str des: str = ""
des_short: str = ""
... ...
async def install(self) -> bool: async def install(self) -> bool:
@ -66,8 +75,61 @@ class RemotePlugin(LocalPlugin):
return False return False
class PluginManager: class PluginRemote(BaseModel):
url: str
status: bool
@property
def text(self) -> str:
return f"{'' if self.status else ''} {self.url}"
class PluginRemoteManager:
def __init__(self): def __init__(self):
self.key = "plugins_remotes"
def get_remotes(self) -> List[PluginRemote]:
return [PluginRemote(**i) for i in sqlite.get(self.key, [])]
def set_remotes(self, remotes: List[PluginRemote]):
sqlite[self.key] = [i.dict() for i in remotes]
def add_remote(self, remote_url: str) -> bool:
remotes = self.get_remotes()
if not next(filter(lambda x: x.url == remote_url, remotes), None):
remotes.append(PluginRemote(url=remote_url, status=True))
self.set_remotes(remotes)
return True
return False
def remove_remote(self, remote_url: str) -> bool:
remotes = self.get_remotes()
if next(filter(lambda x: x.url == remote_url, remotes), None):
remotes = [i for i in remotes if i.url != remote_url]
self.set_remotes(remotes)
return True
return False
def disable_remote(self, remote_url: str) -> bool:
remotes = self.get_remotes()
if remote := next(filter(lambda x: x.url == remote_url, remotes), None):
remote.status = False
self.set_remotes(remotes)
return True
return False
def enable_remote(self, remote_url: str) -> bool:
remotes = self.get_remotes()
if remote := next(filter(lambda x: x.url == remote_url, remotes), None):
remote.status = True
self.set_remotes(remotes)
return True
return False
class PluginManager:
def __init__(self, remote_manager: PluginRemoteManager):
self.remote_manager = remote_manager
self.version_map = {} self.version_map = {}
self.remote_version_map = {} self.remote_version_map = {}
self.plugins: List[LocalPlugin] = [] self.plugins: List[LocalPlugin] = []
@ -134,25 +196,55 @@ class PluginManager:
def get_local_plugin(self, name: str) -> LocalPlugin: def get_local_plugin(self, name: str) -> LocalPlugin:
return next(filter(lambda x: x.name == name, self.plugins), None) return next(filter(lambda x: x.name == name, self.plugins), None)
@cache() @staticmethod
async def _load_remote_plugins(self) -> List[RemotePlugin]: async def fetch_remote_url(url: str) -> List[Dict]:
plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") try:
plugin_list = plugin_list.json()["list"] data = await client.get(f"{url}list.json")
plugins = [ data.raise_for_status()
RemotePlugin( return data.json()["list"]
**plugin, except Exception as e:
status=False, logs.error(f"获取远程插件列表失败: {e}")
) raise e
for plugin in plugin_list
] async def load_remote_plugins_no_cache(self) -> List[RemotePlugin]:
remote_urls = [i.url for i in self.remote_manager.get_remotes()]
remote_urls.insert(0, Config.GIT_SOURCE)
plugins = []
plugins_name = []
for remote in remote_urls:
try:
plugin_list = await self.fetch_remote_url(remote)
except Exception as e:
logs.error(f"获取远程插件列表失败: {e}")
self.remote_manager.disable_remote(remote)
continue
self.remote_manager.enable_remote(remote)
for plugin in plugin_list:
try:
plugin_model = RemotePlugin(**plugin, status=False)
if plugin_model.name in plugins_name:
continue
plugins.append(plugin_model)
plugins_name.append(plugin_model.name)
except ValidationError:
logs.warning(f"远程插件 {plugin} 信息不完整")
continue
self.remote_plugins = plugins self.remote_plugins = plugins
self.remote_version_map = {} self.remote_version_map = {plugin.name: plugin.version for plugin in plugins}
for plugin in plugins:
self.remote_version_map[plugin.name] = plugin.version
return plugins return plugins
async def load_remote_plugins(self) -> List[RemotePlugin]: @cache()
plugin_list = await self._load_remote_plugins() async def load_remote_plugins_cache(self) -> List[RemotePlugin]:
return await self.load_remote_plugins_no_cache()
async def load_remote_plugins(
self, enable_cache: bool = True
) -> List[RemotePlugin]:
plugin_list = (
await self.load_remote_plugins_cache()
if enable_cache
else await self.load_remote_plugins_no_cache()
)
for i in plugin_list: for i in plugin_list:
i.status = self.get_plugin_load_status(i.name) i.status = self.get_plugin_load_status(i.name)
return plugin_list return plugin_list
@ -187,5 +279,40 @@ class PluginManager:
updated_plugins.append(i) updated_plugins.append(i)
return updated_plugins return updated_plugins
@staticmethod
async def download_from_message(message: Message) -> str:
"""Download a plugin from a message"""
reply = message.reply_to_message
file_path = None
if (
reply
and reply.document
and reply.document.file_name
and reply.document.file_name.endswith(".py")
):
file_path = await message.reply_to_message.download()
elif (
message.document
and message.document.file_name
and message.document.file_name.endswith(".py")
):
file_path = await message.download()
return file_path
plugin_manager = PluginManager() def get_plugins_status(
self,
) -> Tuple[List[str], List[LocalPlugin], List[LocalPlugin]]:
"""Get plugins status"""
all_local_plugins = self.plugins
disabled_plugins = []
inactive_plugins = []
for plugin in all_local_plugins:
if not plugin.status:
inactive_plugins.append(plugin)
elif not plugin.load_status:
disabled_plugins.append(plugin)
return active_plugins, disabled_plugins, inactive_plugins
plugin_remote_manager = PluginRemoteManager()
plugin_manager = PluginManager(plugin_remote_manager)

View File

@ -13,5 +13,5 @@ __all__ = [
"AsyncIOScheduler", "AsyncIOScheduler",
"SqliteDict", "SqliteDict",
"AsyncClient", "AsyncClient",
"Logger" "Logger",
] ]

View File

@ -1,19 +1,17 @@
""" PagerMaid module to manage plugins. """ """ PagerMaid module to manage plugins. """
import contextlib import contextlib
import json from os import remove, path, sep
from glob import glob
from os import remove, chdir, path, sep
from os.path import exists from os.path import exists
from re import search, I from re import search, I
from shutil import copyfile, move from shutil import copyfile, move
from pagermaid import log, working_dir, Config from pagermaid import log, working_dir
from pagermaid.common.plugin import plugin_manager from pagermaid.common.plugin import plugin_remote_manager, plugin_manager
from pagermaid.common.reload import reload_all from pagermaid.common.reload import reload_all
from pagermaid.enums import Message
from pagermaid.listener import listener from pagermaid.listener import listener
from pagermaid.modules import plugin_list as active_plugins, __list_plugins from pagermaid.utils import upload_attachment, lang
from pagermaid.utils import upload_attachment, lang, Message, client
def remove_plugin(name): def remove_plugin(name):
@ -31,15 +29,6 @@ def move_plugin(file_path):
move(file_path, plugin_directory) move(file_path, plugin_directory)
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:
version_json = json.load(f)
version_json[plugin_name] = version
with open(f"{plugin_directory}version.json", "w") as f:
json.dump(version_json, f)
@listener( @listener(
is_plugin=False, is_plugin=False,
outgoing=True, outgoing=True,
@ -57,27 +46,19 @@ async def plugin(message: Message):
plugin_directory = f"{working_dir}{sep}plugins{sep}" plugin_directory = f"{working_dir}{sep}plugins{sep}"
if message.parameter[0] == "install": if message.parameter[0] == "install":
if len(message.parameter) == 1: if len(message.parameter) == 1:
message = await message.edit(lang("apt_processing")) file_path = await plugin_manager.download_from_message(message)
file_path = None if file_path is None:
with contextlib.suppress(Exception):
if reply:
file_path = await reply.download()
else:
file_path = await message.download()
if file_path is None or not file_path.endswith(".py"):
await message.edit(lang("apt_no_py")) await message.edit(lang("apt_no_py"))
try:
remove(str(file_path))
except FileNotFoundError:
pass
return return
plugin_name = path.basename(file_path)[:-3]
plugin_manager.remove_plugin(plugin_name)
move_plugin(file_path) move_plugin(file_path)
await message.edit( await message.edit(
f"<b>{lang('apt_name')}</b>\n\n" f"<b>{lang('apt_name')}</b>\n\n"
f"{lang('apt_plugin')} " f"{lang('apt_plugin')} "
f"{path.basename(file_path)[:-3]} {lang('apt_installed')}" f"{plugin_name} {lang('apt_installed')}"
) )
await log(f"{lang('apt_install_success')} {path.basename(file_path)[:-3]}.") await log(f"{lang('apt_install_success')} {plugin_name}.")
await reload_all() await reload_all()
elif len(message.parameter) >= 2: elif len(message.parameter) >= 2:
await plugin_manager.load_remote_plugins() await plugin_manager.load_remote_plugins()
@ -134,27 +115,14 @@ async def plugin(message: Message):
await message.edit(lang("arg_error")) await message.edit(lang("arg_error"))
elif message.parameter[0] == "status": elif message.parameter[0] == "status":
if len(message.parameter) == 1: if len(message.parameter) == 1:
inactive_plugins = sorted(__list_plugins()) (
disabled_plugins = [] active_plugins,
if not len(inactive_plugins) == 0: disabled_plugins,
for target_plugin in active_plugins: inactive_plugins,
inactive_plugins.remove(target_plugin) ) = plugin_manager.get_plugins_status()
chdir(f"plugins{sep}") active_plugins_string = ", ".join(active_plugins)
for target_plugin in glob("*.py.disabled"): inactive_plugins_string = ", ".join([i.name for i in inactive_plugins])
disabled_plugins += [f"{target_plugin[:-12]}"] disabled_plugins_string = ", ".join([i.name for i in disabled_plugins])
chdir(f"..{sep}")
active_plugins_string = ""
inactive_plugins_string = ""
disabled_plugins_string = ""
for target_plugin in active_plugins:
active_plugins_string += f"{target_plugin}, "
active_plugins_string = active_plugins_string[:-2]
for target_plugin in inactive_plugins:
inactive_plugins_string += f"{target_plugin}, "
inactive_plugins_string = inactive_plugins_string[:-2]
for target_plugin in disabled_plugins:
disabled_plugins_string += f"{target_plugin}, "
disabled_plugins_string = disabled_plugins_string[:-2]
if len(active_plugins) == 0: if len(active_plugins) == 0:
active_plugins_string = f"`{lang('apt_no_running_plugins')}`" active_plugins_string = f"`{lang('apt_no_running_plugins')}`"
if len(inactive_plugins) == 0: if len(inactive_plugins) == 0:
@ -207,6 +175,7 @@ async def plugin(message: Message):
elif exists(f"{plugin_directory}{file_name}.disabled"): elif exists(f"{plugin_directory}{file_name}.disabled"):
copyfile(f"{plugin_directory}{file_name}.disabled", file_name) copyfile(f"{plugin_directory}{file_name}.disabled", file_name)
if exists(file_name): if exists(file_name):
try:
await message.edit(lang("apt_uploading")) await message.edit(lang("apt_uploading"))
await upload_attachment( await upload_attachment(
file_name, file_name,
@ -216,8 +185,9 @@ async def plugin(message: Message):
caption=f"<b>{lang('apt_name')}</b>\n\n" caption=f"<b>{lang('apt_name')}</b>\n\n"
f"PagerMaid-Pyro {message.parameter[1]} plugin.", f"PagerMaid-Pyro {message.parameter[1]} plugin.",
) )
remove(file_name)
await message.safe_delete() await message.safe_delete()
finally:
remove(file_name)
else: else:
await message.edit(lang("apt_not_exist")) await message.edit(lang("apt_not_exist"))
else: else:
@ -248,22 +218,12 @@ async def plugin(message: Message):
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"))
elif len(message.parameter) == 2: elif len(message.parameter) == 2:
await plugin_manager.load_remote_plugins()
search_result = [] search_result = []
plugin_name = message.parameter[1] plugin_name = message.parameter[1]
plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") for i in plugin_manager.remote_plugins:
plugin_online = plugin_list.json()["list"] if search(plugin_name, i.name, I):
for i in plugin_online: search_result.append(f"`{i.name}` / `{i.version}`\n {i.des_short}")
if search(plugin_name, i["name"], I):
search_result.extend(
[
"`"
+ i["name"]
+ "` / `"
+ i["version"]
+ "`\n "
+ i["des-short"]
]
)
if len(search_result) == 0: if len(search_result) == 0:
await message.edit(lang("apt_search_not_found")) await message.edit(lang("apt_search_not_found"))
else: else:
@ -277,24 +237,23 @@ async def plugin(message: Message):
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"))
elif len(message.parameter) == 2: elif len(message.parameter) == 2:
await plugin_manager.load_remote_plugins()
search_result = "" search_result = ""
plugin_name = message.parameter[1] plugin_name = message.parameter[1]
plugin_list = await client.get(f"{Config.GIT_SOURCE}list.json") for i in plugin_manager.remote_plugins:
plugin_online = plugin_list.json()["list"] if plugin_name == i.name:
for i in plugin_online: if i.supported:
if plugin_name == i["name"]:
if i["supported"]:
search_support = lang("apt_search_supporting") search_support = lang("apt_search_supporting")
else: else:
search_support = lang("apt_search_not_supporting") search_support = lang("apt_search_not_supporting")
search_result = ( search_result = (
f"{lang('apt_plugin_name')}:`{i['name']}`\n" f"{lang('apt_plugin_name')}:`{i.name}`\n"
f"{lang('apt_plugin_ver')}:`Ver {i['version']}`\n" f"{lang('apt_plugin_ver')}:`Ver {i.version}`\n"
f"{lang('apt_plugin_section')}:`{i['section']}`\n" f"{lang('apt_plugin_section')}:`{i.section}`\n"
f"{lang('apt_plugin_maintainer')}:`{i['maintainer']}`\n" f"{lang('apt_plugin_maintainer')}:`{i.maintainer}`\n"
f"{lang('apt_plugin_size')}:`{i['size']}`\n" f"{lang('apt_plugin_size')}:`{i.size}`\n"
f"{lang('apt_plugin_support')}:{search_support}\n" f"{lang('apt_plugin_support')}:{search_support}\n"
f"{lang('apt_plugin_des_short')}:{i['des-short']}" f"{lang('apt_plugin_des_short')}:{i.des_short}"
) )
break break
if search_result == "": if search_result == "":
@ -307,20 +266,60 @@ async def plugin(message: Message):
return return
message = await message.edit(lang("stats_loading")) message = await message.edit(lang("stats_loading"))
list_plugin = [] list_plugin = []
with open(f"{plugin_directory}version.json", "r", encoding="utf-8") as f: for key, value in plugin_manager.version_map.items():
version_json = json.load(f) if not value:
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 continue
for i in plugin_online:
if key == i["name"]:
list_plugin.append(key) list_plugin.append(key)
break
if len(list_plugin) == 0: if len(list_plugin) == 0:
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))
else: else:
await message.edit(lang("arg_error")) await message.edit(lang("arg_error"))
@listener(
is_plugin=False,
outgoing=True,
command="apt_source",
need_admin=True,
description=lang("apt_source_des"),
parameters=lang("apt_source_parameters"),
)
async def apt_source(message: Message):
if len(message.parameter) == 0:
remotes = plugin_remote_manager.get_remotes()
if len(remotes) == 0:
await message.edit(lang("apt_source_not_found"))
return
await message.edit(
f"{lang('apt_source_header')}\n\n" + "\n".join([i.text for i in remotes]),
disable_web_page_preview=True,
)
elif len(message.parameter) == 2:
url = message.parameter[1]
if not url.endswith("/"):
url += "/"
if message.parameter[0] == "add":
try:
status = await plugin_manager.fetch_remote_url(url)
except Exception:
status = False
if status:
if plugin_remote_manager.add_remote(url):
await message.edit(lang("apt_source_add_success"))
await plugin_manager.load_remote_plugins(enable_cache=False)
else:
await message.edit(lang("apt_source_add_failed"))
else:
await message.edit(lang("apt_source_add_invalid"))
elif message.parameter[0] == "del":
if plugin_remote_manager.remove_remote(url):
await message.edit(lang("apt_source_del_success"))
await plugin_manager.load_remote_plugins(enable_cache=False)
else:
await message.edit(lang("apt_source_del_failed"))
else:
await message.edit(lang("arg_error"))
else:
await message.edit(lang("arg_error"))

View File

@ -30,7 +30,9 @@ def sentry_before_send(event, hint):
sentry_sdk_report_time = time() sentry_sdk_report_time = time()
sentry_sdk_git_hash = ( sentry_sdk_git_hash = (
run("git rev-parse HEAD", stdout=PIPE, shell=True, check=True).stdout.decode().strip() run("git rev-parse HEAD", stdout=PIPE, shell=True, check=True)
.stdout.decode()
.strip()
) )
sentry_sdk.init( sentry_sdk.init(
Config.SENTRY_API, Config.SENTRY_API,

View File

@ -7,7 +7,10 @@ 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.login import route as login_route
from pagermaid.web.api.plugin import route as plugin_route from pagermaid.web.api.plugin import route as plugin_route
from pagermaid.web.api.status import route as status_route from pagermaid.web.api.status import route as status_route
from pagermaid.web.api.web_login import route as web_login_route, html_route as web_login_html_route from pagermaid.web.api.web_login import (
route as web_login_route,
html_route as web_login_html_route,
)
__all__ = ["authentication", "base_api_router", "base_html_router"] __all__ = ["authentication", "base_api_router", "base_html_router"]

View File

@ -20,7 +20,7 @@ class UserModel(BaseModel):
password: str password: str
class WebLogin(): class WebLogin:
def __init__(self): def __init__(self):
self.is_authorized = False self.is_authorized = False
self.need_password = False self.need_password = False
@ -51,9 +51,7 @@ class WebLogin():
if not self.is_authorized: if not self.is_authorized:
return return
if not await bot.storage.is_bot() and bot.takeout: if not await bot.storage.is_bot() and bot.takeout:
bot.takeout_id = ( bot.takeout_id = (await bot.invoke(InitTakeoutSession())).id
await bot.invoke(InitTakeoutSession())
).id
await bot.invoke(GetState()) await bot.invoke(GetState())
bot.me = await bot.get_me() bot.me = await bot.get_me()

View File

@ -5,7 +5,7 @@ from amis import (
CardsCRUD, CardsCRUD,
PageSchema, PageSchema,
Page, Page,
) )
card = Card( card = Card(
header=Card.Header( header=Card.Header(