fix commands

This commit is contained in:
jxxghp 2023-06-09 11:26:53 +08:00
parent 50d71621d7
commit 01428e9f8f
25 changed files with 228 additions and 202 deletions

View File

@ -2,7 +2,7 @@ import traceback
from abc import abstractmethod
from typing import Optional, Any
from app.core import Context, ModuleManager, EventManager
from app.core import Context, ModuleManager
from app.log import logger
from app.utils.singleton import AbstractSingleton, Singleton
@ -17,7 +17,6 @@ class ChainBase(AbstractSingleton, metaclass=Singleton):
公共初始化
"""
self.modulemanager = ModuleManager()
self.eventmanager = EventManager()
@abstractmethod
def process(self, *args, **kwargs) -> Optional[Context]:

View File

@ -1,11 +1,8 @@
from typing import Dict, Any
from typing import Any
from fastapi import Request
from app.chain import ChainBase
from app.chain.common import *
from app.chain.search import SearchChain
from app.core import MediaInfo, TorrentInfo, MetaInfo
from app.core import MediaInfo, TorrentInfo, MetaInfo, EventManager
from app.db.subscribes import Subscribes
from app.log import logger
from app.utils.types import EventType
@ -32,6 +29,7 @@ class UserMessageChain(ChainBase):
self.subscribes = Subscribes()
self.searchchain = SearchChain()
self.torrent = TorrentHelper()
self.eventmanager = EventManager()
def process(self, body: Any, form: Any, args: Any) -> None:
"""
@ -60,6 +58,8 @@ class UserMessageChain(ChainBase):
"cmd": text
}
)
self.common.post_message(title=f"正在运行,请稍候 ...", userid=userid)
elif text.isdigit():
# 缓存
cache_data: dict = self._user_cache.get(userid)
@ -285,7 +285,7 @@ class UserMessageChain(ChainBase):
发送媒体列表消息
"""
self.run_module('post_medias_message',
title=f"共找到{total}条相关信息请回复数字选择对应媒体p:上一页 n:下一页)",
title=f"共找到{total}条相关信息请回复数字选择对应媒体p: 上一页 n: 下一页)",
items=items,
userid=userid)
@ -294,6 +294,6 @@ class UserMessageChain(ChainBase):
发送种子列表消息
"""
self.run_module('post_torrents_message',
title=f"共找到{total}条相关信息请回复数字下载对应资源0:自动选择 p:上一页 n:下一页)",
title=f"共找到{total}条相关信息请回复数字下载对应资源0: 自动选择 p: 上一页 n: 下一页)",
items=items,
userid=userid)

View File

@ -1,24 +1,36 @@
import traceback
from threading import Thread, Event
from typing import Any
from app.chain import ChainBase
from app.chain.cookiecloud import CookieCloudChain
from app.chain.douban_sync import DoubanSyncChain
from app.chain.subscribe import SubscribeChain
from app.core import eventmanager, PluginManager, EventManager
from app.core.event_manager import Event
from app.core.event_manager import Event as ManagerEvent
from app.log import logger
from app.utils.singleton import Singleton
from app.utils.types import EventType
class CommandChian(ChainBase):
"""
插件处理链
"""
def process(self, *args, **kwargs):
pass
class Command(metaclass=Singleton):
"""
全局命令管理
全局命令管理消费事件
"""
# 内建命令
_commands = {
"/cookiecloud": {
"func": CookieCloudChain().process,
"description": "同步CookieCloud的Cookie",
"description": "同步站点Cookie",
"data": {}
},
"/doubansync": {
@ -35,9 +47,16 @@ class Command(metaclass=Singleton):
}
}
# 退出事件
_event = Event()
def __init__(self):
# 注册插件命令
plugin_commands = PluginManager().get_plugin_commands()
# 事件管理器
self.eventmanager = EventManager()
# 插件管理器
self.pluginmanager = PluginManager()
# 汇总插件命令
plugin_commands = self.pluginmanager.get_plugin_commands()
for command in plugin_commands:
self.register(
cmd=command.get('cmd'),
@ -48,6 +67,45 @@ class Command(metaclass=Singleton):
'data': command.get('data')
}
)
# 处理链
self.chain = CommandChian()
# 广播注册命令
self.chain.run_module("register_commands", commands=self.get_commands())
# 消息处理线程
self._thread = Thread(target=self.__run)
# 启动事件处理线程
self._thread.start()
def __run(self):
"""
事件处理线程
"""
while not self._event.is_set():
event, handlers = self.eventmanager.get_event()
if event:
logger.info(f"处理事件:{event.event_type} - {handlers}")
for handler in handlers:
try:
names = handler.__qualname__.split(".")
if names[0] == "Command":
self.command_event(event)
else:
self.pluginmanager.run_plugin_method(names[0], names[1], event)
except Exception as e:
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
def stop(self):
"""
停止事件处理线程
"""
self._event.set()
self._thread.join()
def get_commands(self):
"""
获取命令列表
"""
return self._commands
def register(self, cmd: str, func: Any, data: dict = None, desc: str = None) -> None:
"""
@ -83,7 +141,7 @@ class Command(metaclass=Singleton):
EventManager().send_event(etype, data)
@eventmanager.register(EventType.CommandExcute)
def command_event(self, event: Event) -> None:
def command_event(self, event: ManagerEvent) -> None:
"""
注册命令执行事件
event_data: {

View File

@ -39,6 +39,14 @@ class ModuleManager(metaclass=Singleton):
self._running_modules[module_id].init_module()
logger.info(f"Moudle Loaded{module_id}")
def stop(self):
"""
停止所有模块
"""
for _, module in self._running_modules.items():
if hasattr(module, "stop"):
module.stop()
def get_modules(self, method: str) -> Generator:
"""
获取模块列表

View File

@ -1,8 +1,6 @@
import traceback
from threading import Thread
from typing import Tuple, Optional, List, Any
from typing import List, Any
from app.core import EventManager
from app.db.systemconfigs import SystemConfigs
from app.helper import ModuleHelper
from app.log import logger
@ -14,7 +12,6 @@ class PluginManager(metaclass=Singleton):
插件管理器
"""
systemconfigs: SystemConfigs = None
eventmanager: EventManager = None
# 插件列表
_plugins: dict = {}
@ -22,37 +19,17 @@ class PluginManager(metaclass=Singleton):
_running_plugins: dict = {}
# 配置Key
_config_key: str = "plugin.%s"
# 事件处理线程
_thread: Thread = None
# 开关
_active: bool = False
def __init__(self):
self.init_config()
def init_config(self):
self.systemconfigs = SystemConfigs()
self.eventmanager = EventManager()
# 停止已有插件
self.stop()
# 启动插件
self.start()
def __run(self):
"""
事件处理线程
"""
while self._active:
event, handlers = self.eventmanager.get_event()
if event:
logger.info(f"处理事件:{event.event_type} - {handlers}")
for handler in handlers:
try:
names = handler.__qualname__.split(".")
self.run_plugin_method(names[0], names[1], event)
except Exception as e:
logger.error(f"事件处理出错:{str(e)} - {traceback.format_exc()}")
def start(self):
"""
启动
@ -60,21 +37,10 @@ class PluginManager(metaclass=Singleton):
# 加载插件
self.__load_plugins()
# 将事件管理器设为启动
self._active = True
self._thread = Thread(target=self.__run)
# 启动事件处理线程
self._thread.start()
def stop(self):
"""
停止
"""
# 将事件管理器设为停止
self._active = False
# 等待事件处理线程退出
if self._thread:
self._thread.join()
# 停止所有插件
self.__stop_plugins()
@ -131,37 +97,6 @@ class PluginManager(metaclass=Singleton):
return {}
return self.systemconfigs.get(self._config_key % pid) or {}
def get_plugin_page(self, pid: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
"""
获取插件额外页面数据
:return: 标题页面内容确定按钮响应函数
"""
if not self._running_plugins.get(pid):
return None, None, None
if not hasattr(self._running_plugins[pid], "get_page"):
return None, None, None
return self._running_plugins[pid].get_page()
def get_plugin_script(self, pid: str) -> Optional[str]:
"""
获取插件额外脚本
"""
if not self._running_plugins.get(pid):
return None
if not hasattr(self._running_plugins[pid], "get_script"):
return None
return self._running_plugins[pid].get_script()
def get_plugin_state(self, pid: str) -> Optional[bool]:
"""
获取插件状态
"""
if not self._running_plugins.get(pid):
return None
if not hasattr(self._running_plugins[pid], "get_state"):
return None
return self._running_plugins[pid].get_state()
def save_plugin_config(self, pid: str, conf: dict) -> bool:
"""
保存插件配置
@ -170,98 +105,6 @@ class PluginManager(metaclass=Singleton):
return False
return self.systemconfigs.set(self._config_key % pid, conf)
@staticmethod
def __get_plugin_color(plugin: str) -> str:
"""
获取插件的主题色
"""
if hasattr(plugin, "plugin_color") and plugin.plugin_color:
return plugin.plugin_color
return ""
def get_plugins_conf(self, auth_level: int) -> dict:
"""
获取所有插件配置
"""
all_confs = {}
for pid, plugin in self._running_plugins.items():
# 基本属性
conf = {}
# 权限
if hasattr(plugin, "auth_level") \
and plugin.auth_level > auth_level:
continue
# 名称
if hasattr(plugin, "plugin_name"):
conf.update({"name": plugin.plugin_name})
# 描述
if hasattr(plugin, "plugin_desc"):
conf.update({"desc": plugin.plugin_desc})
# 版本号
if hasattr(plugin, "plugin_version"):
conf.update({"version": plugin.plugin_version})
# 图标
if hasattr(plugin, "plugin_icon"):
conf.update({"icon": plugin.plugin_icon})
# ID前缀
if hasattr(plugin, "plugin_config_prefix"):
conf.update({"prefix": plugin.plugin_config_prefix})
# 插件额外的页面
if hasattr(plugin, "get_page"):
title, _, _ = plugin.get_page()
conf.update({"page": title})
# 插件额外的脚本
if hasattr(plugin, "get_script"):
conf.update({"script": plugin.get_script()})
# 主题色
conf.update({"color": self.__get_plugin_color(plugin)})
# 配置项
conf.update({"fields": plugin.get_fields() or {}})
# 配置值
conf.update({"config": self.get_plugin_config(pid)})
# 状态
conf.update({"state": plugin.get_state()})
# 汇总
all_confs[pid] = conf
return all_confs
def get_plugin_apps(self, auth_level: int) -> dict:
"""
获取所有插件
"""
all_confs = {}
for pid, plugin in self._plugins.items():
# 基本属性
conf = {}
# 权限
if hasattr(plugin, "auth_level") \
and plugin.auth_level > auth_level:
continue
# ID
conf.update({"id": pid})
# 名称
if hasattr(plugin, "plugin_name"):
conf.update({"name": plugin.plugin_name})
# 描述
if hasattr(plugin, "plugin_desc"):
conf.update({"desc": plugin.plugin_desc})
# 版本
if hasattr(plugin, "plugin_version"):
conf.update({"version": plugin.plugin_version})
# 图标
if hasattr(plugin, "plugin_icon"):
conf.update({"icon": plugin.plugin_icon})
# 主题色
conf.update({"color": self.__get_plugin_color(plugin)})
if hasattr(plugin, "plugin_author"):
conf.update({"author": plugin.plugin_author})
# 作者链接
if hasattr(plugin, "author_url"):
conf.update({"author_url": plugin.author_url})
# 汇总
all_confs[pid] = conf
return all_confs
def get_plugin_commands(self) -> List[dict]:
"""
获取插件命令

View File

@ -3,6 +3,7 @@ from fastapi import FastAPI
from uvicorn import Config
from app.api.apiv1 import api_router
from app.command import Command
from app.core import settings, ModuleManager, PluginManager
from app.db.init import init_db, update_db
from app.helper.sites import SitesHelper
@ -28,6 +29,10 @@ def shutdown_server():
Scheduler().stop()
# 停止插件
PluginManager().stop()
# 停止模块
ModuleManager().stop()
# 停止事件消费
Command().stop()
@App.on_event("startup")
@ -43,6 +48,8 @@ def start_module():
SitesHelper()
# 启动定时服务
Scheduler()
# 启动事件消费
Command()
if __name__ == '__main__':

View File

@ -225,3 +225,17 @@ class _ModuleBase(metaclass=ABCMeta):
:return: 成功或失败
"""
pass
def register_commands(self, commands: dict):
"""
注册命令实现这个函数接收系统可用的命令菜单
:param commands: 命令字典
"""
pass
@abstractmethod
def stop(self):
"""
如果关闭时模块有服务需要停止需要实现此方法
"""
pass

View File

@ -23,6 +23,9 @@ class Douban(_ModuleBase):
def init_module(self) -> None:
pass
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass

View File

@ -14,6 +14,9 @@ class EmbyModule(_ModuleBase):
def init_module(self) -> None:
self.emby = Emby()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "emby"

View File

@ -21,6 +21,9 @@ class FanartModule(_ModuleBase):
def init_module(self) -> None:
pass
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "FANART_API_KEY", True

View File

@ -20,6 +20,9 @@ class FileTransferModule(_ModuleBase):
def init_module(self) -> None:
pass
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass

View File

@ -7,6 +7,7 @@ from app.modules.filter.RuleParser import RuleParser
class FilterModule(_ModuleBase):
# 规则解析器
parser: RuleParser = None
@ -62,6 +63,9 @@ class FilterModule(_ModuleBase):
def init_module(self) -> None:
self.parser = RuleParser()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "FILTER_RULE", True

View File

@ -23,6 +23,9 @@ class IndexerModule(_ModuleBase):
def init_module(self) -> None:
pass
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "INDEXER", "builtin"

View File

@ -13,6 +13,9 @@ class JellyfinModule(_ModuleBase):
def init_module(self) -> None:
self.jellyfin = Jellyfin()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "jellyfin"

View File

@ -14,6 +14,9 @@ class PlexModule(_ModuleBase):
def init_module(self) -> None:
self.plex = Plex()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "plex"

View File

@ -14,6 +14,9 @@ class QbittorrentModule(_ModuleBase):
def init_module(self) -> None:
self.qbittorrent = Qbittorrent()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "DOWNLOADER", "qbittorrent"

View File

@ -8,12 +8,14 @@ from app.modules.telegram.telegram import Telegram
class TelegramModule(_ModuleBase):
telegram: Telegram = None
def init_module(self) -> None:
self.telegram = Telegram()
def stop(self):
self.telegram.stop()
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MESSAGER", "telegram"
@ -51,13 +53,16 @@ class TelegramModule(_ModuleBase):
}
}
"""
# 校验token
token = args.get("token")
if not token or token != settings.API_TOKEN:
return None
try:
msg_json: dict = json.loads(body)
message: dict = json.loads(body)
except Exception as err:
logger.error(f"解析Telegram消息失败{err}")
return None
if msg_json:
message = msg_json.get("message", {})
if message:
text = message.get("text")
user_id = message.get("from", {}).get("id")
# 获取用户名
@ -117,3 +122,10 @@ class TelegramModule(_ModuleBase):
:return: 成功或失败
"""
return self.telegram.send_torrents_msg(title=title, torrents=items, userid=userid)
def register_commands(self, commands: dict):
"""
注册命令实现这个函数接收系统可用的命令菜单
:param commands: 命令字典
"""
self.telegram.register_commands(commands)

View File

@ -1,13 +1,13 @@
from threading import Event, Thread
import threading
from threading import Event
from typing import Optional, List
from urllib.parse import urlencode
from app.core import settings, MediaInfo, TorrentInfo, Context
import telebot
from app.core import settings, MediaInfo, Context
from app.log import logger
from app.utils.http import RequestUtils
from app.utils.singleton import Singleton
import telebot
from app.utils.string import StringUtils
@ -26,16 +26,25 @@ class Telegram(metaclass=Singleton):
self._telegram_chat_id = settings.TELEGRAM_CHAT_ID
# 初始化机器人
if self._telegram_token and self._telegram_chat_id:
# bot
_bot = telebot.TeleBot(self._telegram_token, parse_mode="markdown")
# 记录句柄
self._bot = _bot
@_bot.message_handler(func=lambda message: True)
def echo_all(message):
RequestUtils(timeout=10).post_res(self._ds_url, json=message.json)
# 启动轮询
def run_polling():
"""
定义线程函数来运行 infinity_polling
"""
_bot.infinity_polling()
# 启动线程来运行 infinity_polling
self._polling_thread = threading.Thread(target=run_polling)
self._polling_thread.start()
def send_msg(self, title: str, text: str = "", image: str = "", userid: str = "") -> Optional[bool]:
"""
发送Telegram消息
@ -156,8 +165,25 @@ class Telegram(metaclass=Singleton):
return True if ret else False
def register_commands(self, commands: dict):
"""
注册菜单命令
"""
if not self._bot:
return
# 设置bot命令
if commands:
self._bot.delete_my_commands()
self._bot.set_my_commands(
commands=[
telebot.types.BotCommand(cmd[1:], str(desc.get("description"))) for cmd, desc in
commands.items()
]
)
def stop(self):
"""
停止Telegram消息接收服务
"""
self._bot.stop_polling()
self._polling_thread.join()

View File

@ -33,6 +33,9 @@ class TheMovieDb(_ModuleBase):
self.tmdb = TmdbHelper()
self.category = CategoryHelper()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
pass

View File

@ -14,6 +14,9 @@ class TransmissionModule(_ModuleBase):
def init_module(self) -> None:
self.transmission = Transmission()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "DOWNLOADER", "transmission"

View File

@ -16,6 +16,9 @@ class WechatModule(_ModuleBase):
def init_module(self) -> None:
self.wechat = WeChat()
def stop(self):
pass
def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MESSAGER", "wechat"

View File

@ -1,10 +1,10 @@
import json
from abc import ABCMeta, abstractmethod
from pathlib import Path
from typing import Any, Optional
from typing import Any
from app.chain import ChainBase
from app.core import settings, Context
from app.core import settings
from app.db import SessionLocal
from app.db.models import Base
from app.db.models.plugin import PluginData
@ -17,7 +17,7 @@ class PluginChian(ChainBase):
插件处理链
"""
def process(self, *args, **kwargs) -> Optional[Context]:
def process(self, *args, **kwargs):
pass

View File

@ -31,11 +31,8 @@ class AutoSignIn(_PluginBase):
event: EventManager = None
# 定时器
_scheduler = None
# 加载的模块
_site_schema: list = []
# 退出事件
_event = Event()
def init_plugin(self, config: dict = None):
self.sites = SitesHelper()
@ -70,7 +67,7 @@ class AutoSignIn(_PluginBase):
:return: 命令关键字事件描述附带数据
"""
return {
"cmd": "/pts",
"cmd": "/sitesignin",
"event": EventType.SiteSignin,
"desc": "站点自动签到",
"data": {}
@ -191,9 +188,7 @@ class AutoSignIn(_PluginBase):
if self._scheduler:
self._scheduler.remove_all_jobs()
if self._scheduler.running:
self._event.set()
self._scheduler.shutdown()
self._event.clear()
self._scheduler = None
except Exception as e:
logger.error("退出插件失败:%s" % str(e))

View File

@ -7,7 +7,8 @@ import requests
from apscheduler.schedulers.background import BackgroundScheduler
from ruamel.yaml import CommentedMap
from app.core import settings
from app.core import settings, eventmanager
from app.core.event_manager import Event
from app.helper import ModuleHelper
from app.helper.sites import SitesHelper
from app.log import logger
@ -17,6 +18,9 @@ from app.utils.http import RequestUtils
from app.utils.timer import TimerUtils
import warnings
from app.utils.types import EventType
warnings.filterwarnings("ignore", category=FutureWarning)
@ -57,6 +61,19 @@ class SiteStatistic(_PluginBase):
self._scheduler.print_jobs()
self._scheduler.start()
@staticmethod
def get_command() -> dict:
"""
定义远程控制命令
:return: 命令关键字事件描述附带数据
"""
return {
"cmd": "/sitestatistic",
"event": EventType.SiteStatistic,
"desc": "站点数据统计",
"data": {}
}
def stop_service(self):
pass
@ -221,6 +238,14 @@ class SiteStatistic(_PluginBase):
title=f"站点 {site_user_info.site_name} 收到 "
f"{site_user_info.message_unread} 条新消息,请登陆查看")
@eventmanager.register(EventType.SiteStatistic)
def refresh(self, event: Event):
"""
刷新站点数据
"""
logger.info("开始执行站点数据刷新 ...")
self.refresh_all_site_data(force=True)
def refresh_all_site_data(self, force: bool = False, specify_sites: list = None):
"""
多线程刷新站点下载上传量默认间隔6小时
@ -228,6 +253,8 @@ class SiteStatistic(_PluginBase):
if not self.sites.get_indexers():
return
logger.info("开始刷新站点数据 ...")
with lock:
if not force \
@ -251,15 +278,13 @@ class SiteStatistic(_PluginBase):
# 并发刷新
with ThreadPool(min(len(refresh_sites), self._MAX_CONCURRENCY)) as p:
site_user_infos: List[ISiteUserInfo] = p.map(self.__refresh_site_data, refresh_sites)
site_user_infos = [info for info in site_user_infos if info]
# 保存数据
for site_user_info in site_user_infos:
# 获取今天的日期
key = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
value = site_user_info.to_dict()
# 按日期保存为字典
self.save_data(key, value)
p.map(self.__refresh_site_data, refresh_sites)
# 获取今天的日期
key = datetime.now().strftime('%Y-%m-%d')
# 保存数据
self.save_data(key, self._sites_data)
# 更新时间
self._last_update_time = datetime.now()
logger.info("站点数据刷新完成")

View File

@ -19,6 +19,8 @@ class EventType(Enum):
CommandExcute = "command.excute"
# 站点签到
SiteSignin = "site.signin"
# 站点数据统计
SiteStatistic = "site.statistic"
# 系统配置Key字典