diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml
index 66e9924..22787cf 100644
--- a/.github/workflows/test.yaml
+++ b/.github/workflows/test.yaml
@@ -11,7 +11,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- python-version: [ "3.9", "3.10", "3.11","3.12"]
+ python-version: [ "3.10", "3.11","3.12"]
steps:
- name: check out my code
diff --git a/.github/workflows/test_linux.yaml b/.github/workflows/test_linux.yaml
new file mode 100644
index 0000000..80a21a8
--- /dev/null
+++ b/.github/workflows/test_linux.yaml
@@ -0,0 +1,45 @@
+name: test_linux
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+
+jobs:
+ test_linux:
+ strategy:
+ fail-fast: false
+ matrix:
+ python-version: [ "3.10", "3.11","3.12" ]
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: "dev"
+
+ - name: install python
+ uses: actions/setup-python@v4.7.1
+ with:
+ python-version: ${{ matrix.python-version }}
+
+ - name: install depends
+ run: |
+ python -m pip install --upgrade pip
+ pip install -r ./ictye-live-dm/requirements.txt
+ pip install ./ictye-live-dm/
+ pip install pytest pytest-cov pytest-asyncio pytest-aiohttp PyQt5
+
+ - name: test
+ run: |
+ pytest ./ictye-live-dm --junitxml=junit/test-results-${{ matrix.python-version }}.xml --cov --cov-report=html
+
+ - name: upload_results
+ uses: actions/upload-artifact@v3
+ with:
+ name: pytest-results-${{ matrix.python-version }}
+ path: |
+ junit/test-results-${{ matrix.python-version }}.xml
+ htmlcov/*
+ # Use always() to always run this step to publish test results when there are test failures
+ if: ${{ always() }}
+
+
diff --git a/.gitignore b/.gitignore
index 23ccfbe..84e7c26 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@ __pycache__/
.hintrc/
.vscode/
build/
-dist/
+dist/*
data/
ictye-live-dm/src/ictye_live_dm/logs
/.hintrc
diff --git a/.run/Ictye live dm plugin api.run.xml b/.run/Ictye live dm plugin api.run.xml
new file mode 100644
index 0000000..12a6076
--- /dev/null
+++ b/.run/Ictye live dm plugin api.run.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/gen GUI.run.xml b/.run/gen GUI.run.xml
new file mode 100644
index 0000000..9ad353c
--- /dev/null
+++ b/.run/gen GUI.run.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.run/ictye live dm gui.run.xml b/.run/ictye live dm gui.run.xml
index f747b9c..c620ef8 100644
--- a/.run/ictye live dm gui.run.xml
+++ b/.run/ictye live dm gui.run.xml
@@ -20,6 +20,8 @@
-
+
+
+
\ No newline at end of file
diff --git a/Apis/Ildm.yaml b/Apis/Ildm.yaml
new file mode 100644
index 0000000..b631cd0
--- /dev/null
+++ b/Apis/Ildm.yaml
@@ -0,0 +1,12 @@
+openapi: "3.0.2"
+info:
+ title: API awa
+ version: "1.0"
+servers:
+ - url: https://api.server.test/v1
+paths:
+ /test:
+ get:
+ responses:
+ '200':
+ description: OK
diff --git a/README.md b/README.md
index a74d058..bd85772 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,12 @@
-# ictye live Danmku
+
+
+
+
Ictye Live Danmku(ictye-live-dm)
+
+
+
+
+
一个简单而又全能的弹幕姬框架(其实就是一个web服务器加上websocket服务器),通过插件来实现弹幕功能
@@ -9,7 +17,7 @@
## 开始使用它
-1. 部署ictye live Danmku:你只需要简单的解压它就好,默认情况的启动脚本是为Windows用户提供的,不过在非Windows操作系统上,你仍旧可以使用`requirement.txt`来配置环境并且使用`python -m ictye-live-Danmku`来启动项目
+1. 部署ictye live Danmku:你只需要简单的解压它就好,默认情况的启动脚本是为Windows用户提供的,不过在非Windows操作系统上,你仍旧可以使用`requirement.txt`来配置环境并且使用`python -m ictye-live-dm`来启动项目
2. 配置项目:
1. 这个软件是通过插件的形式来实现弹幕的获取,弹幕的分析和处理
2. `/plugin`里放置python脚本插件,可以在后端提供弹幕,分析弹幕以及提供前端接口等待
diff --git a/Writerside/c.list b/Writerside/c.list
deleted file mode 100644
index c4c77a2..0000000
--- a/Writerside/c.list
+++ /dev/null
@@ -1,6 +0,0 @@
-
-
-
-
-
\ No newline at end of file
diff --git a/Writerside/cfg/buildprofiles.xml b/Writerside/cfg/buildprofiles.xml
deleted file mode 100644
index 4ff046f..0000000
--- a/Writerside/cfg/buildprofiles.xml
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
- false
-
-
-
-
diff --git a/Writerside/images/completion_procedure.png b/Writerside/images/completion_procedure.png
deleted file mode 100644
index 3535a3f..0000000
Binary files a/Writerside/images/completion_procedure.png and /dev/null differ
diff --git a/Writerside/images/completion_procedure_dark.png b/Writerside/images/completion_procedure_dark.png
deleted file mode 100644
index a65beb0..0000000
Binary files a/Writerside/images/completion_procedure_dark.png and /dev/null differ
diff --git a/Writerside/images/convert_table_to_xml.png b/Writerside/images/convert_table_to_xml.png
deleted file mode 100644
index 2518a64..0000000
Binary files a/Writerside/images/convert_table_to_xml.png and /dev/null differ
diff --git a/Writerside/images/convert_table_to_xml_dark.png b/Writerside/images/convert_table_to_xml_dark.png
deleted file mode 100644
index 4716122..0000000
Binary files a/Writerside/images/convert_table_to_xml_dark.png and /dev/null differ
diff --git a/Writerside/images/new_topic_options.png b/Writerside/images/new_topic_options.png
deleted file mode 100644
index bc6abb6..0000000
Binary files a/Writerside/images/new_topic_options.png and /dev/null differ
diff --git a/Writerside/images/new_topic_options_dark.png b/Writerside/images/new_topic_options_dark.png
deleted file mode 100644
index bf3e48d..0000000
Binary files a/Writerside/images/new_topic_options_dark.png and /dev/null differ
diff --git a/Writerside/pa.tree b/Writerside/pa.tree
deleted file mode 100644
index 3f95963..0000000
--- a/Writerside/pa.tree
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Writerside/topics/Overview.topic b/Writerside/topics/Overview.topic
deleted file mode 100644
index 77e9efd..0000000
--- a/Writerside/topics/Overview.topic
+++ /dev/null
@@ -1,27 +0,0 @@
-
-
-
-
-
- Overview articles give background information and provide context to a particular subject.
- Their goal is to explain a concept, not to teach or give instructions.
-
-
-
- Provide some background and context, explain choices and alternatives.
-
-
- A definition list or a glossary:
-
-
- This is the definition of the first term.
-
-
- This is the definition of the second term.
-
-
-
-
\ No newline at end of file
diff --git a/Writerside/topics/Section-Starting-Page.topic b/Writerside/topics/Section-Starting-Page.topic
deleted file mode 100644
index 0ddbcb3..0000000
--- a/Writerside/topics/Section-Starting-Page.topic
+++ /dev/null
@@ -1,111 +0,0 @@
-
-
-
-
-
-
-
- Section starting page title
-
- Add an introductory paragraph: explain what this section is about in 2-3 short sentences.
-
-
-
-
- Custom card title
- Another custom title
-
-
-
-
- Get started
-
-
- Custom card title
- Custom card title
-
-
-
-
- Explore advanced features
-
-
- Custom card title
- Custom card title
-
-
-
-
-
- Other relevant topics as wide cards
-
-
- Custom card title
- Custom card title
-
-
-
- Other relevant topics as narrow cards
-
-
-
- Custom card title
- Custom card title
- Custom card title
-
-
-
-
- Other related topics as links
-
-
- Custom card title
- Custom card title
-
-
- Two in a row
-
-
- Custom card title
- Custom card title
-
-
-
-
-
- More related topics
-
-
- Custom card title
- Custom card title
-
-
- Three in a row
-
-
- Custom card title
- Custom card title
-
-
- Each group is narrow
-
-
- Custom card title
- Custom card title
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Writerside/topics/starter-topic.md b/Writerside/topics/starter-topic.md
deleted file mode 100644
index e5e941c..0000000
--- a/Writerside/topics/starter-topic.md
+++ /dev/null
@@ -1,79 +0,0 @@
-# About Plugin Api
-
-
-
-## Add new topics
-You can create empty topics, or choose a template for different types of content that contains some boilerplate structure to help you get started:
-
-{ width=290 }{border-effect=line}
-
-## Write content
-%product% supports two types of markup: Markdown and XML.
-When you create a new help article, you can choose between two topic types, but this doesn't mean you have to stick to a single format.
-You can author content in Markdown and extend it with semantic attributes or inject entire XML elements.
-
-## Inject XML
-For example, this is how you inject a procedure:
-
-
-
- Start typing and select a procedure type from the completion suggestions:
-
-
-
- Press Tab or Enter to insert the markup.
-
-
-
-## Add interactive elements
-
-### Tabs
-To add switchable content, you can make use of tabs (inject them by starting to type `tab` on a new line):
-
-
-
- { width=450 }
-
-
-
- ]]>
-
-
-
-### Collapsible blocks
-Apart from injecting entire XML elements, you can use attributes to configure the behavior of certain elements.
-For example, you can collapse a chapter that contains non-essential information:
-
-#### Supplementary Info {collapsible="true"}
-Content under a collapsible header will be collapsed by default,
-but you can modify the behavior by adding the following attribute:
-`default-state="expanded"`
-
-### Convert selection to XML
-If you need to extend an element with more functions, you can convert selected content from Markdown to semantic markup.
-For example, if you want to merge cells in a table, it's much easier to convert it to XML than do this in Markdown.
-Position the caret anywhere in the table and press Alt+Enter :
-
-
-
-## Feedback and support
-Please report any issues, usability improvements, or feature requests to our
-YouTrack project
-(you will need to register).
-
-You are welcome to join our
-public Slack workspace .
-Before you do, please read our [Code of conduct](https://plugins.jetbrains.com/plugin/20158-writerside/docs/writerside-code-of-conduct.html).
-We assume that you’ve read and acknowledged it before joining.
-
-You can also always email us at [writerside@jetbrains.com](mailto:writerside@jetbrains.com).
-
-
-
- Markup reference
- Reorder topics in the TOC
- Build and publish
- Configure Search
-
-
\ No newline at end of file
diff --git a/Writerside/v.list b/Writerside/v.list
deleted file mode 100644
index 2d12cb3..0000000
--- a/Writerside/v.list
+++ /dev/null
@@ -1,5 +0,0 @@
-
-
-
-
-
diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg
deleted file mode 100644
index e53db68..0000000
--- a/Writerside/writerside.cfg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/build/.gitignore b/build/.gitignore
deleted file mode 100644
index e69de29..0000000
diff --git a/build/lib/__init__.py b/build/lib/__init__.py
deleted file mode 100644
index 1cb1fb6..0000000
--- a/build/lib/__init__.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
diff --git a/build/lib/__main__.py b/build/lib/__main__.py
deleted file mode 100644
index e95c64f..0000000
--- a/build/lib/__main__.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-import logging
-from depends import logger, configs
-import http_server
-import pluginsystem
-import livewebsocket
-import asyncio
-import os
-import argparse
-
-
-# NOTICE by ictye(2023-11-24):项目要尽可能简洁,轻量,因为主播的电脑在开了直播软件后剩余的资源很少,别问我是怎么知道的。
-# 变量命名的时候要声明好类型,我不是很喜欢动态类型,虽然它没什么毛病,但我就是不喜欢。
-# 类名称和方法名称要易懂,除了循环用的临时变量。
-# 好的风格会更容易维护,风格不好的pr我不会批准,别问为啥。
-
-def main():
- os.chdir(os.path.dirname(os.path.abspath(__file__)))
-
- def run_server():
- loop = asyncio.get_event_loop()
- loop.create_task(http_server.http_server(config))
- loop.create_task(livewebsocket.websocket_main(config))
- loop.create_task(plugin_sys.plugin_main_runner())
- try:
- loop.run_forever()
- except KeyboardInterrupt:
- for task in asyncio.Task.all_tasks():
- task.cancel("Keyboard input")
- finally:
- loop.close()
-
- parse = argparse.ArgumentParser(description="""
- power by 楚天尋簫
- 一个基于python实现的模块化弹幕姬框架
- """)
- parse.add_argument('-u', '--unportable', action='store_true', help='非便携性启动')
- parse.add_argument("-cfg", "--config", default="", help='指定配置目錄')
- parse.add_argument('-i', '--install', action="append", default=[], help='安裝插件')
- parse.add_argument('-l', '--list', action="store_true", help='列出所有的插件')
- args = parse.parse_args()
-
- unportable: bool = args.unportable
- """便携启动开关"""
- configdir: str = args.config
- """配置目錄"""
- install: list = args.install
- """安裝插件"""
- list: bool = args.list
- """列出插件"""
- if install:
- print(install)
- exit(0)
-
- # 获取配置
- config = configs.config(configdir)
-
- # 传递配置
- http_server.config = config
- pluginsystem.global_config = config
- livewebsocket.config = config
- # 获取logger
- logger.setup_logging(config, unportable)
- loggers = logging.getLogger(__name__)
- # 获取插件系统
- plugin_sys = pluginsystem.Plugin()
- livewebsocket.plugin_system = plugin_sys
- http_server.plugin_system = plugin_sys
-
- # 启动服务器
- loggers.info("project starting")
- loggers.info("金克拉,你有了吗?")
- run_server()
-
- loggers.info("project already stopped")
-
-
-if __name__ == "__main__":
- main()
diff --git a/build/lib/depends/configs.py b/build/lib/depends/configs.py
deleted file mode 100644
index 5d1de6d..0000000
--- a/build/lib/depends/configs.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import yaml
-import os
-
-cfgdir: str = ""
-
-
-def config(cfg: str) -> dict:
- cfgdir = cfg
- if cfg:
- cfgfile = cfg
- else:
- cfgfile = "./config/system/config.yaml"
- with open(cfgfile, "r", encoding="utf-8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- if configs["debug"] == 1:
- print(f"log:already reading config file: {configs}\n")
- return configs
-
-
-def set_config(config_family: str, config: dict) -> bool:
- """
- 设置插件的配置
- """
- try:
- with open(f"./config/plugin/{config_family}/config.yaml", "w", encoding="utf_8") as f:
- yaml.dump(data=config, stream=f, allow_unicode=True)
- except Exception as e:
- print(str(e))
- return False
- finally:
- return True
-
-
-def read_config(config_family: str) -> dict:
- """
- 读取插件的配置
- """
- configs = {}
- if os.path.exists(f"./config/plugin/{config_family}/config.yaml"):
- with open(f"./config/plugin/{config_family}/config.yaml", "r", encoding="utf_8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- return configs
- else:
- os.makedirs(os.path.dirname(f"./config/plugin/{config_family}/config.yaml"), exist_ok=True)
- with open(f"./config/plugin/{config_family}/config.yaml", 'w+') as f:
- f.write(f"# config for {config_family}")
- return configs
diff --git a/build/lib/depends/connects.py b/build/lib/depends/connects.py
deleted file mode 100644
index b63f8a4..0000000
--- a/build/lib/depends/connects.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-from websockets.server import WebSocketServerProtocol
-import aiohttp.web as web
-
-
-class connect_wrapper:
- """
- 连接包装类
- """
-
- def __init__(self, connect: WebSocketServerProtocol):
- self.__connect__ = connect
- self.id = connect.id # 连接id
- self.open = connect.open # 连接状态
-
- def refresh(self):
- """
- 刷新状态
- """
- self.id = self.__connect__.id
- self.open = self.__connect__.open
diff --git a/build/lib/depends/logger.py b/build/lib/depends/logger.py
deleted file mode 100644
index e6e2cc0..0000000
--- a/build/lib/depends/logger.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import logging
-import time
-import os
-
-
-def setup_logging(config: dict, unportable: bool):
- """
- Setup logging configuration
- """
-
- level_dic: dict = {"DEBUG": logging.DEBUG,
- "INFO": logging.INFO,
- "WARNING": logging.WARNING,
- "ERROR": logging.ERROR,
- "CRITICAL": logging.CRITICAL,
- "FATAL": logging.FATAL}
-
- if unportable:
- appdata_path = os.getenv('APPDATA')
- log_path = os.path.join(appdata_path, "ictye_live_dm", "log")
- else:
- log_path = "logs"
- """日志档案路径"""
-
- logger = logging.getLogger() # 获取全局logger
- logger.setLevel(level_dic[config["loglevel"]]) # 设置日志级别
-
- # 创建一个handler,用于写入日志文件
- if not os.path.exists(log_path):
- os.makedirs(log_path)
-
- fh = logging.FileHandler(
- os.path.join(log_path, config["logfile"]["name"] + time.strftime("%Y%m%d_%H%M%S",time.localtime()) + ".log"),
- encoding="utf-8")
-
- fh.setLevel(level_dic[config["loglevel"]])
-
- # 创建一个handler,用于将日志输出到控制台
- ch = logging.StreamHandler()
- ch.setLevel(level_dic[config["loglevel"]])
-
- # 定义handler的输出格式
- formatter = logging.Formatter("[%(asctime)s,%(name)s] %(levelname)s : %(message)s")
- fh.setFormatter(formatter)
- ch.setFormatter(formatter)
-
- # 给logger添加handler
- if config["logfile"]["open"]:
- logger.addHandler(fh)
- logger.addHandler(ch)
-
- tmp_logger = logging.getLogger(__name__)
-
- tmp_logger.info("log path " + log_path)
diff --git a/build/lib/depends/msgs.py b/build/lib/depends/msgs.py
deleted file mode 100644
index 818b822..0000000
--- a/build/lib/depends/msgs.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""
-这个文件内定义全部的标准消息模板
-
-所有的函数都应该又to_dict方法来将其转为字典,相反的这个函数能输出打包好的字典
-"""
-
-
-class datas:
- def to_dict(self, cls: object):
- return dict(cls)
-
-
-class connect_ok:
- """
- 连接认证消息
- """
- code = 200
- msg = "connect ok"
-
- def to_dict(self):
- return {"code": self.code,
- "msg": self.msg}
-
-
-class dm:
- def __init__(self, msg: str, who: dict):
- """
- params:
- msg:str 消息主体
- who:dict 消息发出者对象(其实是一个字典)
-
- 成员方法:
- to_dict: 输出为字典
- """
- self.msg = msg
- self.who = who
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who}
-
-
-class info:
- def __init__(self,
- msg: str,
- who: str,
- pic: dict):
- self.msg = msg
- self.who = who
- self.pic = pic
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who,
- "pic": self.pic}
-
-
-class socket_responce:
- def __init__(self, config: dict):
- self.code = 200
- self.local = "ws://{}:{}".format(config["host"], config["websocket"]["port"])
-
- def to_dict(self):
- return {"code": self.code,
- "local": self.local}
-
-
-class msg_who:
- def __init__(self, type: int,
- name: str,
- face: str):
- self.type = type
- self.name = name
- self.face = face
-
- def to_dict(self):
- return {"name": self.name,
- "type": self.type,
- "face": self.face}
-
-
-class pic:
- def __init__(self,
- border: bool,
- pic_url: str):
- self.border = border
- self.pic_url = pic_url
-
- def to_dict(self):
- return {"border": self.border,
- "pic_url": self.pic_url}
-
-
-class msg_box:
- """
- 消息标准封装所用的类
- """
-
- def __init__(self,
- message_class: str,
- msg_type: str,
- message_body: dict):
- self.message_class = message_class
- self.msg_type = msg_type
- self.message_body = message_body
-
- def to_dict(self):
- return {"message_class": self.message_class,
- "msg_type": self.msg_type,
- "message_body": self.message_body}
diff --git a/build/lib/depends/plugin_errors.py b/build/lib/depends/plugin_errors.py
deleted file mode 100644
index 7404e76..0000000
--- a/build/lib/depends/plugin_errors.py
+++ /dev/null
@@ -1,19 +0,0 @@
-class PluginTypeError(Exception):
- def __init__(self, message):
- self.message = message
- super().__init__(self.message)
-
-
-class UnexpectedPluginMessage(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMessage, self).__init__(message)
-
-
-class UnexpectedPluginMather(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMather, self).__init__(message)
-
-
-class NoMainMather(Exception):
- def __init__(self, message):
- super(NoMainMather, self).__init__(message)
diff --git a/build/lib/depends/pluginmain.py b/build/lib/depends/pluginmain.py
deleted file mode 100644
index cec0f87..0000000
--- a/build/lib/depends/pluginmain.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-from . import plugin_errors, msgs
-import asyncio
-import typing
-from . import configs as configs
-from . import connects
-from aiohttp import web
-
-
-class PluginMain:
-
- @typing.final
- def __init__(self):
- """
- 不要用这个而是用plugin_init来进行插件的初始化,这个仅供内部使用
- """
- self.stop: bool = False
- """停止标志"""
-
- self.plugin_js_sprit_support: bool = False
- """js插件支持"""
-
- self.plugin_js_sprit: str = ""
- """js插件"""
-
- self.type: str = str()
- """插件类型"""
-
- self.config: dict = dict()
- """配置字典"""
-
- self.sprit_cgi_support = False
- """插件cgi支持"""
-
- self.sprit_cgi_lists: dict = dict()
- """cgi列表"""
-
- self.plugin_name: str = ""
- """插件名称"""
-
- self.web: web = web
- """web前端模块"""
-
- if self.plugin_type() == "message":
- self.message_list = []
-
- def plugin_init(self) -> str:
- """
- 插件开始被加载时调用
- 父函数本身不实现任何功能
- 需要返回插件类型给插件系统以判断插件类型
- (实际上这个类就是个异步迭代器)
-
- return:如果是”message“则表示这是个消息提供插件,如果是”analyzer“则表示这个插件是用来获取中间消息并且进行处理的。
- """
- self.stop = 0
- raise plugin_errors.UnexpectedPluginMessage('插件入口方法没有实现')
-
- async def plugin_main(self):
- """
- 插件的主方法,此方法停止时插件也会被视为运行完毕
- """
- pass
-
- async def message_filter(self, message) -> msgs.msg_box:
- """
- 消息过滤器,用于自动处理消息,比如翻译或者敏感词过滤
- :param message:待处理的消息
- :return 消息
-
- """
- return message
-
- async def message_anaylazer(self, message):
- """
- 消息分析
- """
- pass
-
- async def sprit_cgi(self, request):
- """
- 脚本cgi接口
- :param request:请求对象
- :return 响应,用aiohttp的就行(已经封装为self.web)
- """
- if self.sprit_cgi_support:
- raise plugin_errors.UnexpectedPluginMather("未实现的插件方法")
-
- def dm_iter(self, params: dict) -> object:
- """
- 返回弹幕迭代对象
- :param params: 前端的get参数
- :return 消息迭代对象
-
- """
- return self
-
- @typing.final
- def update_config(self, config: dict):
- """
- 更新配置,将自身的配置写入文件并且保存在计算机上
- """
- assert self.plugin_name != ""
- configs.set_config(self.plugin_name, config)
-
- @typing.final
- def read_config(self):
- """
- 读取配置
- """
- assert self.plugin_name != ""
- self.config = configs.read_config(self.plugin_name)
-
- def __aiter__(self):
- if self.plugin_type() == "message":
- return self
-
- async def __anext__(self):
- if self.plugin_type() == "message":
- if self.message_list:
- return self.message_list.pop(0)
- else:
- raise StopAsyncIteration()
-
- @typing.final
- def plugin_stop(self):
- """
- 插件停止
- """
- self.stop = 1
- asyncio.current_task().cancel()
-
- def plugin_callback(self):
-
- """
- 插件回调
- """
-
- print(f"plugin is done")
-
- @typing.final
- def plugin_getconfig(self) -> dict:
- """
- 获取配置
- """
- return configs.config(configs.cfgdir)
-
- @typing.final
- def plugin_type(self) -> str:
- """
- 获取插件类型
- """
- # 不存在则初始化插件类型,并返回插件类型给软件以判断插件类型
- # 不存在插件类型则表示插件没有被加载,返回插件没有被加载的错误信息给软件以判断插件是否被加载
- if not self.type:
- self.type = self.plugin_init()
- return self.type
diff --git a/build/lib/http_server.py b/build/lib/http_server.py
deleted file mode 100644
index d90323b..0000000
--- a/build/lib/http_server.py
+++ /dev/null
@@ -1,141 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-from aiohttp import web
-from depends import msgs
-import json
-import logging
-import os
-import pluginsystem
-import livewebsocket
-
-config = dict()
-plugin_system: pluginsystem.Plugin
-log = logging.getLogger(__name__)
-
-
-def return_file(file: str):
- async def healder(request):
- nonlocal file
- log.info("return for main_page")
- return web.FileResponse(path=file, status=200)
-
- return healder
-
-
-async def http_handler(request):
- """
- 主文件请求
- """
- return web.HTTPFound("/index")
-
-
-async def http_socket_get(request):
- log.info("return for socket")
- return web.Response(text=json.dumps({"code": 200, "local": "/ws"}) if config["dev"] else json.dumps(
- msgs.socket_responce(config).to_dict()))
-
-
-async def http_websocket(request: web.Request):
- ws = "ws://{}:{}".format(config["host"], config["websocket"]["port"])
- return web.WebSocketResponse
-
-
-async def http_plugin(request: web.Request):
- log.info(f"request for {request.match_info['name']}")
- if os.path.exists(f"web/js/plugin/{request.match_info['name']}"):
- return web.FileResponse(path=f"web/js/plugin/{request.match_info['name']}")
- elif request.match_info["name"] in plugin_system.plugin_js_support:
- return web.Response(text=plugin_system.plugin_js_support[request.match_info["name"]],
- content_type="application/javascript")
- else:
- return web.Response(status=404, text="not found")
-
-
-async def http_style(request):
- log.info(f"request for {request.match_info['name']}")
- return web.FileResponse(path=f"web/style/{request.match_info['name']}")
-
-
-async def http_js(request):
- log.info(f"request for {request.match_info['name']}")
- return web.FileResponse(path=f"web/js/{request.match_info['name']}")
-
-
-async def http_lib(request):
- log.info(f"request for {request.match_info['name']}")
- return web.FileResponse(path=f"web/js/lib/{request.match_info['name']}")
-
-
-async def http_script(request):
- log.info(f"request for {request.match_info['name']}")
- return web.FileResponse(path=f"web/js/script/{request.match_info['name']}")
-
-
-async def http_api_plugin(request):
- log.info(f"request for web plugin list")
-
- plugin_list = {"code": 200,
- "list": [os.path.splitext(file_name)[0] for file_name in os.listdir("web/js/plugin") if
- file_name.endswith('.js')] + list(plugin_system.plugin_js_support)}
-
- return web.json_response(plugin_list)
-
-
-async def http_cgi(request):
- """
- HTTP ic py cgi前端调用
- """
- req = web.Response(status=404, text="not such path")
- try:
- if request.match_info["name"] in plugin_system.plugin_cgi_support:
- if request.match_info["page"] in plugin_system.plugin_cgi_support[request.match_info["name"]]:
- req = await plugin_system.plugin_cgi_support[request.match_info["name"]][request.match_info["page"]](
- request)
- else:
- req = web.Response(status=404, text="no such path")
- else:
- req = web.Response(status=404, text="no such plugin")
- except Exception as e:
- log.error(f"cgi plugin error:{str(e)}")
- req = web.Response(status=504, text=f"cgi error :{str(e)}")
- return req
-
-
-async def http_server(configs):
- # http服务器
- log.info("http server started")
-
- app = web.Application()
-
- route_list: [web.RouteDef] = [web.get("/", http_handler),
- web.get("/get_websocket", http_socket_get),
- web.get("/style/{name}", http_style),
- web.get("/js/plugin/{name}", http_plugin),
- web.get("/js/{name}", http_js),
- web.get("/js/lib/{name}", http_lib),
- web.get("/js/script/{name}", http_script),
- web.get("/ws", livewebsocket.aiohttp_ws),
- web.get("/api/plugin_list", http_api_plugin),
- web.get("/cgi/{name}/{page}", http_cgi)
- ]
- for i in configs["web"]:
- file, path = list(i.items())[0]
- route_list.append(web.get(f"/{file}", return_file(path)))
-
- app.add_routes(route_list)
-
- runner = web.AppRunner(app)
- await runner.setup()
- site = web.TCPSite(runner, configs["host"], configs["port"])
- await site.start()
- log.info(f"seriver is starting at http://{configs['host']}:{configs['port']}")
diff --git a/build/lib/livewebsocket.py b/build/lib/livewebsocket.py
deleted file mode 100644
index bdf9205..0000000
--- a/build/lib/livewebsocket.py
+++ /dev/null
@@ -1,117 +0,0 @@
-import asyncio
-import json
-from websockets import server
-import logging
-from depends import msgs
-import pluginsystem
-from aiohttp import web
-import aiohttp
-
-plugin_system: pluginsystem.Plugin
-config: dict = {}
-param_list: dict = {}
-connect_list: list[server.WebSocketServerProtocol] = []
-loggers = logging.getLogger(__name__)
-
-
-async def websockets(websocket: server.WebSocketServerProtocol):
- """
- websocket消息处理主函数
- """
- # 连接检测
- try:
- async for message in websocket:
- loggers.info("receive a message" + message)
-
- # 解码信息,注意信息必须是json格式
- ret = json.loads(message)
- if (ret["code"] == 200 and ret["msg"] == "ok") or websocket in connect_list: # 连接验证
- loggers.info("connect with blower success")
- await websocket.send(json.dumps(msgs.connect_ok().to_dict()))
-
- # 分析参数,并保存参数信息
- if "param" in ret:
- param_list[websocket.id] = ret["param"]
- loggers.debug("param_list", param_list)
-
- # 发送弹幕
- while websocket.open:
- await asyncio.sleep(0.5)
- dms: dict
- async for dms in plugin_system.get_plugin_message(ret["param"], websocket):
- # 过滤消息,并分析消息
- mdm = await plugin_system.message_filter(dms)
- await plugin_system.message_analyzer(mdm)
-
- loggers.debug(f"sending message {mdm}")
-
- # 发送消息
- await websocket.send(json.dumps(mdm))
- else:
- # 认证失败就关闭连接
- loggers.error("connect failed,unexpected client")
- await websocket.close()
-
- finally:
- # 后续的处理
- await websocket.close()
- await plugin_system.remove_connect_in_id_dict(websocket.id)
-
-
-async def aiohttp_ws(request: web.Request):
- """
- aiohttp ws處理程序
- """
-
- ws = web.WebSocketResponse()
- await ws.prepare(request)
-
- # 连接检测
- try:
- message: aiohttp.WSMessage
- async for message in ws:
- loggers.info("receive a message" + message.data)
-
- # 解码信息,注意信息必须是json格式
- ret = json.loads(message.data)
- if (ret["code"] == 200 and ret["msg"] == "ok") or ws in connect_list: # 连接验证
- loggers.info("connect with blower success")
- await ws.send_str(json.dumps(msgs.connect_ok().to_dict()))
-
- await asyncio.sleep(0.5)
- dms: dict
-
- while 1:
- await asyncio.sleep(0.5)
- async for dms in plugin_system.get_plugin_message_aiohttp(ret["param"], ws):
- # 过滤消息,并分析消息
- mdm = await plugin_system.message_filter(dms)
- await plugin_system.message_analyzer(mdm)
-
- loggers.debug(f"sending message {mdm}")
-
- # 发送消息
- await ws.send_str(json.dumps(mdm))
-
-
- else:
- # 认证失败就关闭连接
- loggers.error("connect failed,unexpected client")
- await ws.close()
-
- finally:
- # 后续的处理
- await ws.close()
- await plugin_system.remove_connect_in_id_dict_aiohttp(ws)
- return ws
-
-
-async def websocket_main(configs):
- """
- websocket主函数,通过configs传递参数字典
- """
- loggers.info("websocket server started")
- try:
- await server.serve(websockets, configs["host"], configs["websocket"]["port"])
- except Exception as e:
- loggers.error(f"websockets haven't started currently,because of {str(e)}")
diff --git a/build/lib/plugin/bilibili_dm_plugin/__init__.py b/build/lib/plugin/bilibili_dm_plugin/__init__.py
deleted file mode 100644
index 9da4502..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/__init__.py
+++ /dev/null
@@ -1,178 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-import asyncio
-
-try_imp = True
-import sys
-
-sys.path.append("./")
-
-import os
-from urllib.parse import urlparse
-from . import blivedm
-from depends import pluginmain, msgs
-import shutil
-
-import aiohttp
-from aiohttp import web
-from typing import *
-import http.cookies
-from .blivedm.clients import ws_base
-from .blivedm.models import web as web_models
-import logging
-
-logger = logging.getLogger(__name__)
-local_path = __path__[0]
-
-
-async def download_file(url, file_name):
- async with aiohttp.ClientSession() as session:
- async with session.get(url) as response:
- with open(file_name, 'wb') as file:
- while True:
- chunk = await response.content.read(1024)
- if not chunk:
- break
- file.write(chunk)
-
-
-async def return_for_face(path: str):
- if path:
- files = os.listdir(os.path.join(local_path, "tmp"))
- file = os.path.basename(urlparse(path).path)
- if file in files:
- return web.FileResponse(os.path.join(local_path, "tmp", file))
- else:
- await download_file(path, os.path.join(local_path, "tmp", file))
- return web.FileResponse(os.path.join(local_path, "tmp", file))
-
-
-class Handler(blivedm.BaseHandler):
- def __init__(self, lists: list):
- self.user_face: dict = {}
- self.lists = lists
-
- def _on_danmaku(self, client: blivedm.BLiveClient, message):
- logger.info(f'[{client.room_id}] {message.uname}:{message.msg}')
- self.user_face[message.uname] = ""
- peop_type = {0: 0, 1: 1, 2: 2, 3: 3}
- message = msgs.msg_box(
- message_class="default",
- message_body=msgs.dm(
- msg=message.msg,
- who=msgs.msg_who(
- type=peop_type[message.privilege_type] if message.admin == 0 else 5,
- name=message.uname,
- face="/cgi/b_dm_plugin/face?url=" + ""
- ).to_dict()
- ).to_dict(),
- msg_type="dm"
- ).to_dict()
- self.lists.append(message)
-
- def _on_gift(self, client: ws_base.WebSocketClientBase, message: web_models.GiftMessage):
- logger.info(f'[{client.room_id}] {message.uname} 赠送{message.gift_name}x{message.num}'
- f' ({message.coin_type}瓜子x{message.total_coin})')
- peop_type = {0: 0, 1: 1, 2: 2, 3: 3}
- message = msgs.msg_box(
- message_class="default",
- message_body=msgs.info(
- msg=f"感谢{message.uname}赠送的{message.gift_name}",
- who=msgs.msg_who(
- type=peop_type[message.guard_level],
- name=message.uname,
- face="/cgi/b_dm_plugin/face?url=" + message.face
- ).to_dict(),
- pic=msgs.pic(
- border=False,
- pic_url="/cgi/b_dm_plugin/gift?item=" + message.gift_name + ".png"
- ).to_dict()
- ).to_dict(),
- msg_type="info"
- ).to_dict()
- self.lists.append(message)
-
-
-# noinspection DuplicatedCode
-class PluginMain(pluginmain.PluginMain):
-
- def plugin_init(self):
- if os.path.exists(os.path.join(local_path, "tmp")):
- shutil.rmtree(os.path.join(local_path, "tmp"))
- os.mkdir(os.path.join(local_path, "tmp"))
- else:
- os.mkdir(os.path.join(local_path, "tmp"))
- self.plugin_name = "b_dm_plugin"
-
- self.sprit_cgi_support = True
- self.sprit_cgi_lists["face"] = self.cgi_face
- self.sprit_cgi_lists["gift"] = self.cgi_gift
- self.read_config()
-
- # print(self.config)
- if "session" in self.config:
- self.SESSDATA = self.config["session"]
- else:
- self.config["session"] = ""
- self.update_config(self.config)
-
- return "message"
-
- async def cgi_face(self, request: web.Request):
- ret = web.Response(status=404, text="no such file")
- ret = await return_for_face(request.rel_url.query.get("url"))
- return ret
-
- async def cgi_gift(self, request: web.Request):
- return web.FileResponse(os.path.join(local_path, "resource", request.rel_url.query.get("item")))
-
- async def plugin_main(self):
- while True:
- await asyncio.sleep(1)
-
- def plugin_callback(self):
- logger.info(f"plugin {__name__} is done")
-
- def dm_iter(self, params: dict) -> object:
- class dm_iter_back:
- def __init__(self, params, session):
- self.messages = []
- if "broom" in params:
- cookies = http.cookies.SimpleCookie()
- cookies['SESSDATA'] = session
- cookies['SESSDATA']['domain'] = 'bilibili.com'
-
- self.session: Optional[aiohttp.ClientSession]
- self.session = aiohttp.ClientSession()
- self.session.cookie_jar.update_cookies(cookies)
-
- self.client = blivedm.BLiveClient(params["broom"], session=self.session)
-
- handler = Handler(self.messages)
- self.client.set_handler(handler)
- self.client.start()
- else:
- logger.error("unexpected room, client will be invalid!")
-
- async def __aiter__(self):
- try:
- yield self.messages.pop()
- except IndexError:
- return
-
- async def callback(self):
- logger.info("blivedm closing")
- if hasattr(self, "client"):
- await self.session.close()
- await self.client.stop_and_close()
-
- return dm_iter_back(params, self.SESSDATA)
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/__init__.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/__init__.py
deleted file mode 100644
index e3e5b60..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-from .handlers import *
-from .clients import *
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/__init__.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/__init__.py
deleted file mode 100644
index 13974f0..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-# -*- coding: utf-8 -*-
-from .web import *
-from .open_live import *
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/open_live.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/open_live.py
deleted file mode 100644
index b94a299..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/open_live.py
+++ /dev/null
@@ -1,293 +0,0 @@
-# -*- coding: utf-8 -*-
-import asyncio
-import datetime
-import hashlib
-import hmac
-import json
-import logging
-import uuid
-from typing import *
-
-import aiohttp
-
-from . import ws_base
-
-__all__ = (
- 'OpenLiveClient',
-)
-
-logger = logging.getLogger('blivedm')
-
-START_URL = 'https://live-open.biliapi.com/v2/app/start'
-HEARTBEAT_URL = 'https://live-open.biliapi.com/v2/app/heartbeat'
-END_URL = 'https://live-open.biliapi.com/v2/app/end'
-
-
-class OpenLiveClient(ws_base.WebSocketClientBase):
- """
- 开放平台客户端
-
- 文档参考:https://open-live.bilibili.com/document/
-
- :param access_key_id: 在开放平台申请的access_key_id
- :param access_key_secret: 在开放平台申请的access_key_secret
- :param app_id: 在开放平台创建的项目ID
- :param room_owner_auth_code: 主播身份码
- :param session: cookie、连接池
- :param heartbeat_interval: 发送连接心跳包的间隔时间(秒)
- :param game_heartbeat_interval: 发送项目心跳包的间隔时间(秒)
- """
-
- def __init__(
- self,
- access_key_id: str,
- access_key_secret: str,
- app_id: int,
- room_owner_auth_code: str,
- *,
- session: Optional[aiohttp.ClientSession] = None,
- heartbeat_interval=30,
- game_heartbeat_interval=20,
- ):
- super().__init__(session, heartbeat_interval)
-
- self._access_key_id = access_key_id
- self._access_key_secret = access_key_secret
- self._app_id = app_id
- self._room_owner_auth_code = room_owner_auth_code
- self._game_heartbeat_interval = game_heartbeat_interval
-
- # 在调用init_room后初始化的字段
- self._room_owner_uid: Optional[int] = None
- """主播用户ID"""
- self._room_owner_open_id: Optional[str] = None
- """主播Open ID"""
- self._host_server_url_list: Optional[List[str]] = []
- """弹幕服务器URL列表"""
- self._auth_body: Optional[str] = None
- """连接弹幕服务器用的认证包内容"""
- self._game_id: Optional[str] = None
- """项目场次ID"""
-
- # 在运行时初始化的字段
- self._game_heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
- """发项目心跳包定时器的handle"""
-
- @property
- def room_owner_uid(self) -> Optional[int]:
- """
- 主播用户ID,调用init_room后初始化
- """
- return self._room_owner_uid
-
- @property
- def room_owner_open_id(self) -> Optional[str]:
- """
- 主播Open ID,调用init_room后初始化
- """
- return self._room_owner_open_id
-
- @property
- def room_owner_auth_code(self):
- """
- 主播身份码
- """
- return self._room_owner_auth_code
-
- @property
- def app_id(self):
- """
- 在开放平台创建的项目ID
- """
- return self._app_id
-
- @property
- def game_id(self) -> Optional[str]:
- """
- 项目场次ID,调用init_room后初始化
- """
- return self._game_id
-
- async def close(self):
- """
- 释放本客户端的资源,调用后本客户端将不可用
- """
- if self.is_running:
- logger.warning('room=%s is calling close(), but client is running', self.room_id)
-
- if self._game_heartbeat_timer_handle is not None:
- self._game_heartbeat_timer_handle.cancel()
- self._game_heartbeat_timer_handle = None
- await self._end_game()
-
- await super().close()
-
- def _request_open_live(self, url, body: dict):
- body_bytes = json.dumps(body).encode('utf-8')
- headers = {
- 'x-bili-accesskeyid': self._access_key_id,
- 'x-bili-content-md5': hashlib.md5(body_bytes).hexdigest(),
- 'x-bili-signature-method': 'HMAC-SHA256',
- 'x-bili-signature-nonce': uuid.uuid4().hex,
- 'x-bili-signature-version': '1.0',
- 'x-bili-timestamp': str(int(datetime.datetime.now().timestamp())),
- }
-
- str_to_sign = '\n'.join(
- f'{key}:{value}'
- for key, value in headers.items()
- )
- signature = hmac.new(
- self._access_key_secret.encode('utf-8'), str_to_sign.encode('utf-8'), hashlib.sha256
- ).hexdigest()
- headers['Authorization'] = signature
-
- headers['Content-Type'] = 'application/json'
- headers['Accept'] = 'application/json'
- return self._session.post(url, headers=headers, data=body_bytes)
-
- async def init_room(self):
- """
- 开启项目,并初始化连接房间需要的字段
-
- :return: 是否成功
- """
- if not await self._start_game():
- return False
-
- if self._game_id != '' and self._game_heartbeat_timer_handle is None:
- self._game_heartbeat_timer_handle = asyncio.get_running_loop().call_later(
- self._game_heartbeat_interval, self._on_send_game_heartbeat
- )
- return True
-
- async def _start_game(self):
- try:
- async with self._request_open_live(
- START_URL,
- {'code': self._room_owner_auth_code, 'app_id': self._app_id}
- ) as res:
- if res.status != 200:
- logger.warning('_start_game() failed, status=%d, reason=%s', res.status, res.reason)
- return False
- data = await res.json()
- if data['code'] != 0:
- logger.warning('_start_game() failed, code=%d, message=%s, request_id=%s',
- data['code'], data['message'], data['request_id'])
- return False
- if not self._parse_start_game(data['data']):
- return False
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('_start_game() failed:')
- return False
- return True
-
- def _parse_start_game(self, data):
- self._game_id = data['game_info']['game_id']
- websocket_info = data['websocket_info']
- self._auth_body = websocket_info['auth_body']
- self._host_server_url_list = websocket_info['wss_link']
- anchor_info = data['anchor_info']
- self._room_id = anchor_info['room_id']
- self._room_owner_uid = anchor_info['uid']
- self._room_owner_open_id = anchor_info['open_id']
- return True
-
- async def _end_game(self):
- """
- 关闭项目。建议关闭客户端时保证调用到这个函数(close会调用),否则可能短时间内无法重复连接同一个房间
- """
- if self._game_id in (None, ''):
- return True
-
- try:
- async with self._request_open_live(
- END_URL,
- {'app_id': self._app_id, 'game_id': self._game_id}
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _end_game() failed, status=%d, reason=%s',
- self._room_id, res.status, res.reason)
- return False
- data = await res.json()
- code = data['code']
- if code != 0:
- if code in (7000, 7003):
- # 项目已经关闭了也算成功
- return True
-
- logger.warning('room=%d _end_game() failed, code=%d, message=%s, request_id=%s',
- self._room_id, code, data['message'], data['request_id'])
- return False
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _end_game() failed:', self._room_id)
- return False
- return True
-
- def _on_send_game_heartbeat(self):
- """
- 定时发送项目心跳包的回调
- """
- self._game_heartbeat_timer_handle = asyncio.get_running_loop().call_later(
- self._game_heartbeat_interval, self._on_send_game_heartbeat
- )
- asyncio.create_task(self._send_game_heartbeat())
-
- async def _send_game_heartbeat(self):
- """
- 发送项目心跳包
- """
- if self._game_id in (None, ''):
- logger.warning('game=%d _send_game_heartbeat() failed, game_id not found', self._game_id)
- return False
-
- try:
- # 保存一下,防止await之后game_id改变
- game_id = self._game_id
- async with self._request_open_live(
- HEARTBEAT_URL,
- {'game_id': game_id}
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _send_game_heartbeat() failed, status=%d, reason=%s',
- self._room_id, res.status, res.reason)
- return False
- data = await res.json()
- code = data['code']
- if code != 0:
- logger.warning('room=%d _send_game_heartbeat() failed, code=%d, message=%s, request_id=%s',
- self._room_id, code, data['message'], data['request_id'])
-
- if code == 7003 and self._game_id == game_id:
- # 项目异常关闭,可能是心跳超时,需要重新开启项目
- self._need_init_room = True
- if self._websocket is not None and not self._websocket.closed:
- await self._websocket.close()
-
- return False
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _send_game_heartbeat() failed:', self._room_id)
- return False
- return True
-
- async def _on_before_ws_connect(self, retry_count):
- """
- 在每次建立连接之前调用,可以用来初始化房间
- """
- # 重连次数太多则重新init_room,保险
- reinit_period = max(3, len(self._host_server_url_list or ()))
- if retry_count > 0 and retry_count % reinit_period == 0:
- self._need_init_room = True
- await super()._on_before_ws_connect(retry_count)
-
- def _get_ws_url(self, retry_count) -> str:
- """
- 返回WebSocket连接的URL,可以在这里做故障转移和负载均衡
- """
- return self._host_server_url_list[retry_count % len(self._host_server_url_list)]
-
- async def _send_auth(self):
- """
- 发送认证包
- """
- await self._websocket.send_bytes(self._make_packet(self._auth_body, ws_base.Operation.AUTH))
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/web.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/web.py
deleted file mode 100644
index a12c297..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/web.py
+++ /dev/null
@@ -1,266 +0,0 @@
-# -*- coding: utf-8 -*-
-import asyncio
-import logging
-from typing import *
-
-import aiohttp
-import yarl
-
-from . import ws_base
-from .. import utils
-
-__all__ = (
- 'BLiveClient',
-)
-
-logger = logging.getLogger('blivedm')
-
-UID_INIT_URL = 'https://api.bilibili.com/x/web-interface/nav'
-BUVID_INIT_URL = 'https://www.bilibili.com/'
-ROOM_INIT_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getInfoByRoom'
-DANMAKU_SERVER_CONF_URL = 'https://api.live.bilibili.com/xlive/web-room/v1/index/getDanmuInfo'
-DEFAULT_DANMAKU_SERVER_LIST = [
- {'host': 'broadcastlv.chat.bilibili.com', 'port': 2243, 'wss_port': 443, 'ws_port': 2244}
-]
-
-
-class BLiveClient(ws_base.WebSocketClientBase):
- """
- web端客户端
-
- :param room_id: URL中的房间ID,可以用短ID
- :param uid: B站用户ID,0表示未登录,None表示自动获取
- :param session: cookie、连接池
- :param heartbeat_interval: 发送心跳包的间隔时间(秒)
- """
-
- def __init__(
- self,
- room_id: int,
- *,
- uid: Optional[int] = None,
- session: Optional[aiohttp.ClientSession] = None,
- heartbeat_interval=30,
- ):
- super().__init__(session, heartbeat_interval)
-
- self._tmp_room_id = room_id
- """用来init_room的临时房间ID,可以用短ID"""
- self._uid = uid
-
- # 在调用init_room后初始化的字段
- self._room_owner_uid: Optional[int] = None
- """主播用户ID"""
- self._host_server_list: Optional[List[dict]] = None
- """
- 弹幕服务器列表
-
- `[{host: "tx-bj4-live-comet-04.chat.bilibili.com", port: 2243, wss_port: 443, ws_port: 2244}, ...]`
- """
- self._host_server_token: Optional[str] = None
- """连接弹幕服务器用的token"""
-
- @property
- def tmp_room_id(self) -> int:
- """
- 构造时传进来的room_id参数
- """
- return self._tmp_room_id
-
- @property
- def room_owner_uid(self) -> Optional[int]:
- """
- 主播用户ID,调用init_room后初始化
- """
- return self._room_owner_uid
-
- @property
- def uid(self) -> Optional[int]:
- """
- 当前登录的用户ID,未登录则为0,调用init_room后初始化
- """
- return self._uid
-
- async def init_room(self):
- """
- 初始化连接房间需要的字段
-
- :return: True代表没有降级,如果需要降级后还可用,重载这个函数返回True
- """
- if self._uid is None:
- if not await self._init_uid():
- logger.warning('room=%d _init_uid() failed', self._tmp_room_id)
- self._uid = 0
-
- if self._get_buvid() == '':
- if not await self._init_buvid():
- logger.warning('room=%d _init_buvid() failed', self._tmp_room_id)
-
- res = True
- if not await self._init_room_id_and_owner():
- res = False
- # 失败了则降级
- self._room_id = self._tmp_room_id
- self._room_owner_uid = 0
-
- if not await self._init_host_server():
- res = False
- # 失败了则降级
- self._host_server_list = DEFAULT_DANMAKU_SERVER_LIST
- self._host_server_token = None
- return res
-
- async def _init_uid(self):
- cookies = self._session.cookie_jar.filter_cookies(yarl.URL(UID_INIT_URL))
- sessdata_cookie = cookies.get('SESSDATA', None)
- if sessdata_cookie is None or sessdata_cookie.value == '':
- # cookie都没有,不用请求了
- self._uid = 0
- return True
-
- try:
- async with self._session.get(
- UID_INIT_URL,
- headers={'User-Agent': utils.USER_AGENT},
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _init_uid() failed, status=%d, reason=%s', self._tmp_room_id,
- res.status, res.reason)
- return False
- data = await res.json()
- if data['code'] != 0:
- if data['code'] == -101:
- # 未登录
- self._uid = 0
- return True
- logger.warning('room=%d _init_uid() failed, message=%s', self._tmp_room_id,
- data['message'])
- return False
-
- data = data['data']
- if not data['isLogin']:
- # 未登录
- self._uid = 0
- else:
- self._uid = data['mid']
- return True
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _init_uid() failed:', self._tmp_room_id)
- return False
-
- def _get_buvid(self):
- cookies = self._session.cookie_jar.filter_cookies(yarl.URL(BUVID_INIT_URL))
- buvid_cookie = cookies.get('buvid3', None)
- if buvid_cookie is None:
- return ''
- return buvid_cookie.value
-
- async def _init_buvid(self):
- try:
- async with self._session.get(
- BUVID_INIT_URL,
- headers={'User-Agent': utils.USER_AGENT},
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _init_buvid() status error, status=%d, reason=%s',
- self._tmp_room_id, res.status, res.reason)
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _init_buvid() exception:', self._tmp_room_id)
- return self._get_buvid() != ''
-
- async def _init_room_id_and_owner(self):
- try:
- async with self._session.get(
- ROOM_INIT_URL,
- headers={'User-Agent': utils.USER_AGENT},
- params={
- 'room_id': self._tmp_room_id
- },
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _init_room_id_and_owner() failed, status=%d, reason=%s', self._tmp_room_id,
- res.status, res.reason)
- return False
- data = await res.json()
- if data['code'] != 0:
- logger.warning('room=%d _init_room_id_and_owner() failed, message=%s', self._tmp_room_id,
- data['message'])
- return False
- if not self._parse_room_init(data['data']):
- return False
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _init_room_id_and_owner() failed:', self._tmp_room_id)
- return False
- return True
-
- def _parse_room_init(self, data):
- room_info = data['room_info']
- self._room_id = room_info['room_id']
- self._room_owner_uid = room_info['uid']
- return True
-
- async def _init_host_server(self):
- try:
- async with self._session.get(
- DANMAKU_SERVER_CONF_URL,
- headers={'User-Agent': utils.USER_AGENT},
- params={
- 'id': self._room_id,
- 'type': 0
- },
- ) as res:
- if res.status != 200:
- logger.warning('room=%d _init_host_server() failed, status=%d, reason=%s', self._room_id,
- res.status, res.reason)
- return False
- data = await res.json()
- if data['code'] != 0:
- logger.warning('room=%d _init_host_server() failed, message=%s', self._room_id, data['message'])
- return False
- if not self._parse_danmaku_server_conf(data['data']):
- return False
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- logger.exception('room=%d _init_host_server() failed:', self._room_id)
- return False
- return True
-
- def _parse_danmaku_server_conf(self, data):
- self._host_server_list = data['host_list']
- self._host_server_token = data['token']
- if not self._host_server_list:
- logger.warning('room=%d _parse_danmaku_server_conf() failed: host_server_list is empty', self._room_id)
- return False
- return True
-
- async def _on_before_ws_connect(self, retry_count):
- """
- 在每次建立连接之前调用,可以用来初始化房间
- """
- # 重连次数太多则重新init_room,保险
- reinit_period = max(3, len(self._host_server_list or ()))
- if retry_count > 0 and retry_count % reinit_period == 0:
- self._need_init_room = True
- await super()._on_before_ws_connect(retry_count)
-
- def _get_ws_url(self, retry_count) -> str:
- """
- 返回WebSocket连接的URL,可以在这里做故障转移和负载均衡
- """
- host_server = self._host_server_list[retry_count % len(self._host_server_list)]
- return f"wss://{host_server['host']}:{host_server['wss_port']}/sub"
-
- async def _send_auth(self):
- """
- 发送认证包
- """
- auth_params = {
- 'uid': self._uid,
- 'roomid': self._room_id,
- 'protover': 3,
- 'platform': 'web',
- 'type': 2,
- 'buvid': self._get_buvid(),
- }
- if self._host_server_token is not None:
- auth_params['key'] = self._host_server_token
- await self._websocket.send_bytes(self._make_packet(auth_params, ws_base.Operation.AUTH))
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/ws_base.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/ws_base.py
deleted file mode 100644
index 612e609..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/clients/ws_base.py
+++ /dev/null
@@ -1,494 +0,0 @@
-# -*- coding: utf-8 -*-
-import asyncio
-import enum
-import json
-import logging
-import struct
-import zlib
-from typing import *
-
-import aiohttp
-import brotli
-
-from .. import handlers, utils
-
-logger = logging.getLogger('blivedm')
-
-USER_AGENT = (
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
-)
-
-HEADER_STRUCT = struct.Struct('>I2H2I')
-
-
-class HeaderTuple(NamedTuple):
- pack_len: int
- raw_header_size: int
- ver: int
- operation: int
- seq_id: int
-
-
-# WS_BODY_PROTOCOL_VERSION
-class ProtoVer(enum.IntEnum):
- NORMAL = 0
- HEARTBEAT = 1
- DEFLATE = 2
- BROTLI = 3
-
-
-# go-common\app\service\main\broadcast\model\operation.go
-class Operation(enum.IntEnum):
- HANDSHAKE = 0
- HANDSHAKE_REPLY = 1
- HEARTBEAT = 2
- HEARTBEAT_REPLY = 3
- SEND_MSG = 4
- SEND_MSG_REPLY = 5
- DISCONNECT_REPLY = 6
- AUTH = 7
- AUTH_REPLY = 8
- RAW = 9
- PROTO_READY = 10
- PROTO_FINISH = 11
- CHANGE_ROOM = 12
- CHANGE_ROOM_REPLY = 13
- REGISTER = 14
- REGISTER_REPLY = 15
- UNREGISTER = 16
- UNREGISTER_REPLY = 17
- # B站业务自定义OP
- # MinBusinessOp = 1000
- # MaxBusinessOp = 10000
-
-
-# WS_AUTH
-class AuthReplyCode(enum.IntEnum):
- OK = 0
- TOKEN_ERROR = -101
-
-
-class InitError(Exception):
- """初始化失败"""
-
-
-class AuthError(Exception):
- """认证失败"""
-
-
-DEFAULT_RECONNECT_POLICY = utils.make_constant_retry_policy(1)
-
-
-class WebSocketClientBase:
- """
- 基于WebSocket的客户端
-
- :param session: cookie、连接池
- :param heartbeat_interval: 发送心跳包的间隔时间(秒)
- """
-
- def __init__(
- self,
- session: Optional[aiohttp.ClientSession] = None,
- heartbeat_interval: float = 30,
- ):
- if session is None:
- self._session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=10))
- self._own_session = True
- else:
- self._session = session
- self._own_session = False
- assert self._session.loop is asyncio.get_event_loop() # noqa
-
- self._heartbeat_interval = heartbeat_interval
-
- self._need_init_room = True
- self._handler: Optional[handlers.HandlerInterface] = None
- """消息处理器"""
- self._get_reconnect_interval: Callable[[int, int], float] = DEFAULT_RECONNECT_POLICY
- """重连间隔时间增长策略"""
-
- # 在调用init_room后初始化的字段
- self._room_id: Optional[int] = None
-
- # 在运行时初始化的字段
- self._websocket: Optional[aiohttp.ClientWebSocketResponse] = None
- """WebSocket连接"""
- self._network_future: Optional[asyncio.Future] = None
- """网络协程的future"""
- self._heartbeat_timer_handle: Optional[asyncio.TimerHandle] = None
- """发心跳包定时器的handle"""
-
- @property
- def is_running(self) -> bool:
- """
- 本客户端正在运行,注意调用stop后还没完全停止也算正在运行
- """
- return self._network_future is not None
-
- @property
- def room_id(self) -> Optional[int]:
- """
- 房间ID,调用init_room后初始化
- """
- return self._room_id
-
- def set_handler(self, handler: Optional['handlers.HandlerInterface']):
- """
- 设置消息处理器
-
- 注意消息处理器和网络协程运行在同一个协程,如果处理消息耗时太长会阻塞接收消息。如果是CPU密集型的任务,建议将消息推到线程池处理;
- 如果是IO密集型的任务,应该使用async函数,并且在handler里使用create_task创建新的协程
-
- :param handler: 消息处理器
- """
- self._handler = handler
-
- def set_reconnect_policy(self, get_reconnect_interval: Callable[[int, int], float]):
- """
- 设置重连间隔时间增长策略
-
- :param get_reconnect_interval: 一个可调用对象,输入重试次数 (retry_count, total_retry_count),返回间隔时间
- """
- self._get_reconnect_interval = get_reconnect_interval
-
- def start(self):
- """
- 启动本客户端
- """
- if self.is_running:
- logger.warning('room=%s client is running, cannot start() again', self.room_id)
- return
-
- self._network_future = asyncio.create_task(self._network_coroutine_wrapper())
-
- def stop(self):
- """
- 停止本客户端
- """
- if not self.is_running:
- logger.warning('room=%s client is stopped, cannot stop() again', self.room_id)
- return
-
- self._network_future.cancel()
-
- async def stop_and_close(self):
- """
- 便利函数,停止本客户端并释放本客户端的资源,调用后本客户端将不可用
- """
- if self.is_running:
- self.stop()
- await self.join()
- await self.close()
-
- async def join(self):
- """
- 等待本客户端停止
- """
- if not self.is_running:
- logger.warning('room=%s client is stopped, cannot join()', self.room_id)
- return
-
- await asyncio.shield(self._network_future)
-
- async def close(self):
- """
- 释放本客户端的资源,调用后本客户端将不可用
- """
- if self.is_running:
- logger.warning('room=%s is calling close(), but client is running', self.room_id)
-
- # 如果session是自己创建的则关闭session
- if self._own_session:
- await self._session.close()
-
- async def init_room(self) -> bool:
- """
- 初始化连接房间需要的字段
-
- :return: True代表没有降级,如果需要降级后还可用,重载这个函数返回True
- """
- raise NotImplementedError
-
- @staticmethod
- def _make_packet(data: Union[dict, str, bytes], operation: int) -> bytes:
- """
- 创建一个要发送给服务器的包
-
- :param data: 包体JSON数据
- :param operation: 操作码,见Operation
- :return: 整个包的数据
- """
- if isinstance(data, dict):
- body = json.dumps(data).encode('utf-8')
- elif isinstance(data, str):
- body = data.encode('utf-8')
- else:
- body = data
- header = HEADER_STRUCT.pack(*HeaderTuple(
- pack_len=HEADER_STRUCT.size + len(body),
- raw_header_size=HEADER_STRUCT.size,
- ver=1,
- operation=operation,
- seq_id=1
- ))
- return header + body
-
- async def _network_coroutine_wrapper(self):
- """
- 负责处理网络协程的异常,网络协程具体逻辑在_network_coroutine里
- """
- exc = None
- try:
- await self._network_coroutine()
- except asyncio.CancelledError:
- # 正常停止
- pass
- except Exception as e:
- logger.exception('room=%s _network_coroutine() finished with exception:', self.room_id)
- exc = e
- finally:
- logger.debug('room=%s _network_coroutine() finished', self.room_id)
- self._network_future = None
-
- if self._handler is not None:
- self._handler.on_client_stopped(self, exc)
-
- async def _network_coroutine(self):
- """
- 网络协程,负责连接服务器、接收消息、解包
- """
- # retry_count在连接成功后会重置为0,total_retry_count不会
- retry_count = 0
- total_retry_count = 0
- while True:
- try:
- await self._on_before_ws_connect(retry_count)
-
- # 连接
- async with self._session.ws_connect(
- self._get_ws_url(retry_count),
- headers={'User-Agent': utils.USER_AGENT}, # web端的token也会签名UA
- receive_timeout=self._heartbeat_interval + 5,
- ) as websocket:
- self._websocket = websocket
- await self._on_ws_connect()
-
- # 处理消息
- message: aiohttp.WSMessage
- async for message in websocket:
- await self._on_ws_message(message)
- # 至少成功处理1条消息
- retry_count = 0
-
- except (aiohttp.ClientConnectionError, asyncio.TimeoutError):
- # 掉线重连
- pass
- except AuthError:
- # 认证失败了,应该重新获取token再重连
- logger.exception('room=%d auth failed, trying init_room() again', self.room_id)
- self._need_init_room = True
- finally:
- self._websocket = None
- await self._on_ws_close()
-
- # 准备重连
- retry_count += 1
- total_retry_count += 1
- logger.warning(
- 'room=%d is reconnecting, retry_count=%d, total_retry_count=%d',
- self.room_id, retry_count, total_retry_count
- )
- await asyncio.sleep(self._get_reconnect_interval(retry_count, total_retry_count))
-
- async def _on_before_ws_connect(self, retry_count):
- """
- 在每次建立连接之前调用,可以用来初始化房间
- """
- if not self._need_init_room:
- return
-
- if not await self.init_room():
- raise InitError('init_room() failed')
- self._need_init_room = False
-
- def _get_ws_url(self, retry_count) -> str:
- """
- 返回WebSocket连接的URL,可以在这里做故障转移和负载均衡
- """
- raise NotImplementedError
-
- async def _on_ws_connect(self):
- """
- WebSocket连接成功
- """
- await self._send_auth()
- self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
- self._heartbeat_interval, self._on_send_heartbeat
- )
-
- async def _on_ws_close(self):
- """
- WebSocket连接断开
- """
- if self._heartbeat_timer_handle is not None:
- self._heartbeat_timer_handle.cancel()
- self._heartbeat_timer_handle = None
-
- async def _send_auth(self):
- """
- 发送认证包
- """
- raise NotImplementedError
-
- def _on_send_heartbeat(self):
- """
- 定时发送心跳包的回调
- """
- if self._websocket is None or self._websocket.closed:
- self._heartbeat_timer_handle = None
- return
-
- self._heartbeat_timer_handle = asyncio.get_running_loop().call_later(
- self._heartbeat_interval, self._on_send_heartbeat
- )
- asyncio.create_task(self._send_heartbeat())
-
- async def _send_heartbeat(self):
- """
- 发送心跳包
- """
- if self._websocket is None or self._websocket.closed:
- return
-
- try:
- await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
- except (ConnectionResetError, aiohttp.ClientConnectionError) as e:
- logger.warning('room=%d _send_heartbeat() failed: %r', self.room_id, e)
- except Exception: # noqa
- logger.exception('room=%d _send_heartbeat() failed:', self.room_id)
-
- async def _on_ws_message(self, message: aiohttp.WSMessage):
- """
- 收到WebSocket消息
-
- :param message: WebSocket消息
- """
- if message.type != aiohttp.WSMsgType.BINARY:
- logger.warning('room=%d unknown websocket message type=%s, data=%s', self.room_id,
- message.type, message.data)
- return
-
- try:
- await self._parse_ws_message(message.data)
- except AuthError:
- # 认证失败,让外层处理
- raise
- except Exception: # noqa
- logger.exception('room=%d _parse_ws_message() error:', self.room_id)
-
- async def _parse_ws_message(self, data: bytes):
- """
- 解析WebSocket消息
-
- :param data: WebSocket消息数据
- """
- offset = 0
- try:
- header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset))
- except struct.error:
- logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data)
- return
-
- if header.operation in (Operation.SEND_MSG_REPLY, Operation.AUTH_REPLY):
- # 业务消息,可能有多个包一起发,需要分包
- while True:
- body = data[offset + header.raw_header_size: offset + header.pack_len]
- await self._parse_business_message(header, body)
-
- offset += header.pack_len
- if offset >= len(data):
- break
-
- try:
- header = HeaderTuple(*HEADER_STRUCT.unpack_from(data, offset))
- except struct.error:
- logger.exception('room=%d parsing header failed, offset=%d, data=%s', self.room_id, offset, data)
- break
-
- elif header.operation == Operation.HEARTBEAT_REPLY:
- # 服务器心跳包,前4字节是人气值,后面是客户端发的心跳包内容
- # pack_len不包括客户端发的心跳包内容,不知道是不是服务器BUG
- body = data[offset + header.raw_header_size: offset + header.raw_header_size + 4]
- popularity = int.from_bytes(body, 'big')
- # 自己造个消息当成业务消息处理
- body = {
- 'cmd': '_HEARTBEAT',
- 'data': {
- 'popularity': popularity
- }
- }
- self._handle_command(body)
-
- else:
- # 未知消息
- body = data[offset + header.raw_header_size: offset + header.pack_len]
- logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id,
- header.operation, header, body)
-
- async def _parse_business_message(self, header: HeaderTuple, body: bytes):
- """
- 解析业务消息
- """
- if header.operation == Operation.SEND_MSG_REPLY:
- # 业务消息
- if header.ver == ProtoVer.BROTLI:
- # 压缩过的先解压,为了避免阻塞网络线程,放在其他线程执行
- body = await asyncio.get_running_loop().run_in_executor(None, brotli.decompress, body)
- await self._parse_ws_message(body)
- elif header.ver == ProtoVer.DEFLATE:
- # web端已经不用zlib压缩了,但是开放平台会用
- body = await asyncio.get_running_loop().run_in_executor(None, zlib.decompress, body)
- await self._parse_ws_message(body)
- elif header.ver == ProtoVer.NORMAL:
- # 没压缩过的直接反序列化,因为有万恶的GIL,这里不能并行避免阻塞
- if len(body) != 0:
- try:
- body = json.loads(body.decode('utf-8'))
- self._handle_command(body)
- except Exception:
- logger.error('room=%d, body=%s', self.room_id, body)
- raise
- else:
- # 未知格式
- logger.warning('room=%d unknown protocol version=%d, header=%s, body=%s', self.room_id,
- header.ver, header, body)
-
- elif header.operation == Operation.AUTH_REPLY:
- # 认证响应
- body = json.loads(body.decode('utf-8'))
- if body['code'] != AuthReplyCode.OK:
- raise AuthError(f"auth reply error, code={body['code']}, body={body}")
- await self._websocket.send_bytes(self._make_packet({}, Operation.HEARTBEAT))
-
- else:
- # 未知消息
- logger.warning('room=%d unknown message operation=%d, header=%s, body=%s', self.room_id,
- header.operation, header, body)
-
- def _handle_command(self, command: dict):
- """
- 处理业务消息
-
- :param command: 业务消息
- """
- if self._handler is None:
- return
- try:
- # 为什么不做成异步的:
- # 1. 为了保持处理消息的顺序,这里不使用call_soon、create_task等方法延迟处理
- # 2. 如果支持handle使用async函数,用户可能会在里面处理耗时很长的异步操作,导致网络协程阻塞
- # 这里做成同步的,强制用户使用create_task或消息队列处理异步操作,这样就不会阻塞网络协程
- self._handler.handle(self, command)
- except Exception as e:
- logger.exception('room=%d _handle_command() failed, command=%s', self.room_id, command, exc_info=e)
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/handlers.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/handlers.py
deleted file mode 100644
index 76caca6..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/handlers.py
+++ /dev/null
@@ -1,199 +0,0 @@
-# -*- coding: utf-8 -*-
-import logging
-from typing import *
-
-from .clients import ws_base
-from .models import web as web_models, open_live as open_models
-
-__all__ = (
- 'HandlerInterface',
- 'BaseHandler',
-)
-
-logger = logging.getLogger('blivedm')
-
-logged_unknown_cmds = {
- 'COMBO_SEND',
- 'ENTRY_EFFECT',
- 'HOT_RANK_CHANGED',
- 'HOT_RANK_CHANGED_V2',
- 'INTERACT_WORD',
- 'LIVE',
- 'LIVE_INTERACTIVE_GAME',
- 'NOTICE_MSG',
- 'ONLINE_RANK_COUNT',
- 'ONLINE_RANK_TOP3',
- 'ONLINE_RANK_V2',
- 'PK_BATTLE_END',
- 'PK_BATTLE_FINAL_PROCESS',
- 'PK_BATTLE_PROCESS',
- 'PK_BATTLE_PROCESS_NEW',
- 'PK_BATTLE_SETTLE',
- 'PK_BATTLE_SETTLE_USER',
- 'PK_BATTLE_SETTLE_V2',
- 'PREPARING',
- 'ROOM_REAL_TIME_MESSAGE_UPDATE',
- 'STOP_LIVE_ROOM_LIST',
- 'SUPER_CHAT_MESSAGE_JPN',
- 'WIDGET_BANNER',
-}
-"""已打日志的未知cmd"""
-
-
-class HandlerInterface:
- """
- 直播消息处理器接口
- """
-
- def handle(self, client: ws_base.WebSocketClientBase, command: dict):
- raise NotImplementedError
-
- def on_client_stopped(self, client: ws_base.WebSocketClientBase, exception: Optional[Exception]):
- """
- 当客户端停止时调用。可以在这里close或者重新start
- """
-
-
-def _make_msg_callback(method_name, message_cls):
- def callback(self: 'BaseHandler', client: ws_base.WebSocketClientBase, command: dict):
- method = getattr(self, method_name)
- return method(client, message_cls.from_command(command['data']))
- return callback
-
-
-class BaseHandler(HandlerInterface):
- """
- 一个简单的消息处理器实现,带消息分发和消息类型转换。继承并重写_on_xxx方法即可实现自己的处理器
- """
-
- def __danmu_msg_callback(self, client: ws_base.WebSocketClientBase, command: dict):
- return self._on_danmaku(client, web_models.DanmakuMessage.from_command(command['info']))
-
- _CMD_CALLBACK_DICT: Dict[
- str,
- Optional[Callable[
- ['BaseHandler', ws_base.WebSocketClientBase, dict],
- Any
- ]]
- ] = {
- # 收到心跳包,这是blivedm自造的消息,原本的心跳包格式不一样
- '_HEARTBEAT': _make_msg_callback('_on_heartbeat', web_models.HeartbeatMessage),
- # 收到弹幕
- # go-common\app\service\live\live-dm\service\v1\send.go
- 'DANMU_MSG': __danmu_msg_callback,
- # 有人送礼
- 'SEND_GIFT': _make_msg_callback('_on_gift', web_models.GiftMessage),
- # 有人上舰
- 'GUARD_BUY': _make_msg_callback('_on_buy_guard', web_models.GuardBuyMessage),
- # 醒目留言
- 'SUPER_CHAT_MESSAGE': _make_msg_callback('_on_super_chat', web_models.SuperChatMessage),
- # 删除醒目留言
- 'SUPER_CHAT_MESSAGE_DELETE': _make_msg_callback('_on_super_chat_delete', web_models.SuperChatDeleteMessage),
-
- #
- # 开放平台消息
- #
-
- # 收到弹幕
- 'LIVE_OPEN_PLATFORM_DM': _make_msg_callback('_on_open_live_danmaku', open_models.DanmakuMessage),
- # 有人送礼
- 'LIVE_OPEN_PLATFORM_SEND_GIFT': _make_msg_callback('_on_open_live_gift', open_models.GiftMessage),
- # 有人上舰
- 'LIVE_OPEN_PLATFORM_GUARD': _make_msg_callback('_on_open_live_buy_guard', open_models.GuardBuyMessage),
- # 醒目留言
- 'LIVE_OPEN_PLATFORM_SUPER_CHAT': _make_msg_callback('_on_open_live_super_chat', open_models.SuperChatMessage),
- # 删除醒目留言
- 'LIVE_OPEN_PLATFORM_SUPER_CHAT_DEL': _make_msg_callback(
- '_on_open_live_super_chat_delete', open_models.SuperChatDeleteMessage
- ),
- # 点赞
- 'LIVE_OPEN_PLATFORM_LIKE': _make_msg_callback('_on_open_live_like', open_models.LikeMessage),
- }
- """cmd -> 处理回调"""
-
- def handle(self, client: ws_base.WebSocketClientBase, command: dict):
- cmd = command.get('cmd', '')
- pos = cmd.find(':') # 2019-5-29 B站弹幕升级新增了参数
- if pos != -1:
- cmd = cmd[:pos]
-
- if cmd not in self._CMD_CALLBACK_DICT:
- # 只有第一次遇到未知cmd时打日志
- if cmd not in logged_unknown_cmds:
- logger.warning('room=%d unknown cmd=%s, command=%s', client.room_id, cmd, command)
- logged_unknown_cmds.add(cmd)
- return
-
- callback = self._CMD_CALLBACK_DICT[cmd]
- if callback is not None:
- callback(self, client, command)
-
- def _on_heartbeat(self, client: ws_base.WebSocketClientBase, message: web_models.HeartbeatMessage):
- """
- 收到心跳包
- """
-
- def _on_danmaku(self, client: ws_base.WebSocketClientBase, message: web_models.DanmakuMessage):
- """
- 收到弹幕
- """
-
- def _on_gift(self, client: ws_base.WebSocketClientBase, message: web_models.GiftMessage):
- """
- 收到礼物
- """
-
- def _on_buy_guard(self, client: ws_base.WebSocketClientBase, message: web_models.GuardBuyMessage):
- """
- 有人上舰
- """
-
- def _on_super_chat(self, client: ws_base.WebSocketClientBase, message: web_models.SuperChatMessage):
- """
- 醒目留言
- """
-
- def _on_super_chat_delete(
- self, client: ws_base.WebSocketClientBase, message: web_models.SuperChatDeleteMessage
- ):
- """
- 删除醒目留言
- """
-
- #
- # 开放平台消息
- #
-
- def _on_open_live_danmaku(self, client: ws_base.WebSocketClientBase, message: open_models.DanmakuMessage):
- """
- 收到弹幕
- """
-
- def _on_open_live_gift(self, client: ws_base.WebSocketClientBase, message: open_models.GiftMessage):
- """
- 收到礼物
- """
-
- def _on_open_live_buy_guard(self, client: ws_base.WebSocketClientBase, message: open_models.GuardBuyMessage):
- """
- 有人上舰
- """
-
- def _on_open_live_super_chat(
- self, client: ws_base.WebSocketClientBase, message: open_models.SuperChatMessage
- ):
- """
- 醒目留言
- """
-
- def _on_open_live_super_chat_delete(
- self, client: ws_base.WebSocketClientBase, message: open_models.SuperChatDeleteMessage
- ):
- """
- 删除醒目留言
- """
-
- def _on_open_live_like(self, client: ws_base.WebSocketClientBase, message: open_models.LikeMessage):
- """
- 点赞
- """
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/__init__.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/models/__init__.py
deleted file mode 100644
index 40a96af..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# -*- coding: utf-8 -*-
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/open_live.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/models/open_live.py
deleted file mode 100644
index 09bd248..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/open_live.py
+++ /dev/null
@@ -1,390 +0,0 @@
-# -*- coding: utf-8 -*-
-import dataclasses
-from typing import *
-
-__all__ = (
- 'DanmakuMessage',
- 'GiftMessage',
- 'GuardBuyMessage',
- 'SuperChatMessage',
- 'SuperChatDeleteMessage',
- 'LikeMessage',
-)
-
-# 注释都是复制自官方文档的,看不懂的话问B站
-# https://open-live.bilibili.com/document/f9ce25be-312e-1f4a-85fd-fef21f1637f8
-
-
-@dataclasses.dataclass
-class DanmakuMessage:
- """
- 弹幕消息
- """
-
- uname: str = ''
- """用户昵称"""
- open_id: str = ''
- """用户唯一标识"""
- uface: str = ''
- """用户头像"""
- timestamp: int = 0
- """弹幕发送时间秒级时间戳"""
- room_id: int = 0
- """弹幕接收的直播间"""
- msg: str = ''
- """弹幕内容"""
- msg_id: str = ''
- """消息唯一id"""
- guard_level: int = 0
- """对应房间大航海等级"""
- fans_medal_wearing_status: bool = False
- """该房间粉丝勋章佩戴情况"""
- fans_medal_name: str = ''
- """粉丝勋章名"""
- fans_medal_level: int = 0
- """对应房间勋章信息"""
- emoji_img_url: str = ''
- """表情包图片地址"""
- dm_type: int = 0
- """弹幕类型 0:普通弹幕 1:表情包弹幕"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- uname=data['uname'],
- open_id=data['open_id'],
- uface=data['uface'],
- timestamp=data['timestamp'],
- room_id=data['room_id'],
- msg=data['msg'],
- msg_id=data['msg_id'],
- guard_level=data['guard_level'],
- fans_medal_wearing_status=data['fans_medal_wearing_status'],
- fans_medal_name=data['fans_medal_name'],
- fans_medal_level=data['fans_medal_level'],
- emoji_img_url=data['emoji_img_url'],
- dm_type=data['dm_type'],
- )
-
-
-@dataclasses.dataclass
-class AnchorInfo:
- """
- 主播信息
- """
-
- uid: int = 0
- """收礼主播uid"""
- open_id: str = ''
- """收礼主播唯一标识"""
- uname: str = ''
- """收礼主播昵称"""
- uface: str = ''
- """收礼主播头像"""
-
- @classmethod
- def from_dict(cls, data: dict):
- return cls(
- uid=data['uid'],
- open_id=data['open_id'],
- uname=data['uname'],
- uface=data['uface'],
- )
-
-
-@dataclasses.dataclass
-class ComboInfo:
- """
- 连击信息
- """
-
- combo_base_num: int = 0
- """每次连击赠送的道具数量"""
- combo_count: int = 0
- """连击次数"""
- combo_id: str = ''
- """连击id"""
- combo_timeout: int = 0
- """连击有效期秒"""
-
- @classmethod
- def from_dict(cls, data: dict):
- return cls(
- combo_base_num=data['combo_base_num'],
- combo_count=data['combo_count'],
- combo_id=data['combo_id'],
- combo_timeout=data['combo_timeout'],
- )
-
-
-@dataclasses.dataclass
-class GiftMessage:
- """
- 礼物消息
- """
-
- room_id: int = 0
- """房间号"""
- open_id: str = ''
- """用户唯一标识"""
- uname: str = ''
- """送礼用户昵称"""
- uface: str = ''
- """送礼用户头像"""
- gift_id: int = 0
- """道具id(盲盒:爆出道具id)"""
- gift_name: str = ''
- """道具名(盲盒:爆出道具名)"""
- gift_num: int = 0
- """赠送道具数量"""
- price: int = 0
- """礼物爆出单价,(1000 = 1元 = 10电池),盲盒:爆出道具的价值"""
- paid: bool = False
- """是否是付费道具"""
- fans_medal_level: int = 0
- """实际送礼人的勋章信息"""
- fans_medal_name: str = ''
- """粉丝勋章名"""
- fans_medal_wearing_status: bool = False
- """该房间粉丝勋章佩戴情况"""
- guard_level: int = 0
- """大航海等级"""
- timestamp: int = 0
- """收礼时间秒级时间戳"""
- anchor_info: AnchorInfo = dataclasses.field(default_factory=AnchorInfo)
- """主播信息"""
- msg_id: str = ''
- """消息唯一id"""
- gift_icon: str = ''
- """道具icon"""
- combo_gift: bool = False
- """是否是combo道具"""
- combo_info: ComboInfo = dataclasses.field(default_factory=ComboInfo)
- """连击信息"""
-
- @classmethod
- def from_command(cls, data: dict):
- combo_info = data.get('combo_info', None)
- if combo_info is None:
- combo_info = ComboInfo()
- else:
- combo_info = ComboInfo.from_dict(combo_info)
-
- return cls(
- room_id=data['room_id'],
- open_id=data['open_id'],
- uname=data['uname'],
- uface=data['uface'],
- gift_id=data['gift_id'],
- gift_name=data['gift_name'],
- gift_num=data['gift_num'],
- price=data['price'],
- paid=data['paid'],
- fans_medal_level=data['fans_medal_level'],
- fans_medal_name=data['fans_medal_name'],
- fans_medal_wearing_status=data['fans_medal_wearing_status'],
- guard_level=data['guard_level'],
- timestamp=data['timestamp'],
- anchor_info=AnchorInfo.from_dict(data['anchor_info']),
- msg_id=data['msg_id'],
- gift_icon=data['gift_icon'],
- combo_gift=data.get('combo_gift', False), # 官方的调试工具没发这个字段
- combo_info=combo_info, # 官方的调试工具没发这个字段
- )
-
-
-@dataclasses.dataclass
-class UserInfo:
- """
- 用户信息
- """
-
- open_id: str = ''
- """用户唯一标识"""
- uname: str = ''
- """用户昵称"""
- uface: str = ''
- """用户头像"""
-
- @classmethod
- def from_dict(cls, data: dict):
- return cls(
- open_id=data['open_id'],
- uname=data['uname'],
- uface=data['uface'],
- )
-
-
-@dataclasses.dataclass
-class GuardBuyMessage:
- """
- 上舰消息
- """
-
- user_info: UserInfo = dataclasses.field(default_factory=UserInfo)
- """用户信息"""
- guard_level: int = 0
- """大航海等级"""
- guard_num: int = 0
- """大航海数量"""
- guard_unit: str = ''
- """大航海单位"""
- price: int = 0
- """大航海金瓜子"""
- fans_medal_level: int = 0
- """粉丝勋章等级"""
- fans_medal_name: str = ''
- """粉丝勋章名"""
- fans_medal_wearing_status: bool = False
- """该房间粉丝勋章佩戴情况"""
- room_id: int = 0
- """房间号"""
- msg_id: str = ''
- """消息唯一id"""
- timestamp: int = 0
- """上舰时间秒级时间戳"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- user_info=UserInfo.from_dict(data['user_info']),
- guard_level=data['guard_level'],
- guard_num=data['guard_num'],
- guard_unit=data['guard_unit'],
- price=data['price'],
- fans_medal_level=data['fans_medal_level'],
- fans_medal_name=data['fans_medal_name'],
- fans_medal_wearing_status=data['fans_medal_wearing_status'],
- room_id=data['room_id'],
- msg_id=data['msg_id'],
- timestamp=data['timestamp'],
- )
-
-
-@dataclasses.dataclass
-class SuperChatMessage:
- """
- 醒目留言消息
- """
-
- room_id: int = 0
- """直播间id"""
- open_id: str = ''
- """用户唯一标识"""
- uname: str = ''
- """购买的用户昵称"""
- uface: str = ''
- """购买用户头像"""
- message_id: int = 0
- """留言id(风控场景下撤回留言需要)"""
- message: str = ''
- """留言内容"""
- rmb: int = 0
- """支付金额(元)"""
- timestamp: int = 0
- """赠送时间秒级"""
- start_time: int = 0
- """生效开始时间"""
- end_time: int = 0
- """生效结束时间"""
- guard_level: int = 0
- """对应房间大航海等级"""
- fans_medal_level: int = 0
- """对应房间勋章信息"""
- fans_medal_name: str = ''
- """对应房间勋章名字"""
- fans_medal_wearing_status: bool = False
- """该房间粉丝勋章佩戴情况"""
- msg_id: str = ''
- """消息唯一id"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- room_id=data['room_id'],
- open_id=data['open_id'],
- uname=data['uname'],
- uface=data['uface'],
- message_id=data['message_id'],
- message=data['message'],
- rmb=data['rmb'],
- timestamp=data['timestamp'],
- start_time=data['start_time'],
- end_time=data['end_time'],
- guard_level=data['guard_level'],
- fans_medal_level=data['fans_medal_level'],
- fans_medal_name=data['fans_medal_name'],
- fans_medal_wearing_status=data['fans_medal_wearing_status'],
- msg_id=data['msg_id'],
- )
-
-
-@dataclasses.dataclass
-class SuperChatDeleteMessage:
- """
- 删除醒目留言消息
- """
-
- room_id: int = 0
- """直播间id"""
- message_ids: List[int] = dataclasses.field(default_factory=list)
- """留言id"""
- msg_id: str = ''
- """消息唯一id"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- room_id=data['room_id'],
- message_ids=data['message_ids'],
- msg_id=data['msg_id'],
- )
-
-
-@dataclasses.dataclass
-class LikeMessage:
- """
- 点赞消息
-
- 请注意:用户端每分钟触发若干次的情况下只会推送一次该消息
- """
-
- uname: str = ''
- """用户昵称"""
- open_id: str = ''
- """用户唯一标识"""
- uface: str = ''
- """用户头像"""
- timestamp: int = 0
- """时间秒级时间戳"""
- room_id: int = 0
- """发生的直播间"""
- like_text: str = ''
- """点赞文案(“xxx点赞了”)"""
- like_count: int = 0
- """对单个用户最近2秒的点赞次数聚合"""
- fans_medal_wearing_status: bool = False
- """该房间粉丝勋章佩戴情况"""
- fans_medal_name: str = ''
- """粉丝勋章名"""
- fans_medal_level: int = 0
- """对应房间勋章信息"""
- msg_id: str = '' # 官方文档表格里没列出这个字段,但是参考JSON里面有
- """消息唯一id"""
- # 还有个guard_level,但官方文档没有出现这个字段,就不添加了
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- uname=data['uname'],
- open_id=data['open_id'],
- uface=data['uface'],
- timestamp=data['timestamp'],
- room_id=data['room_id'],
- like_text=data['like_text'],
- like_count=data['like_count'],
- fans_medal_wearing_status=data['fans_medal_wearing_status'],
- fans_medal_name=data['fans_medal_name'],
- fans_medal_level=data['fans_medal_level'],
- msg_id=data.get('msg_id', ''), # 官方文档表格里没列出这个字段,但是参考JSON里面有
- )
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/web.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/models/web.py
deleted file mode 100644
index 1473f49..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/models/web.py
+++ /dev/null
@@ -1,390 +0,0 @@
-# -*- coding: utf-8 -*-
-import dataclasses
-import json
-from typing import *
-
-__all__ = (
- 'HeartbeatMessage',
- 'DanmakuMessage',
- 'GiftMessage',
- 'GuardBuyMessage',
- 'SuperChatMessage',
- 'SuperChatDeleteMessage',
-)
-
-
-@dataclasses.dataclass
-class HeartbeatMessage:
- """
- 心跳消息
- """
-
- popularity: int = 0
- """人气值,已废弃"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- popularity=data['popularity'],
- )
-
-
-@dataclasses.dataclass
-class DanmakuMessage:
- """
- 弹幕消息
- """
-
- mode: int = 0
- """弹幕显示模式(滚动、顶部、底部)"""
- font_size: int = 0
- """字体尺寸"""
- color: int = 0
- """颜色"""
- timestamp: int = 0
- """时间戳(毫秒)"""
- rnd: int = 0
- """随机数,前端叫作弹幕ID,可能是去重用的"""
- uid_crc32: str = ''
- """用户ID文本的CRC32"""
- msg_type: int = 0
- """是否礼物弹幕(节奏风暴)"""
- bubble: int = 0
- """右侧评论栏气泡"""
- dm_type: int = 0
- """弹幕类型,0文本,1表情,2语音"""
- emoticon_options: Union[dict, str] = ''
- """表情参数"""
- voice_config: Union[dict, str] = ''
- """语音参数"""
- mode_info: dict = dataclasses.field(default_factory=dict)
- """一些附加参数"""
-
- msg: str = ''
- """弹幕内容"""
-
- uid: int = 0
- """用户ID"""
- uname: str = ''
- """用户名"""
- admin: int = 0
- """是否房管"""
- vip: int = 0
- """是否月费老爷"""
- svip: int = 0
- """是否年费老爷"""
- urank: int = 0
- """用户身份,用来判断是否正式会员,猜测非正式会员为5000,正式会员为10000"""
- mobile_verify: int = 0
- """是否绑定手机"""
- uname_color: str = ''
- """用户名颜色"""
-
- medal_level: str = ''
- """勋章等级"""
- medal_name: str = ''
- """勋章名"""
- runame: str = ''
- """勋章房间主播名"""
- medal_room_id: int = 0
- """勋章房间ID"""
- mcolor: int = 0
- """勋章颜色"""
- special_medal: str = ''
- """特殊勋章"""
-
- user_level: int = 0
- """用户等级"""
- ulevel_color: int = 0
- """用户等级颜色"""
- ulevel_rank: str = ''
- """用户等级排名,>50000时为'>50000'"""
-
- old_title: str = ''
- """旧头衔"""
- title: str = ''
- """头衔"""
-
- privilege_type: int = 0
- """舰队类型,0非舰队,1总督,2提督,3舰长"""
-
- @classmethod
- def from_command(cls, info: list):
- if len(info[3]) != 0:
- medal_level = info[3][0]
- medal_name = info[3][1]
- runame = info[3][2]
- room_id = info[3][3]
- mcolor = info[3][4]
- special_medal = info[3][5]
- else:
- medal_level = 0
- medal_name = ''
- runame = ''
- room_id = 0
- mcolor = 0
- special_medal = 0
-
- if len(info[5]) != 0:
- old_title = info[5][0]
- title = info[5][1]
- else:
- old_title = ''
- title = ''
-
- return cls(
- mode=info[0][1],
- font_size=info[0][2],
- color=info[0][3],
- timestamp=info[0][4],
- rnd=info[0][5],
- uid_crc32=info[0][7],
- msg_type=info[0][9],
- bubble=info[0][10],
- dm_type=info[0][12],
- emoticon_options=info[0][13],
- voice_config=info[0][14],
- mode_info=info[0][15],
-
- msg=info[1],
-
- uid=info[2][0],
- uname=info[2][1],
- admin=info[2][2],
- vip=info[2][3],
- svip=info[2][4],
- urank=info[2][5],
- mobile_verify=info[2][6],
- uname_color=info[2][7],
-
- medal_level=medal_level,
- medal_name=medal_name,
- runame=runame,
- medal_room_id=room_id,
- mcolor=mcolor,
- special_medal=special_medal,
-
- user_level=info[4][0],
- ulevel_color=info[4][2],
- ulevel_rank=info[4][3],
-
- old_title=old_title,
- title=title,
-
- privilege_type=info[7],
- )
-
- @property
- def emoticon_options_dict(self) -> dict:
- """
- 示例:
- {'bulge_display': 0, 'emoticon_unique': 'official_13', 'height': 60, 'in_player_area': 1, 'is_dynamic': 1,
- 'url': 'https://i0.hdslb.com/bfs/live/a98e35996545509188fe4d24bd1a56518ea5af48.png', 'width': 183}
- """
- if isinstance(self.emoticon_options, dict):
- return self.emoticon_options
- try:
- return json.loads(self.emoticon_options)
- except (json.JSONDecodeError, TypeError):
- return {}
-
- @property
- def voice_config_dict(self) -> dict:
- """
- 示例:
- {'voice_url': 'https%3A%2F%2Fboss.hdslb.com%2Flive-dm-voice%2Fb5b26e48b556915cbf3312a59d3bb2561627725945.wav
- %3FX-Amz-Algorithm%3DAWS4-HMAC-SHA256%26X-Amz-Credential%3D2663ba902868f12f%252F20210731%252Fshjd%252Fs3%25
- 2Faws4_request%26X-Amz-Date%3D20210731T100545Z%26X-Amz-Expires%3D600000%26X-Amz-SignedHeaders%3Dhost%26
- X-Amz-Signature%3D114e7cb5ac91c72e231c26d8ca211e53914722f36309b861a6409ffb20f07ab8',
- 'file_format': 'wav', 'text': '汤,下午好。', 'file_duration': 1}
- """
- if isinstance(self.voice_config, dict):
- return self.voice_config
- try:
- return json.loads(self.voice_config)
- except (json.JSONDecodeError, TypeError):
- return {}
-
-
-@dataclasses.dataclass
-class GiftMessage:
- """
- 礼物消息
- """
-
- gift_name: str = ''
- """礼物名"""
- num: int = 0
- """数量"""
- uname: str = ''
- """用户名"""
- face: str = ''
- """用户头像URL"""
- guard_level: int = 0
- """舰队等级,0非舰队,1总督,2提督,3舰长"""
- uid: int = 0
- """用户ID"""
- timestamp: int = 0
- """时间戳"""
- gift_id: int = 0
- """礼物ID"""
- gift_type: int = 0
- """礼物类型(未知)"""
- action: str = ''
- """目前遇到的有'喂食'、'赠送'"""
- price: int = 0
- """礼物单价瓜子数"""
- rnd: str = ''
- """随机数,可能是去重用的。有时是时间戳+去重ID,有时是UUID"""
- coin_type: str = ''
- """瓜子类型,'silver'或'gold',1000金瓜子 = 1元"""
- total_coin: int = 0
- """总瓜子数"""
- tid: str = ''
- """可能是事务ID,有时和rnd相同"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- gift_name=data['giftName'],
- num=data['num'],
- uname=data['uname'],
- face=data['face'],
- guard_level=data['guard_level'],
- uid=data['uid'],
- timestamp=data['timestamp'],
- gift_id=data['giftId'],
- gift_type=data['giftType'],
- action=data['action'],
- price=data['price'],
- rnd=data['rnd'],
- coin_type=data['coin_type'],
- total_coin=data['total_coin'],
- tid=data['tid'],
- )
-
-
-@dataclasses.dataclass
-class GuardBuyMessage:
- """
- 上舰消息
- """
-
- uid: int = 0
- """用户ID"""
- username: str = ''
- """用户名"""
- guard_level: int = 0
- """舰队等级,0非舰队,1总督,2提督,3舰长"""
- num: int = 0
- """数量"""
- price: int = 0
- """单价金瓜子数"""
- gift_id: int = 0
- """礼物ID"""
- gift_name: str = ''
- """礼物名"""
- start_time: int = 0
- """开始时间戳,和结束时间戳相同"""
- end_time: int = 0
- """结束时间戳,和开始时间戳相同"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- uid=data['uid'],
- username=data['username'],
- guard_level=data['guard_level'],
- num=data['num'],
- price=data['price'],
- gift_id=data['gift_id'],
- gift_name=data['gift_name'],
- start_time=data['start_time'],
- end_time=data['end_time'],
- )
-
-
-@dataclasses.dataclass
-class SuperChatMessage:
- """
- 醒目留言消息
- """
-
- price: int = 0
- """价格(人民币)"""
- message: str = ''
- """消息"""
- message_trans: str = ''
- """消息日文翻译(目前只出现在SUPER_CHAT_MESSAGE_JPN)"""
- start_time: int = 0
- """开始时间戳"""
- end_time: int = 0
- """结束时间戳"""
- time: int = 0
- """剩余时间(约等于 结束时间戳 - 开始时间戳)"""
- id: int = 0
- """醒目留言ID,删除时用"""
- gift_id: int = 0
- """礼物ID"""
- gift_name: str = ''
- """礼物名"""
- uid: int = 0
- """用户ID"""
- uname: str = ''
- """用户名"""
- face: str = ''
- """用户头像URL"""
- guard_level: int = 0
- """舰队等级,0非舰队,1总督,2提督,3舰长"""
- user_level: int = 0
- """用户等级"""
- background_bottom_color: str = ''
- """底部背景色,'#rrggbb'"""
- background_color: str = ''
- """背景色,'#rrggbb'"""
- background_icon: str = ''
- """背景图标"""
- background_image: str = ''
- """背景图URL"""
- background_price_color: str = ''
- """背景价格颜色,'#rrggbb'"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- price=data['price'],
- message=data['message'],
- message_trans=data['message_trans'],
- start_time=data['start_time'],
- end_time=data['end_time'],
- time=data['time'],
- id=data['id'],
- gift_id=data['gift']['gift_id'],
- gift_name=data['gift']['gift_name'],
- uid=data['uid'],
- uname=data['user_info']['uname'],
- face=data['user_info']['face'],
- guard_level=data['user_info']['guard_level'],
- user_level=data['user_info']['user_level'],
- background_bottom_color=data['background_bottom_color'],
- background_color=data['background_color'],
- background_icon=data['background_icon'],
- background_image=data['background_image'],
- background_price_color=data['background_price_color'],
- )
-
-
-@dataclasses.dataclass
-class SuperChatDeleteMessage:
- """
- 删除醒目留言消息
- """
-
- ids: List[int] = dataclasses.field(default_factory=list)
- """醒目留言ID数组"""
-
- @classmethod
- def from_command(cls, data: dict):
- return cls(
- ids=data['ids'],
- )
diff --git a/build/lib/plugin/bilibili_dm_plugin/blivedm/utils.py b/build/lib/plugin/bilibili_dm_plugin/blivedm/utils.py
deleted file mode 100644
index 2c66f6a..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/blivedm/utils.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# -*- coding: utf-8 -*-
-USER_AGENT = (
- 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36'
-)
-
-
-def make_constant_retry_policy(interval: float):
- def get_interval(_retry_count: int, _total_retry_count: int):
- return interval
- return get_interval
-
-
-def make_linear_retry_policy(start_interval: float, interval_step: float, max_interval: float):
- def get_interval(retry_count: int, _total_retry_count: int):
- return min(
- start_interval + (retry_count - 1) * interval_step,
- max_interval
- )
- return get_interval
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/configs.py b/build/lib/plugin/bilibili_dm_plugin/depends/configs.py
deleted file mode 100644
index 0f66a43..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/configs.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import yaml
-
-
-def config() -> dict:
- with open("./config/system/config.yaml", "r", encoding="utf-8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- if configs["debug"] == 1:
- print(f"log:already reading config file: {configs}\n")
- return configs
-
-
-def set_config(config_family: str, config: dict) -> bool:
- """
- 设置插件的配置
- """
- try:
- with open(f"./config/plugin/{config_family}/config.yaml", "w", encoding="utf_8") as f:
- yaml.dump(data=config, stream=f, allow_unicode=True)
- except Exception as e:
- print(str(e))
- return False
- finally:
- return True
-
-def read_config(config_family: str) -> dict:
- """
- 读取插件的配置
- """
- with open(f"./config/plugin/{config_family}/config.yaml", "r", encoding="utf_8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- return configs
-
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/connects.py b/build/lib/plugin/bilibili_dm_plugin/depends/connects.py
deleted file mode 100644
index ed230e7..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/connects.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-from websockets.server import WebSocketServerProtocol
-
-
-class connect_wrapper:
-
- """
- 连接包装类
- """
-
- def __init__(self, connect: WebSocketServerProtocol):
- self.__connect__ = connect
- self.id = connect.id # 连接id
- self.open = connect.open # 连接状态
-
- def refresh(self):
- """
- 刷新状态
- """
- self.id = self.__connect__.id
- self.open = self.__connect__.open
-
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/logger.py b/build/lib/plugin/bilibili_dm_plugin/depends/logger.py
deleted file mode 100644
index b36b276..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/logger.py
+++ /dev/null
@@ -1,15 +0,0 @@
-import logging
-
-
-def setup_logging(config: dict):
- """
- Setup logging configuration
- """
-
- level_dic: dict = {"DEBUG": logging.DEBUG,
- "INFO": logging.INFO,
- "WARNING": logging.WARNING,
- "ERROR": logging.ERROR,
- "CRITICAL": logging.CRITICAL,
- "FATAL": logging.FATAL}
- logging.basicConfig(level=level_dic[config["loglevel"]], format="[%(asctime)s,%(name)s] %(levelname)s : %(message)s")
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/msgs.py b/build/lib/plugin/bilibili_dm_plugin/depends/msgs.py
deleted file mode 100644
index 3147026..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/msgs.py
+++ /dev/null
@@ -1,111 +0,0 @@
-"""
-这个文件内定义全部的标准消息模板
-
-所有的函数都应该又to_dict方法来将其转为字典,相反的这个函数能输出打包好的字典
-别问我为啥,问就是不知道
-"""
-
-
-class datas:
- def to_dict(self, cls: object):
- return dict(cls)
-
-
-class connect_ok:
- """
- 连接认证消息
- """
- code = 200
- msg = "connect ok"
-
- def to_dict(self):
- return {"code": self.code,
- "msg": self.msg}
-
-
-class dm:
- def __init__(self, msg: str, who: dict):
- """
- params:
- msg:str 消息主体
- who:dict 消息发出者对象(其实是一个字典)
-
- 成员方法:
- to_dict: 输出为字典
- """
- self.msg = msg
- self.who = who
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who}
-
-
-class info:
- def __init__(self,
- msg: str,
- who: str,
- pic: dict):
- self.msg = msg
- self.who = who
- self.pic = pic
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who,
- "pic": self.pic}
-
-
-class socket_responce:
- def __init__(self, config: dict):
- self.code = 200
- self.local = "ws://{}:{}".format(config["host"], config["websocket"]["port"])
-
- def to_dict(self):
- return {"code": self.code,
- "local": self.local}
-
-
-class msg_who:
- def __init__(self, type: int,
- name: str,
- face: str):
- self.type = type
- self.name = name
- self.face = face
-
- def to_dict(self):
- return {"name": self.name,
- "type": self.type,
- "face": self.face}
-
-
-class pic:
- def __init__(self,
- border: bool,
- pic_url: str):
- self.border = border
- self.pic_url = pic_url
-
- def to_dict(self):
- return {"border": self.border,
- "pic_url": self.pic_url}
-
-
-class msg_box:
- """
- 消息标准封装所用的类
- """
-
- def __init__(self,
- message_class: str,
- msg_type: str,
- message_body: dict):
- self.message_class = message_class
- self.msg_type = msg_type
- self.message_body = message_body
-
- def to_dict(self):
- return {"message_class": self.message_class,
- "msg_type": self.msg_type,
- "message_body": self.message_body}
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/plugin_errors.py b/build/lib/plugin/bilibili_dm_plugin/depends/plugin_errors.py
deleted file mode 100644
index 7404e76..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/plugin_errors.py
+++ /dev/null
@@ -1,19 +0,0 @@
-class PluginTypeError(Exception):
- def __init__(self, message):
- self.message = message
- super().__init__(self.message)
-
-
-class UnexpectedPluginMessage(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMessage, self).__init__(message)
-
-
-class UnexpectedPluginMather(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMather, self).__init__(message)
-
-
-class NoMainMather(Exception):
- def __init__(self, message):
- super(NoMainMather, self).__init__(message)
diff --git a/build/lib/plugin/bilibili_dm_plugin/depends/plugin_main.py b/build/lib/plugin/bilibili_dm_plugin/depends/plugin_main.py
deleted file mode 100644
index 9a7490b..0000000
--- a/build/lib/plugin/bilibili_dm_plugin/depends/plugin_main.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-from . import plugin_errors, msgs
-import asyncio
-import typing
-from . import configs as configs
-from . import connects
-from aiohttp import web
-
-
-class Plugin_Main:
-
- @typing.final
- def __init__(self):
- """
- 不要用这个而是用plugin_init来进行插件的初始化,这个仅供内部使用
- """
- self.stop: bool = False
- self.plugin_js_sprit_support: bool = False # js插件支持
- self.plugin_js_sprit: str = "" # js插件
-
- self.type: str = str() # 插件类型
-
- self.config: dict = dict() # 配置
-
- self.sprit_cgi_support = False # 插件cgi支持
-
- self.sprit_cgi_lists: {} = {} # cgi列表
-
- self.plugin_name: str = "" # 插件名称
-
- self.web: web = web # web模块
-
- if self.plugin_type() == "message":
- self.message_list = []
-
- def plugin_init(self) -> str:
- """
- 插件开始被加载时调用
- 父函数本身不实现任何功能
- 需要返回插件类型给插件系统以判断插件类型
- (实际上这个类就是个异步迭代器)
-
- return:如果是”message“则表示这是个消息提供插件,如果是”analyzer“则表示这个插件是用来获取中间消息并且进行处理的。
- """
- self.stop = 0
- raise plugin_errors.UnexpectedPluginMessage('插件入口方法没有实现')
-
- async def plugin_main(self):
- """
- 插件的主方法,此方法停止时插件也会被视为运行完毕
- """
- pass
-
- async def message_filter(self, message) -> msgs.msg_box:
- """
-
- 消息过滤器,用于自动处理消息,比如翻译或者敏感词过滤
- :param message:待处理的消息
- :return 消息
-
- """
- return message
-
- async def message_anaylazer(self, message):
-
- """
- 消息分析
- """
-
- pass
-
- async def sprit_cgi(self, request):
- """
- 脚本cgi接口
- :param request:请求对象
- :return 响应,用aiohttp的就行(已经封装为self.web)
- """
- if self.sprit_cgi_support:
- raise plugin_errors.UnexpectedPluginMather("未实现的插件方法")
-
- def dm_iter(self, params: dict, connect_waper: connects.connect_wrapper) -> object:
- """
- 返回弹幕迭代对象
- :param params: 前端的get参数
- :param connect_waper: 连接信息
- :return 消息迭代对象
-
- """
- return self
-
- @typing.final
- def update_config(self):
- """
- 更新配置,将自身的配置写入文件并且保存在计算机上
- """
- assert self.plugin_name != ""
- configs.set_config(self.plugin_name, self.config)
-
- @typing.final
- def read_config(self):
- """
- 读取配置
- """
- assert self.plugin_name != ""
- self.config = configs.read_config(self.plugin_name)
-
- def __aiter__(self):
- if self.plugin_type() == "message":
- return self
-
- async def __anext__(self):
- if self.plugin_type() == "message":
- if self.message_list:
- return self.message_list.pop(0)
- else:
- raise StopAsyncIteration()
-
- @typing.final
- def plugin_stop(self):
- """
- 插件停止
- """
- self.stop = 1
- asyncio.current_task().cancel()
-
- def plugin_callback(self):
-
- """
- 插件回调
- """
-
- print(f"plugin is done")
-
- @typing.final
- def plugin_getconfig(self) -> dict:
- """
- 获取配置
- """
- return configs.config()
-
- @typing.final
- def plugin_type(self) -> str:
- """
- 获取插件类型
- """
- # 不存在则初始化插件类型,并返回插件类型给软件以判断插件类型
- # 不存在插件类型则表示插件没有被加载,返回插件没有被加载的错误信息给软件以判断插件是否被加载
- if not self.type:
- self.type = self.plugin_init()
- return self.type
diff --git a/build/lib/pluginsystem.py b/build/lib/pluginsystem.py
deleted file mode 100644
index 1af8e51..0000000
--- a/build/lib/pluginsystem.py
+++ /dev/null
@@ -1,206 +0,0 @@
-# Copyright (c) 2023 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-import asyncio
-from depends import pluginmain, plugin_errors
-import logging
-import os
-import importlib
-import importlib.util
-from websockets.server import WebSocketServerProtocol
-import aiohttp.web as web
-from depends import connects
-
-global_config = {} # 配置
-
-
-class Plugin:
- def __init__(self):
- mlogger = logging.getLogger(__name__)
-
- self.logger = mlogger
-
- self.message_plugin_list: list = [] # 消息插件列表
- self.analyzer_plugin_list: list = [] # 分析插件列表
- self.connect_id_dict: dict[any, list] = {} # 连接id——消息对象,为了防止重复向插件申请迭代对象
- self.plugin_cgi_support: dict = {} # 消息插件cgi
- self.plugin_js_support: dict = {} # js支持字典
- self.connect_id_dict_aiohttp = {} # 连接id——消息对象,为了防止重复向插件申请迭代对象(aiohttp)
-
- # 加载默认插件目录
- self.__lod_init_plugin__()
-
- def __lod_init_plugin__(self):
- plugin_name = ""
- for plugin_file in os.listdir(global_config['plugins']['default_path']):
- try:
- # 排除一些非法的文件和缓存目录,还要同时保障能够加载软件包
- if os.path.splitext(plugin_file)[1] == ".py" or os.path.isdir(
- os.path.join(global_config['plugins']['default_path'],
- plugin_file)) and not plugin_file == "__pycache__":
-
- plugin_name = os.path.splitext(plugin_file)[0]
- path_name = os.path.basename(global_config['plugins']['default_path'])
-
- self.logger.info(f"found a plugin '{plugin_name}' in dir {path_name}")
-
- plugin_module = importlib.import_module(f'{path_name}.{plugin_name}')
-
- # 合法性检查
- if not hasattr(plugin_module, "PluginMain"):
- raise plugin_errors.NoMainMather("函数未实现主方法或者主方法名称错误")
-
- plugin_class = getattr(plugin_module, "PluginMain")
- plugin_interface: pluginmain.PluginMain = plugin_class()
-
- # 获取插件类型
- if plugin_interface.plugin_type() == "message":
- self.message_plugin_list.append(plugin_interface)
- elif plugin_interface.plugin_type() == "analyzer":
- self.analyzer_plugin_list.append(plugin_interface)
- else:
- raise plugin_errors.PluginTypeError("未知的插件类型,该不会是插件吃了金克拉了吧?")
-
- # 注册脚本cgi接口
- if plugin_interface.sprit_cgi_support:
- self.plugin_cgi_support[plugin_interface.plugin_name] = plugin_interface.sprit_cgi_lists
- # 注册插件js
- if plugin_interface.plugin_js_sprit_support:
- self.plugin_js_support[plugin_interface.plugin_name] = plugin_interface.plugin_js_sprit
-
- except IndexError as e:
- self.logger.error(f"failed to import plugin :\n{plugin_name} {str(e)}")
-
- async def remove_connect_in_id_dict(self, id):
- """
- 当连接关闭时,移除连接
- :param id 连接id
- """
- for i in self.connect_id_dict[id]:
- if hasattr(i, "callback"):
- await i.callback()
- return self.connect_id_dict.pop(id, False)
-
- async def remove_connect_in_id_dict_aiohttp(self, id):
- """
- 当连接关闭时,移除连接
- :param id 连接id
- """
- for i in self.connect_id_dict_aiohttp[id]:
- if hasattr(i, "callback"):
- await i.callback()
- return self.connect_id_dict_aiohttp.pop(id, False)
-
- async def get_plugin_message(self, params, connect: WebSocketServerProtocol):
- """
- 弹幕对象迭代器,迭代对应参数的弹幕
- """
- if connect.id in self.connect_id_dict.keys():
- # 已经缓存消息迭代器
- for dm_iter in self.connect_id_dict[connect.id]:
- async for _dm in dm_iter:
- self.logger.debug("get a dm:", _dm)
- yield _dm
- else:
- # 获取未缓存的消息迭代器
- self.connect_id_dict[connect.id] = []
- for plugin in self.message_plugin_list:
- dm = plugin.dm_iter(params)
- if dm is None:
- continue
- self.connect_id_dict[connect.id].append(dm)
-
- async for _dm in dm:
- self.logger.debug("get a dm:", _dm)
- yield _dm
-
- async def get_plugin_message_aiohttp(self, params, connect: web.WebSocketResponse):
- """
- 弹幕对象迭代器,迭代对应参数的弹幕
- """
- if connect in self.connect_id_dict_aiohttp.keys():
- # 已经缓存消息迭代器
- for dm_iter in self.connect_id_dict_aiohttp[connect]:
- async for _dm in dm_iter:
- self.logger.debug("get a dm:", _dm)
- yield _dm
- else:
- # 获取未缓存的消息迭代器
- self.connect_id_dict_aiohttp[connect] = []
- for plugin in self.message_plugin_list:
- dm = plugin.dm_iter(params)
- if dm is None:
- continue
- self.connect_id_dict_aiohttp[connect].append(dm)
-
- async for _dm in dm:
- self.logger.debug("get a dm:", _dm)
- yield _dm
-
- async def message_analyzer(self, message):
- # 消息分析插件
- for plugins in self.analyzer_plugin_list:
- plugins.message_anaylazer(message)
-
- async def message_filter(self, message) -> dict:
- """
- 消息过滤方法
- :param message: 消息对象
- :return: 处理后的消息对象
- :rtype: Message_Object
- """
- message_filtered = message
- for plugins in self.analyzer_plugin_list:
- if not plugins:
- try:
- plugins.message_filter(message_filtered)
- except Exception as e:
- self.logger.error(f"a error is happened:{str(e)}")
- return message_filtered
-
- async def plugin_main_runner(self):
- """
- 运行插件主方法
- """
- message_plugin = []
- anaylazer_tasks = []
- for plugin in self.analyzer_plugin_list:
- tsk = asyncio.get_event_loop().create_task(plugin.plugin_main())
- tsk.add_done_callback(lambda l: asyncio.ensure_future(self.analyzer_plugin_callback(plugin)))
- anaylazer_tasks.append(tsk)
- for plugin in self.message_plugin_list:
- tsk = asyncio.get_event_loop().create_task(plugin.plugin_main())
- tsk.add_done_callback(lambda l: asyncio.ensure_future(self.message_plugin_call_back(plugin)))
- message_plugin.append(tsk)
-
- while True:
- for tsk in message_plugin:
- if tsk.done():
- message_plugin.remove(tsk)
- for tsk in anaylazer_tasks:
- if tsk.done():
- anaylazer_tasks.remove(tsk)
- await asyncio.sleep(1)
-
- async def message_plugin_call_back(self, obj):
- try:
- obj.plugin_callback()
- except Exception as e:
- self.logger.error(f"a error is happened:{str(e)}")
- self.message_plugin_list.remove(obj)
-
- async def analyzer_plugin_callback(self, obj):
- try:
- obj.plugin_callback()
- except Exception as e:
- self.logger.error(f"a error is happened:{str(e)}")
- self.analyzer_plugin_list.remove(obj)
diff --git a/dist/ictye_live_dm-1.0-py2.py3-none-any.whl b/dist/ictye_live_dm-1.0-py2.py3-none-any.whl
deleted file mode 100644
index f4ddd6d..0000000
Binary files a/dist/ictye_live_dm-1.0-py2.py3-none-any.whl and /dev/null differ
diff --git a/dist/ictye_live_dm-1.0.tar.gz b/dist/ictye_live_dm-1.0.tar.gz
deleted file mode 100644
index eb71f89..0000000
Binary files a/dist/ictye_live_dm-1.0.tar.gz and /dev/null differ
diff --git a/icon.png b/icon.png
new file mode 100644
index 0000000..4c02914
Binary files /dev/null and b/icon.png differ
diff --git a/ictye-live-dm/pyproject.toml b/ictye-live-dm/pyproject.toml
index e8d44c3..1bc6d3d 100644
--- a/ictye-live-dm/pyproject.toml
+++ b/ictye-live-dm/pyproject.toml
@@ -53,10 +53,11 @@ exclude = [
]
[[tool.hatch.build.hooks.build-scripts.scripts]]
-out_dir = ".\\src\\ictye_live_dm\\GUI"
-work_dir = ".\\src\\ictye_live_dm\\QT-GUI"
+out_dir = "./src/ictye_live_dm/GUI"
+work_dir = "./src/ictye_live_dm/QT-GUI"
commands = [
- "pyuic5 .\\main.ui -o ..\\GUI\\Ui_MainWindow.py"
+ "pyuic5 ./main.ui -o ../GUI/Ui_MainWindow.py"
+
]
artifacts = [
"Ui_MainWindow.py"
diff --git a/ictye-live-dm/src/ictye_live_dm/GUI/SettingLayOut.py b/ictye-live-dm/src/ictye_live_dm/GUI/SettingLayOut.py
new file mode 100644
index 0000000..d0a47cf
--- /dev/null
+++ b/ictye-live-dm/src/ictye_live_dm/GUI/SettingLayOut.py
@@ -0,0 +1,69 @@
+from PyQt5.QtCore import QSize, QRect, Qt
+from PyQt5.QtWidgets import QLayout, QWidget, QLabel
+
+
+class SettingLayout(QLayout):
+ def __init__(self, parent=None):
+ super(SettingLayout, self).__init__(parent)
+ self.items = {}
+ self.item = {}
+
+ def addItem(self, widget, label=None):
+ self.item[widget] = label
+
+ def addWidget(self, widget, label: str = None):
+ label = QLabel(label)
+ self.items[widget] = label
+ super(SettingLayout, self).addWidget(label)
+ super(SettingLayout, self).addWidget(widget)
+
+ def count(self):
+ return len(self.items)
+
+ def itemAt(self, index):
+ if 0 <= index < len(self.items):
+ return list(self.items.keys())[index]
+ return None
+
+ def takeAt(self, index):
+ if 0 <= index < len(self.items):
+ item = list(self.items.keys())[index]
+ label = self.items.pop(item)
+ return item, label
+ return None, None
+
+ def sizeHint(self):
+ return self.minimumSize()
+
+ def minimumSize(self):
+ min_size = QSize()
+ for widget in self.items:
+ min_size = min_size.expandedTo(widget.minimumSize())
+ return min_size
+
+ def setGeometry(self, rect):
+ super(SettingLayout, self).setGeometry(rect)
+ self.layout(rect)
+
+ def layout(self, rect=QRect()):
+ if not self.items:
+ return
+
+ x = rect.x()
+ y = rect.y()
+
+ width = rect.width()
+ item_height = 50
+
+ items_copy = self.items.copy()
+
+ for widget, label in items_copy.items():
+ if label:
+ label.setGeometry(QRect(x, y + item_height // 2, width // 4, item_height))
+ label.setAlignment(Qt.AlignHCenter)
+ else:
+ continue
+
+ content_area = QRect(x + width // 4 if label else x, y, 2 * width // 4 if label else width, item_height)
+ widget.setGeometry(content_area)
+ y += item_height
diff --git a/ictye-live-dm/src/ictye_live_dm/GUI/StyleSheets/CommonStyle.qss b/ictye-live-dm/src/ictye_live_dm/GUI/StyleSheets/CommonStyle.qss
new file mode 100644
index 0000000..1ba2bff
--- /dev/null
+++ b/ictye-live-dm/src/ictye_live_dm/GUI/StyleSheets/CommonStyle.qss
@@ -0,0 +1,458 @@
+/* ================================================ *
+author:lei
+lastedited:2020.2
+* ================================================ */
+/*hover*/
+/*actived*/
+/*gradient start*/
+/*gradient end*/
+
+QWidget
+{
+ color: #222;
+ background-color: #FDFDFD;
+}
+
+QFrame{
+ color: #222;
+ background-color: #FDFDFD;/*不能设置为transparent*/
+}
+QMainWindow::separator{
+ border: 1px solid #999999;
+ border-style: outset;
+ width: 4px;
+ height: 4px;
+}
+QMainWindow::separator:hover{
+ background: #8BF;
+}
+QSplitter::handle{
+ border: 1px solid #999999;
+ border-style: outset;
+ width: 4px;
+ height: 4px;
+}
+QSplitter::handle:hover{/*splitter->handle(1)->setAttribute(Qt::WA_Hover, true);才生效*/
+ border-color: #EA2;
+}
+QSplitter::handle:pressed{
+ border-color: #59F;
+}
+QSizeGrip{
+ background-color: none;
+}
+
+/* =============================================== */
+/* Label */
+/* =============================================== */
+QLabel {
+ background: transparent;
+ border: 1px solid transparent;
+ padding: 1px;
+}
+
+
+/* A QLabel is a QFrame ... */
+/* A QToolTip is a QLabel ... */
+QToolTip {
+ border: 1px solid #999999;
+ padding: 5px;
+ border-radius: 3px;
+ opacity:210;
+}
+
+/* =============================================== */
+/* TextBox */
+/* =============================================== */
+QLineEdit {
+ background: #FDFDFD;/*不建议设为透明,否则table编辑时会字显示*/
+ selection-background-color: #8BF;
+ border: 1px solid #999999;
+ border-radius: 2px;
+ border-style: inset;
+ padding: 0 1px;
+}
+
+QLineEdit:hover{
+ border-color: #8BF;
+}
+QLineEdit:focus{
+ border-color: #EA2;
+}
+/*QLineEdit[readOnly="true"] { color: gray }*/
+QLineEdit[echoMode="2"]{
+ lineedit-password-character: 9679;/*字符的ascii码35 88等 */
+}
+
+QLineEdit:read-only {
+ color: lightgray;
+}
+
+QLineEdit:disabled{
+ color: lightgray;
+ background: lightgray;
+}
+
+QTextEdit{
+ selection-background-color:#8BF;
+ border: 1px solid #999999;
+ border-style: inset;
+}
+QTextEdit:hover{
+ border-color: #8BF;
+}
+QTextEdit:focus{
+ border-color: #EA2;
+}
+/* =============================================== */
+/* Button */
+/* =============================================== */
+QPushButton {
+ border: 1px solid #999999;
+ border-radius: 2px;
+ padding: 1px 4px;
+ min-width: 50px;
+ min-height: 16px;
+}
+
+QPushButton:disabled{
+ background: #c4c4c4;
+ color:#727273;
+}
+
+QPushButton:hover{
+ background-color: #8BF;
+ border-color: #59F;
+}
+
+QPushButton:pressed
+{
+ border-width: 1px;
+ background-color: #59F;
+ border-color: #999999;
+}
+
+QPushButton:focus, QPushButton:default {
+ border-color: #EA2; /* make the default button prominent */
+}
+
+
+QToolButton,QToolButton:unchecked { /* ToolBar里的按钮和带下拉菜单的按钮 */
+ border: 1px solid transparent;
+ border-radius: 3px;
+ background-color: transparent;
+ margin: 1px;
+}
+
+QToolButton:checked{
+ background-color: #8BF;
+ border-color: #59F;
+}
+QToolButton:hover{
+ background-color: #8BF;
+ border-color: #59F;
+}
+
+QToolButton:pressed,QToolButton:checked:hover{
+ background-color: #59F;
+ border-color: #EA2;
+}
+QToolButton:checked:pressed{
+ background-color: #8BF;
+}
+
+/* only for MenuButtonPopup */
+QToolButton[popupMode="1"]{
+ padding-left: 1px;
+ padding-right: 15px; /* make way for the popup button */
+ border: 1px solid #999999;
+ min-height: 15px;
+ /*background: qlineargradient(x1:0, y1:0 ,x2:0, y2:1
+ stop: 0 #EEEEEF, stop: 0.05 #DADADF, stop: 0.5 #DADADF
+ stop: 0.95 #EEEEEF stop: 1#EEEEEF)*/
+}
+QToolButton[popupMode="1"]:hover{
+ background-color: #8BF;
+ border-color: #59F;
+}
+QToolButton[popupMode="1"]:pressed{
+ border-width: 1px;
+ background-color: #59F;
+ border-color: #999999;
+}
+QToolButton::menu-button {
+ border: 1px solid #999999;
+ border-top-right-radius: 2px;
+ border-bottom-right-radius: 2px;
+ width: 16px;
+}
+
+
+/* =============================================== */
+/* Slider ProgressBar */
+/* =============================================== */
+QProgressBar {
+ border: 1px solid #999999;
+ border-radius: 4px;
+ text-align: center;
+}
+
+QProgressBar::chunk {
+ background-color: #EA2;
+ width: 4px;
+ margin: 1px;
+}
+
+QSlider{
+ border: 1px solid transparent;
+}
+QSlider::groove{
+ border: 1px solid #999999;
+ background: #FDFDFD;
+}
+QSlider::handle {/*设置中间的那个滑动的键*/
+ border: 1px solid #999999;
+ background: #8BF;
+}
+QSlider::groove:horizontal {
+ height: 3px; /* the groove expands to the size of the slider by default. by giving it a height, it has a fixed size */
+ left:5px; right: 5px;
+}
+QSlider::groove:vertical{
+ width: 3px;
+ top: 5px; bottom: 5px;
+}
+QSlider::handle:horizontal{
+ width: 6px;
+ margin: -7px; /* height */
+}
+QSlider::handle:vertical{
+ height: 6px;
+ margin: -7px; /* height */
+}
+QSlider::add-page{/*还没有滑上去的地方*/
+ border: 1px solid #999999;
+ background:#EEEEEF;
+}
+QSlider::sub-page{/*已经划过的从地方*/
+ background: #EA2;
+}
+
+/* =============================================== */
+/* ScrollBar */
+/* =============================================== */
+QScrollBar{
+ background-color: #FDFDFD;
+ border: 1px solid #999999;
+ border-radius: 5px;
+ padding: 1px;
+ height: 10px;
+ width: 10px;
+}
+QScrollBar:hover{
+ border-color:#8BF;
+}
+QScrollBar::handle{
+ border-radius: 3px;
+ background: #59F;
+ min-width: 16px;
+ min-height: 16px;
+}
+QScrollBar::handle:hover {
+ background: #EA2;
+}
+QScrollBar::add-line, QScrollBar::sub-line,
+QScrollBar::add-page, QScrollBar::sub-page {
+ width: 0px;
+ background: transparent;
+}
+QScrollArea{
+ border: none;
+}
+/*QScrollArea QAbstractSlider{
+ border-radius: 0px;
+}*/
+/* =============================================== */
+/* DockWidget */
+/* =============================================== */
+QDockWidget, QDockWidget > QWidget/*not work*/
+{
+ border-color: #999999;/*qt bug*/
+ background: transparent;
+}
+QDockWidget::title {
+ border-bottom: 1px solid #999999;
+ border-style: inset;
+ text-align: left; /* align the text to the left */
+ padding: 6px;
+}
+
+/* =============================================== */
+/* GroupBox */
+/* =============================================== */
+QGroupBox {
+ background-color: #FDFDFD;
+ border: 1px solid #999999;
+ border-radius: 4px;
+ margin-top: 0.5em;
+}
+QGroupBox::title {
+ subcontrol-origin: margin;
+ subcontrol-position: top left;
+ left: 1em;
+ top: 0.1em;
+ background-color: #FDFDFD;
+}
+/* =============================================== */
+/* ToolBox */
+/* =============================================== */
+QToolBox{
+ border: 1px solid #999999;
+}
+QToolBox::tab {
+ background: #EEEEEF;
+ border: 1px solid #999999;
+ border-radius: 1px;
+}
+QToolBox::tab:hover {
+ background-color: #8BF;
+ border-color: transparent;
+}
+QToolBox::tab:pressed {
+ background-color: #59F;
+ border-color: transparent;
+}
+QToolBox::tab:selected {
+ font-weight: bold;
+ border-color: #8BF;
+}
+
+/* =============================================== */
+/* TabWidget */
+/* =============================================== */
+QTabWidget{
+ margin-top:10px;
+}
+QTabWidget::pane{
+ border: 1px solid #999999;
+}
+QTabWidget::tab-bar {
+ left: 0px;
+}
+QTabBar::tab {
+ background: #FDFDFD;
+ border: 1px solid #999999;
+ padding: 3px 5px;
+}
+QTabBar::tab:hover {
+ background: #8BF;
+ border-color: transparent;
+}
+QTabBar::tab:selected {
+ background: #8BF;
+ border-color: #59F;
+}
+QTabBar::tab:pressed {
+ background: #59F;
+ border-color: transparent;
+}
+QTabBar::tab:focus {
+ border-color: #EA2;
+}
+QTabBar::tab:top{
+ margin-top: 3px;
+ border-bottom: transparent;
+ margin-right: 1px;
+}
+QTabBar::tab:bottom{
+ margin-bottom: 3px;
+ border-top: transparent;
+ margin-right: 1px;
+}
+QTabBar::tab:left{
+ border-right: transparent;
+ margin-bottom: 1px;
+}
+QTabBar::tab:right{
+ border-left: transparent;
+ margin-bottom: 1px;
+}
+
+/* =============================================== */
+/* QHeaderView for list table */
+/* =============================================== */
+QHeaderView {
+ border: none;
+ margin: 0px;
+ padding: 0px;
+}
+QHeaderView::section, QTableCornerButton::section {/*设置表头属性*//*左上角*/
+ background-color: #EEEEEF;
+ padding: 0 3px;
+ border-right: 1px solid #999999;
+ border-bottom: 1px solid #999999;
+ border-radius: 0px;
+}
+QHeaderView::section:hover, QTableCornerButton::section:hover{
+ background-color: #8BF;
+}
+QHeaderView::section:pressed{
+ background-color: #59F;
+}
+QHeaderView::section:checked {
+ background-color: #EA2;
+}
+
+/* =============================================== */
+/* QTableWidget */
+/* =============================================== */
+QTableWidget, QTableView
+{
+ gridline-color: #999999; /*表格中的网格线条颜色*/
+ background: #FDFDFD;
+ /*设置交替颜色,需要在函数属性中设置:tableWidget->setAlternatingRowColors(true)*/
+ alternate-background-color: #EEEEEF;
+ /*selection-color:#FDFDFD; 鼠标选中时前景色:文字颜色*/
+ selection-background-color:#8BF; /*鼠标选中时背景色*/
+ border:1px solid #999999; /*边框线的宽度、颜色*/
+ /*border:none; 去除边界线*/
+ /*border-radius:5px;*/
+ /*padding:10px 10px;*/ /*表格与边框的间距*/
+}
+QTableView::item, QTabWidget::item{
+ background: transparent;
+ outline-style: none;
+ border: none;
+}
+
+QTableView::item:hover {
+ background: #8BF;
+ border: 1px solid #EA2;
+}
+
+QTableView::item:selected {
+ background: #8BF;
+ color: #EEEEEF;
+}
+
+QTableView::item:selected:active {
+ background: #59F;
+ color: #EEEEEF;
+}
+
+QTableWidget QComboBox{
+ margin: 2px;
+ border: none;
+}
+
+QCheckBox::indicator:checked{
+ border: 1px solid #999999;
+ border-radius:3px;
+ background-color:#5599ff
+}
+
+QCheckBox::indicator{
+ border: 1px solid #999999;
+ border-radius:3px;
+ background-color:#fff;
+}
\ No newline at end of file
diff --git a/ictye-live-dm/src/ictye_live_dm/GUI/Ui_MainWindow.py b/ictye-live-dm/src/ictye_live_dm/GUI/Ui_MainWindow.py
index 12d7945..1e86467 100644
--- a/ictye-live-dm/src/ictye_live_dm/GUI/Ui_MainWindow.py
+++ b/ictye-live-dm/src/ictye_live_dm/GUI/Ui_MainWindow.py
@@ -1,8 +1,9 @@
# -*- coding: utf-8 -*-
-# Form implementation generated from reading ui file '.\main.ui'
+
+# Form implementation generated from reading ui file './main.ui'
#
-# Created by: PyQt5 UI code generator 5.15.10
+# Created by: PyQt5 UI code generator 5.15.11
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.
@@ -14,11 +15,12 @@
class Ui_Form(object):
def setupUi(self, Form):
Form.setObjectName("Form")
- Form.setWindowModality(QtCore.Qt.WindowModal)
- Form.resize(729, 499)
+ Form.setWindowModality(QtCore.Qt.NonModal)
+ Form.resize(863, 740)
icon = QtGui.QIcon()
icon.addPixmap(QtGui.QPixmap(".\\../ictye_live_dm/icon.ico"), QtGui.QIcon.Normal, QtGui.QIcon.Off)
Form.setWindowIcon(icon)
+ Form.setStyleSheet("")
self.verticalLayout = QtWidgets.QVBoxLayout(Form)
self.verticalLayout.setObjectName("verticalLayout")
self.horizontalLayout = QtWidgets.QHBoxLayout()
@@ -38,7 +40,53 @@ def setupUi(self, Form):
self.horizontalLayout.addWidget(self.stopButton)
self.verticalLayout.addLayout(self.horizontalLayout)
self.tabWidget = QtWidgets.QTabWidget(Form)
+ self.tabWidget.setTabPosition(QtWidgets.QTabWidget.West)
+ self.tabWidget.setTabShape(QtWidgets.QTabWidget.Rounded)
+ self.tabWidget.setElideMode(QtCore.Qt.ElideNone)
+ self.tabWidget.setMovable(False)
+ self.tabWidget.setTabBarAutoHide(False)
self.tabWidget.setObjectName("tabWidget")
+ self.tab_2 = QtWidgets.QWidget()
+ self.tab_2.setObjectName("tab_2")
+ self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.tab_2)
+ self.verticalLayout_6.setObjectName("verticalLayout_6")
+ self.horizontalLayout_6 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_6.setObjectName("horizontalLayout_6")
+ self.label_2 = QtWidgets.QLabel(self.tab_2)
+ self.label_2.setObjectName("label_2")
+ self.horizontalLayout_6.addWidget(self.label_2)
+ spacerItem = QtWidgets.QSpacerItem(228, 28, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_6.addItem(spacerItem)
+ self.StatusLable = QtWidgets.QLabel(self.tab_2)
+ self.StatusLable.setObjectName("StatusLable")
+ self.horizontalLayout_6.addWidget(self.StatusLable)
+ self.status_display_lable = QtWidgets.QLabel(self.tab_2)
+ self.status_display_lable.setObjectName("status_display_lable")
+ self.horizontalLayout_6.addWidget(self.status_display_lable)
+ self.verticalLayout_6.addLayout(self.horizontalLayout_6)
+ self.horizontalLayout_7 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_7.setObjectName("horizontalLayout_7")
+ self.label_3 = QtWidgets.QLabel(self.tab_2)
+ self.label_3.setObjectName("label_3")
+ self.horizontalLayout_7.addWidget(self.label_3)
+ spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_7.addItem(spacerItem1)
+ self.verticalLayout_6.addLayout(self.horizontalLayout_7)
+ self.adress_content_list = QtWidgets.QListWidget(self.tab_2)
+ self.adress_content_list.setObjectName("adress_content_list")
+ self.verticalLayout_6.addWidget(self.adress_content_list)
+ self.horizontalLayout_8 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_8.setObjectName("horizontalLayout_8")
+ self.label_5 = QtWidgets.QLabel(self.tab_2)
+ self.label_5.setObjectName("label_5")
+ self.horizontalLayout_8.addWidget(self.label_5)
+ spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_8.addItem(spacerItem2)
+ self.pushButton_4 = QtWidgets.QPushButton(self.tab_2)
+ self.pushButton_4.setObjectName("pushButton_4")
+ self.horizontalLayout_8.addWidget(self.pushButton_4)
+ self.verticalLayout_6.addLayout(self.horizontalLayout_8)
+ self.tabWidget.addTab(self.tab_2, "")
self.log_Tab = QtWidgets.QWidget()
self.log_Tab.setObjectName("log_Tab")
self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.log_Tab)
@@ -49,7 +97,11 @@ def setupUi(self, Form):
self.totalLogLable.setObjectName("totalLogLable")
self.horizontalLayout_2.addWidget(self.totalLogLable)
self.followCheckBox = QtWidgets.QCheckBox(self.log_Tab)
+ self.followCheckBox.setEnabled(True)
self.followCheckBox.setMaximumSize(QtCore.QSize(194, 16777215))
+ self.followCheckBox.setTabletTracking(False)
+ self.followCheckBox.setChecked(True)
+ self.followCheckBox.setTristate(False)
self.followCheckBox.setObjectName("followCheckBox")
self.horizontalLayout_2.addWidget(self.followCheckBox)
self.verticalLayout_4.addLayout(self.horizontalLayout_2)
@@ -76,9 +128,9 @@ def setupUi(self, Form):
self.pluginListPage.setObjectName("pluginListPage")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.pluginListPage)
self.verticalLayout_2.setObjectName("verticalLayout_2")
- self.label_2 = QtWidgets.QLabel(self.pluginListPage)
- self.label_2.setObjectName("label_2")
- self.verticalLayout_2.addWidget(self.label_2)
+ self.pluginTotalLable = QtWidgets.QLabel(self.pluginListPage)
+ self.pluginTotalLable.setObjectName("pluginTotalLable")
+ self.verticalLayout_2.addWidget(self.pluginTotalLable)
self.horizontalLayout_4 = QtWidgets.QHBoxLayout()
self.horizontalLayout_4.setObjectName("horizontalLayout_4")
self.lineEdit = QtWidgets.QLineEdit(self.pluginListPage)
@@ -88,38 +140,158 @@ def setupUi(self, Form):
self.pushButton.setObjectName("pushButton")
self.horizontalLayout_4.addWidget(self.pushButton)
self.verticalLayout_2.addLayout(self.horizontalLayout_4)
- self.tableWidget = QtWidgets.QTableWidget(self.pluginListPage)
- self.tableWidget.setObjectName("tableWidget")
- self.tableWidget.setColumnCount(3)
- self.tableWidget.setRowCount(0)
+ self.pluginListTable = QtWidgets.QTableWidget(self.pluginListPage)
+ self.pluginListTable.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
+ self.pluginListTable.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
+ self.pluginListTable.setObjectName("pluginListTable")
+ self.pluginListTable.setColumnCount(4)
+ self.pluginListTable.setRowCount(0)
item = QtWidgets.QTableWidgetItem()
- self.tableWidget.setHorizontalHeaderItem(0, item)
+ self.pluginListTable.setHorizontalHeaderItem(0, item)
item = QtWidgets.QTableWidgetItem()
- self.tableWidget.setHorizontalHeaderItem(1, item)
+ self.pluginListTable.setHorizontalHeaderItem(1, item)
item = QtWidgets.QTableWidgetItem()
- self.tableWidget.setHorizontalHeaderItem(2, item)
- self.tableWidget.horizontalHeader().setCascadingSectionResizes(False)
- self.tableWidget.horizontalHeader().setSortIndicatorShown(False)
- self.tableWidget.horizontalHeader().setStretchLastSection(True)
- self.verticalLayout_2.addWidget(self.tableWidget)
+ self.pluginListTable.setHorizontalHeaderItem(2, item)
+ item = QtWidgets.QTableWidgetItem()
+ self.pluginListTable.setHorizontalHeaderItem(3, item)
+ self.pluginListTable.horizontalHeader().setCascadingSectionResizes(False)
+ self.pluginListTable.horizontalHeader().setSortIndicatorShown(False)
+ self.pluginListTable.horizontalHeader().setStretchLastSection(True)
+ self.verticalLayout_2.addWidget(self.pluginListTable)
self.tabWidget.addTab(self.pluginListPage, "")
+ self.tab = QtWidgets.QWidget()
+ self.tab.setObjectName("tab")
+ self.horizontalLayout_12 = QtWidgets.QHBoxLayout(self.tab)
+ self.horizontalLayout_12.setObjectName("horizontalLayout_12")
+ self.scrollArea = QtWidgets.QScrollArea(self.tab)
+ self.scrollArea.setWidgetResizable(True)
+ self.scrollArea.setObjectName("scrollArea")
+ self.scrollAreaWidgetContents_2 = QtWidgets.QWidget()
+ self.scrollAreaWidgetContents_2.setGeometry(QtCore.QRect(0, 0, 789, 651))
+ self.scrollAreaWidgetContents_2.setObjectName("scrollAreaWidgetContents_2")
+ self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents_2)
+ self.formLayout.setObjectName("formLayout")
+ self.label_4 = QtWidgets.QLabel(self.scrollAreaWidgetContents_2)
+ font = QtGui.QFont()
+ font.setPointSize(9)
+ font.setBold(False)
+ font.setWeight(50)
+ self.label_4.setFont(font)
+ self.label_4.setObjectName("label_4")
+ self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_4)
+ self.horizontalLayout_9 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_9.setObjectName("horizontalLayout_9")
+ self.lineEdit_2 = QtWidgets.QLineEdit(self.scrollAreaWidgetContents_2)
+ self.lineEdit_2.setObjectName("lineEdit_2")
+ self.horizontalLayout_9.addWidget(self.lineEdit_2)
+ self.formLayout.setLayout(0, QtWidgets.QFormLayout.FieldRole, self.horizontalLayout_9)
+ self.label_6 = QtWidgets.QLabel(self.scrollAreaWidgetContents_2)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.label_6.setFont(font)
+ self.label_6.setObjectName("label_6")
+ self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.label_6)
+ self.verticalLayout_7 = QtWidgets.QVBoxLayout()
+ self.verticalLayout_7.setObjectName("verticalLayout_7")
+ self.horizontalLayout_10 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_10.setObjectName("horizontalLayout_10")
+ spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_10.addItem(spacerItem3)
+ self.pushButton_5 = QtWidgets.QPushButton(self.scrollAreaWidgetContents_2)
+ self.pushButton_5.setObjectName("pushButton_5")
+ self.horizontalLayout_10.addWidget(self.pushButton_5)
+ self.verticalLayout_7.addLayout(self.horizontalLayout_10)
+ self.listWidget = QtWidgets.QListWidget(self.scrollAreaWidgetContents_2)
+ self.listWidget.setObjectName("listWidget")
+ self.verticalLayout_7.addWidget(self.listWidget)
+ self.formLayout.setLayout(1, QtWidgets.QFormLayout.FieldRole, self.verticalLayout_7)
+ self.label_7 = QtWidgets.QLabel(self.scrollAreaWidgetContents_2)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.label_7.setFont(font)
+ self.label_7.setObjectName("label_7")
+ self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_7)
+ self.comboBox = QtWidgets.QComboBox(self.scrollAreaWidgetContents_2)
+ self.comboBox.setObjectName("comboBox")
+ self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.comboBox)
+ self.label_8 = QtWidgets.QLabel(self.scrollAreaWidgetContents_2)
+ font = QtGui.QFont()
+ font.setBold(False)
+ font.setWeight(50)
+ self.label_8.setFont(font)
+ self.label_8.setObjectName("label_8")
+ self.formLayout.setWidget(3, QtWidgets.QFormLayout.LabelRole, self.label_8)
+ self.checkBox = QtWidgets.QCheckBox(self.scrollAreaWidgetContents_2)
+ self.checkBox.setText("")
+ self.checkBox.setObjectName("checkBox")
+ self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.checkBox)
+ spacerItem4 = QtWidgets.QSpacerItem(702, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
+ self.formLayout.setItem(4, QtWidgets.QFormLayout.FieldRole, spacerItem4)
+ self.scrollArea.setWidget(self.scrollAreaWidgetContents_2)
+ self.horizontalLayout_12.addWidget(self.scrollArea)
+ self.tabWidget.addTab(self.tab, "")
self.setting_Tab = QtWidgets.QWidget()
self.setting_Tab.setObjectName("setting_Tab")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.setting_Tab)
self.verticalLayout_3.setObjectName("verticalLayout_3")
self.settingScrollArea = QtWidgets.QScrollArea(self.setting_Tab)
+ self.settingScrollArea.setLineWidth(3)
+ self.settingScrollArea.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
+ self.settingScrollArea.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff)
+ self.settingScrollArea.setSizeAdjustPolicy(QtWidgets.QAbstractScrollArea.AdjustToContents)
self.settingScrollArea.setWidgetResizable(True)
+ self.settingScrollArea.setAlignment(QtCore.Qt.AlignCenter)
self.settingScrollArea.setObjectName("settingScrollArea")
self.scrollAreaWidgetContents = QtWidgets.QWidget()
- self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 677, 388))
+ self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 789, 651))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
+ self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.scrollAreaWidgetContents)
+ self.verticalLayout_5.setObjectName("verticalLayout_5")
+ self.horizontalLayout_5 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_5.setObjectName("horizontalLayout_5")
+ spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_5.addItem(spacerItem5)
+ self.pushButton_3 = QtWidgets.QPushButton(self.scrollAreaWidgetContents)
+ self.pushButton_3.setObjectName("pushButton_3")
+ self.horizontalLayout_5.addWidget(self.pushButton_3)
+ self.pushButton_2 = QtWidgets.QPushButton(self.scrollAreaWidgetContents)
+ self.pushButton_2.setObjectName("pushButton_2")
+ self.horizontalLayout_5.addWidget(self.pushButton_2)
+ self.verticalLayout_5.addLayout(self.horizontalLayout_5)
+ self.settingTreeWidget = QtWidgets.QTreeWidget(self.scrollAreaWidgetContents)
+ self.settingTreeWidget.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.ArrowCursor))
+ self.settingTreeWidget.setEditTriggers(QtWidgets.QAbstractItemView.DoubleClicked|QtWidgets.QAbstractItemView.EditKeyPressed|QtWidgets.QAbstractItemView.SelectedClicked)
+ self.settingTreeWidget.setObjectName("settingTreeWidget")
+ font = QtGui.QFont()
+ font.setBold(True)
+ font.setWeight(75)
+ self.settingTreeWidget.headerItem().setFont(0, font)
+ self.settingTreeWidget.headerItem().setBackground(0, QtGui.QColor(255, 255, 255))
+ font = QtGui.QFont()
+ font.setBold(True)
+ font.setWeight(75)
+ self.settingTreeWidget.headerItem().setFont(1, font)
+ self.verticalLayout_5.addWidget(self.settingTreeWidget)
+ self.horizontalLayout_3 = QtWidgets.QHBoxLayout()
+ self.horizontalLayout_3.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint)
+ self.horizontalLayout_3.setSpacing(6)
+ self.horizontalLayout_3.setObjectName("horizontalLayout_3")
+ spacerItem6 = QtWidgets.QSpacerItem(90, 20, QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_3.addItem(spacerItem6)
+ self.applyButton = QtWidgets.QPushButton(self.scrollAreaWidgetContents)
+ self.applyButton.setObjectName("applyButton")
+ self.horizontalLayout_3.addWidget(self.applyButton)
+ spacerItem7 = QtWidgets.QSpacerItem(90, 20, QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Minimum)
+ self.horizontalLayout_3.addItem(spacerItem7)
+ self.verticalLayout_5.addLayout(self.horizontalLayout_3)
self.settingScrollArea.setWidget(self.scrollAreaWidgetContents)
self.verticalLayout_3.addWidget(self.settingScrollArea)
self.tabWidget.addTab(self.setting_Tab, "")
self.verticalLayout.addWidget(self.tabWidget)
-
self.retranslateUi(Form)
- self.tabWidget.setCurrentIndex(0)
+ self.tabWidget.setCurrentIndex(3)
QtCore.QMetaObject.connectSlotsByName(Form)
def retranslateUi(self, Form):
@@ -128,7 +300,14 @@ def retranslateUi(self, Form):
self.label.setText(_translate("Form", "本項目由美國聖地亞哥(American Sangdiyagou)獨家贊助研發(x)"))
self.startButtoen.setText(_translate("Form", "Start"))
self.stopButton.setText(_translate("Form", "Stop"))
- self.totalLogLable.setText(_translate("Form", "TextLabel"))
+ self.label_2.setText(_translate("Form", "Home "))
+ self.StatusLable.setText(_translate("Form", "Status:"))
+ self.status_display_lable.setText(_translate("Form", "%1"))
+ self.label_3.setText(_translate("Form", "Address Contents "))
+ self.label_5.setText(_translate("Form", "Location http://127.0.0.1:8082
"))
+ self.pushButton_4.setText(_translate("Form", "open in blsower"))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("Form", "Hone"))
+ self.totalLogLable.setText(_translate("Form", "Total"))
self.followCheckBox.setText(_translate("Form", "Follow newest log"))
self.logTable.setSortingEnabled(False)
item = self.logTable.horizontalHeaderItem(0)
@@ -138,13 +317,26 @@ def retranslateUi(self, Form):
item = self.logTable.horizontalHeaderItem(2)
item.setText(_translate("Form", "Contents"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.log_Tab), _translate("Form", "Log"))
- self.label_2.setText(_translate("Form", "Total:"))
+ self.pluginTotalLable.setText(_translate("Form", "Total:"))
self.pushButton.setText(_translate("Form", "Submit"))
- item = self.tableWidget.horizontalHeaderItem(0)
- item.setText(_translate("Form", "New Column"))
- item = self.tableWidget.horizontalHeaderItem(1)
+ item = self.pluginListTable.horizontalHeaderItem(0)
item.setText(_translate("Form", "Plugin name"))
- item = self.tableWidget.horizontalHeaderItem(2)
+ item = self.pluginListTable.horizontalHeaderItem(1)
+ item.setText(_translate("Form", "Author"))
+ item = self.pluginListTable.horizontalHeaderItem(2)
+ item.setText(_translate("Form", "Version"))
+ item = self.pluginListTable.horizontalHeaderItem(3)
item.setText(_translate("Form", "Description"))
self.tabWidget.setTabText(self.tabWidget.indexOf(self.pluginListPage), _translate("Form", "Plugins"))
- self.tabWidget.setTabText(self.tabWidget.indexOf(self.setting_Tab), _translate("Form", "Setting"))
+ self.label_4.setText(_translate("Form", "Port"))
+ self.label_6.setText(_translate("Form", "Plugins"))
+ self.pushButton_5.setText(_translate("Form", "Add"))
+ self.label_7.setText(_translate("Form", "LogLevel"))
+ self.label_8.setText(_translate("Form", "Debug"))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("Form", "Setting"))
+ self.pushButton_3.setText(_translate("Form", "PushButton"))
+ self.pushButton_2.setText(_translate("Form", "PushButton"))
+ self.settingTreeWidget.headerItem().setText(0, _translate("Form", "Item"))
+ self.settingTreeWidget.headerItem().setText(1, _translate("Form", "Value"))
+ self.applyButton.setText(_translate("Form", "Apply"))
+ self.tabWidget.setTabText(self.tabWidget.indexOf(self.setting_Tab), _translate("Form", "Setting Views"))
diff --git a/ictye-live-dm/src/ictye_live_dm/GUI_main.py b/ictye-live-dm/src/ictye_live_dm/GUI_main.py
index bd0d272..63044e1 100644
--- a/ictye-live-dm/src/ictye_live_dm/GUI_main.py
+++ b/ictye-live-dm/src/ictye_live_dm/GUI_main.py
@@ -1,19 +1,68 @@
import asyncio
+import logging
import os
import sys
import threading
-import logging
+from typing import Union
from PyQt5 import QtWidgets
+from PyQt5.QtCore import QTranslator, Qt
+from PyQt5.QtWidgets import QTreeWidgetItem, QComboBox
-from ictye_live_dm.depends import configs
+from ictye_live_dm.depends import configs, config_registrar
from . import main as server
+from . import pluginsystem
from .GUI import Ui_MainWindow
from .depends import logger
+__all__ = ["main", "MainWindow"]
__logger__ = logging.getLogger(__name__)
+class SettingTreeWidgetItem(QTreeWidgetItem):
+ def __init__(self, parent, key, values: Union[config_registrar.ConfigTree, config_registrar.ConfigKey]):
+ print(repr(values)) # TODO: 臨時代碼
+ self.__value = values
+ keys = [str(key), str("" if isinstance(values, config_registrar.ConfigTree) else values.get())]
+ super().__init__(parent, keys)
+
+ if isinstance(values, config_registrar.ConfigKey) and values.has_option():
+ combo_box = QComboBox()
+ combo_box.addItems([str(item) for item in values.get_options()])
+ self.treeWidget().setItemWidget(self, 1, combo_box)
+
+ def setData(self, column, role, value):
+ print(value) # TODO: 臨時代碼
+ if isinstance(self.__value, config_registrar.ConfigKey):
+ self.__value.set(value)
+ super().setData(column, role, value)
+
+
+class SettingTreeBuilder:
+ def __init__(self, tree_widget):
+ self.tree_widget = tree_widget
+
+ def build_tree(self, config_tree: config_registrar.ConfigTree):
+ parent: QTreeWidgetItem
+ if config_tree.is_list():
+ for i, v in enumerate(config_tree.values()):
+ parent = SettingTreeWidgetItem(self.tree_widget, i, v)
+ if isinstance(v, config_registrar.ConfigKey):
+ parent.setFlags(parent.flags() | Qt.ItemIsEditable)
+
+ if isinstance(v, config_registrar.ConfigTree):
+ SettingTreeBuilder(parent).build_tree(v)
+
+ elif config_tree.is_dict():
+ for k, v in config_tree.items():
+ parent = SettingTreeWidgetItem(self.tree_widget, k, v)
+ if isinstance(v, config_registrar.ConfigKey):
+ parent.setFlags(parent.flags() | Qt.ItemIsEditable)
+
+ if isinstance(v, config_registrar.ConfigTree):
+ SettingTreeBuilder(parent).build_tree(v)
+
+
class MainWindow(QtWidgets.QWidget, Ui_MainWindow.Ui_Form):
_instance = None
_server = None
@@ -24,6 +73,27 @@ def __new__(cls, *args, **kwargs):
cls._instance = super().__new__(cls)
return cls._instance
+ def __init__(self, parent=None):
+ if self._inited:
+ return
+ super(MainWindow, self).__init__()
+ self.setupUi(self)
+ self.tabWidget.setCurrentIndex(0)
+ self.init_setting_tab()
+ self.retranslateUi(self)
+ self._server = ServerClass()
+ self.startButtoen.clicked.connect(self.start_series)
+ self.stopButton.clicked.connect(self.stop_series)
+ self.settingTreeWidget.expandAll()
+
+ self.status_display_lable.setText("Stopped")
+
+ def init_setting_tab(self):
+ config = configs.ConfigManager()
+ tree_builder = SettingTreeBuilder(self.settingTreeWidget)
+ config_tree = config.get_config_tree()
+ tree_builder.build_tree(config_tree)
+
def submit_log(self, time, log_level, log_text):
"""
寫入日志
@@ -39,26 +109,33 @@ def submit_log(self, time, log_level, log_text):
self.logTable.setItem(row, 2, QtWidgets.QTableWidgetItem(log_text))
except RuntimeError:
pass
-
- def __init__(self, parent=None):
- if self._inited:
- return
- super(MainWindow, self).__init__()
- self.setupUi(self)
- self._server = ServerClass()
- self.startButtoen.clicked.connect(self.start_series)
- self.stopButton.clicked.connect(self.stop_series)
+ finally:
+ self.table_follow()
def start_series(self):
self._server.start()
+ self.status_display_lable.setText("Started")
self.startButtoen.setEnabled(False)
self.stopButton.setEnabled(True)
def stop_series(self):
self._server.stop()
+ self.status_display_lable.setText("Stopped")
self.startButtoen.setEnabled(True)
self.stopButton.setEnabled(False)
+ def table_follow(self):
+ if self.followCheckBox.isChecked():
+ self.logTable.scrollToBottom()
+
+ def show_plugin_list(self):
+ plugins = pluginsystem.Plugin().list_plugin()
+ for i in plugins:
+ row = self.pluginListTable.rowCount()
+ self.pluginListTable.insertRow(row)
+ self.pluginListTable.setItem(row, 0, QtWidgets.QTableWidgetItem(i[0]))
+ self.pluginListTable.setItem(row, 1, QtWidgets.QTableWidgetItem(i[1]))
+
class ServerClass:
_instance = None
@@ -92,13 +169,24 @@ def main():
os.chdir(os.path.dirname(os.path.abspath(__file__)))
app = QtWidgets.QApplication(sys.argv)
- form = MainWindow()
- form.show()
# 讀取設定
config = configs.ConfigManager()
config.read_default(os.path.dirname(__file__) + "/config/system/config.yaml")
+ translater = QTranslator()
+ translater.load(os.path.dirname(__file__) + "/translate/zh-ch_MainUI.qm")
+ app.installTranslator(translater)
+
+ with open(os.path.normpath(os.path.dirname(__file__) + f'/GUI/StyleSheets/{config["style"]}.qss'), 'r',
+ encoding='utf-8') as f:
+ style = f.read()
+ app.setStyleSheet(style)
+
+ form = MainWindow()
+ form.show_plugin_list()
+ form.show()
+
# 設定logger
logger.setup_logging(window=form)
diff --git a/ictye-live-dm/src/ictye_live_dm/QT-GUI/main.ui b/ictye-live-dm/src/ictye_live_dm/QT-GUI/main.ui
index 7615466..6cbf0a3 100644
--- a/ictye-live-dm/src/ictye_live_dm/QT-GUI/main.ui
+++ b/ictye-live-dm/src/ictye_live_dm/QT-GUI/main.ui
@@ -3,14 +3,14 @@
Form
- Qt::WindowModal
+ Qt::NonModal
0
0
- 729
- 499
+ 863
+ 740
@@ -20,6 +20,9 @@
../ictye_live_dm/icon.ico ../ictye_live_dm/icon.ico
+
+
+
-
@@ -69,9 +72,127 @@
-
+
+ QTabWidget::West
+
+
+ QTabWidget::Rounded
+
- 0
+ 3
+
+
+ Qt::ElideNone
+
+ false
+
+
+ false
+
+
+
+ Hone
+
+
+ -
+
+ -
+
+
+ <h1>Home</h1>
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 228
+ 28
+
+
+
+
+ -
+
+
+ Status:
+
+
+
+ -
+
+
+ %1
+
+
+
+
+
+ -
+
+ -
+
+
+ <h2>Address Contents</h2>
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+ -
+
+
+ -
+
+ -
+
+
+ <html><head/><body><p><span style=" font-size:x-large; font-weight:600;">Location </span><a href="http://127.0.0.1:8082"><span style=" text-decoration: underline; color:#0000ff;">http://127.0.0.1:8082</span></a></p></body></html>
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ open in blsower
+
+
+
+
+
+
+
Log
@@ -82,21 +203,33 @@
-
- TextLabel
+ Total
-
+
+ true
+
194
16777215
+
+ false
+
Follow newest log
+
+ true
+
+
+ false
+
@@ -152,7 +285,7 @@
-
-
+
Total:
@@ -173,7 +306,14 @@
-
-
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ QAbstractItemView::NoSelection
+
+
false
@@ -185,12 +325,17 @@
- New Column
+ Plugin name
- Plugin name
+ Author
+
+
+
+
+ Version
@@ -202,25 +347,301 @@
-
+
Setting
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 789
+ 651
+
+
+
+ -
+
+
+
+ 9
+ 50
+ false
+
+
+
+ Port
+
+
+
+ -
+
+ -
+
+
+
+
+ -
+
+
+
+ 50
+ false
+
+
+
+ Plugins
+
+
+
+ -
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Add
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+ 50
+ false
+
+
+
+ LogLevel
+
+
+
+ -
+
+
+ -
+
+
+
+ 50
+ false
+
+
+
+ Debug
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 702
+ 40
+
+
+
+
+
+
+
+
+
+
+
+
+ Setting Views
+
-
+
+ 3
+
+
+ Qt::ScrollBarAsNeeded
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ QAbstractScrollArea::AdjustToContents
+
true
+
+ Qt::AlignCenter
+
0
0
- 677
- 388
+ 789
+ 651
+
+ -
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ PushButton
+
+
+
+ -
+
+
+ PushButton
+
+
+
+
+
+ -
+
+
+ ArrowCursor
+
+
+ QAbstractItemView::DoubleClicked|QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked
+
+
+
+ Item
+
+
+
+ 75
+ true
+
+
+
+
+ 255
+ 255
+ 255
+
+
+
+
+
+ Value
+
+
+
+ 75
+ true
+
+
+
+
+
+ -
+
+
+ 6
+
+
+ QLayout::SetDefaultConstraint
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 90
+ 20
+
+
+
+
+ -
+
+
+ Apply
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 90
+ 20
+
+
+
+
+
+
+
diff --git a/ictye-live-dm/src/ictye_live_dm/__init__.py b/ictye-live-dm/src/ictye_live_dm/__init__.py
index e69de29..8b13789 100644
--- a/ictye-live-dm/src/ictye_live_dm/__init__.py
+++ b/ictye-live-dm/src/ictye_live_dm/__init__.py
@@ -0,0 +1 @@
+
diff --git a/ictye-live-dm/src/ictye_live_dm/config/system/config.yaml b/ictye-live-dm/src/ictye_live_dm/config/system/config.yaml
index bd6512b..502b95e 100644
--- a/ictye-live-dm/src/ictye_live_dm/config/system/config.yaml
+++ b/ictye-live-dm/src/ictye_live_dm/config/system/config.yaml
@@ -3,13 +3,12 @@ port: 8083
host: "127.0.0.1"
#弹幕姬前端配置
web:
- - index: "./web/living room dm.html"
- - tku: "./web/tku.html"
+ - tku: "./web/tku.html"
GUI: 0
use_local_plugin: 0
plugins:
- - "ictye_live_dm.plugin.bilibili_dm_plugin"
+ - "ictye_live_dm.plugin.bilibili_dm_plugin"
debug: 0 #调试开关,非开发人员勿动(动吧,随便动(bushi)
dev: 1
diff --git a/ictye-live-dm/src/ictye_live_dm/depends/config_registrar.py b/ictye-live-dm/src/ictye_live_dm/depends/config_registrar.py
index bced679..59a94d6 100644
--- a/ictye-live-dm/src/ictye_live_dm/depends/config_registrar.py
+++ b/ictye-live-dm/src/ictye_live_dm/depends/config_registrar.py
@@ -1,64 +1,459 @@
-from typing import TypeVar, Generic
+from typing import Union
+import re
+from abc import ABCMeta, abstractmethod
+from deprecated.sphinx import deprecated
-T = TypeVar('T')
+s_dict = "dict"
+s_list = "list"
+s_int = "int"
+s_str = "str"
+s_float = "float"
+s_bool = "bool"
-def __get_type_default__(type_: type):
- if type_ == str:
- return ""
- elif type_ == bool:
- return False
- elif type_ == int:
- return 0
- elif type_ == float:
- return 0.0
- elif type_ == list:
- return []
- elif type_ == dict:
- return {}
- elif type_ == tuple:
- return ()
- elif type_ == set:
- return set()
- elif type_ == type(None):
- return None
- else:
- raise ValueError(f"Unsupported type: {type_}")
+py_data_type = int | float | str | bool
+py_struct_type = dict | list
-class ConfigKey(Generic[T]):
+class ConfigKey:
"""
- A config key that can be registered to the ConfigRegistrar.
+ 一個鍵值,用以存儲配置
"""
- def __init__(self, default: T = None, optional: bool = False, option: list[T] = None, value: T = None):
- self.default = __get_type_default__(type(value)) if default is None else default
- self.optional = optional
- self.option = option
- self.value = value
+ def __init__(self, default: py_data_type = None,
+ optional: bool = False,
+ option: list[py_data_type] = None,
+ value: py_data_type = None):
+ if value is None and default is None:
+ raise ValueError("None ConfigKey is invalid")
+ self.__value = default if value is None else value
+ self.__default = __get_type_default__(type(value)) if default is None else default
+ self.__optional = optional
+ self.__option = option if option else [True, False] if isinstance(default, bool) else None
+ # self.__value = value
+
+ def get_default(self):
+ """
+ 獲取默認值
+ @return: 默認值
+ """
+ return self.__default
+
+ def is_optional(self):
+ """
+ 檢查是否為可選的
+ @return:
+ """
+ return self.__optional
+
+ def has_option(self):
+ """
+ 檢查是否具有選項
+ @return:
+ """
+ return self.__option is not None
+
+ def get_options(self):
+ """
+ 獲取選項
+ @return: 選項
+ """
+ return self.__option
- def getdefault(self):
- return self.default
+ def get_type(self):
+ if self.__value is not None:
+ return type(self.__value)
+ else:
+ return type(self.__default)
- def isoptional(self):
- return self.optional
+ def is_set(self):
+ return self.__value is not None
- def isoption(self):
- return self.option is not None
+ @property
+ def value(self):
+ return self.__value if self.__value is not None else self.__default
- def getoption(self):
- return self.option
+ @value.setter
+ def value(self, value):
+ if self.has_option() and value not in self.__option:
+ raise ValueError(f"Invalid value for unexpected option: {value}")
+ if value is None:
+ return
+ if self.__default is None:
+ self.__default = __get_type_default__(type(self.__value))
+ if self.__value is None:
+ self.__value = type(self.__default)(value)
+ return
+ self.__value = type(self.__value)(value)
+ @deprecated(version="2.0", reason="Use value instead")
def set(self, value):
- if self.option is not None and value not in self.option:
- raise ValueError(f"Invalid value for option: {value}")
+ """
+ 設置ConfigKey的值,注意的是,此方法會對類型進行强制轉換
+ @param value: 目的值
+ @return:
+ """
self.value = value
- def isset(self):
- return self.value is not None
-
+ @deprecated(version="2.0", reason="Use value instead")
def get(self):
- return self.value if self.value is not None else self.default
+ # 检查是否存在存储的值,如果存在则返回该值,否则返回默认值
+ return self.value
+
+ def __repr__(self):
+ return (f"ConfigKey(dfault={self.__default}, "
+ f"is optional={self.__optional}, "
+ f"options={self.__option}, "
+ f"value={self.__value})")
+
+ def __int__(self):
+ value = self.value
+ if isinstance(value, int):
+ return value
+ else:
+ raise TypeError(f"Cannot convert {value} to int")
+
+ def __float__(self):
+ value = self.value
+ if isinstance(value, float):
+ return value
+ else:
+ raise TypeError(f"Cannot convert {value} to float")
+
+ def __bool__(self):
+ value = self.value
+ return bool(value)
+
+ def __str__(self):
+ value = self.value
+ if isinstance(value, str):
+ return value
+ else:
+ return self.__repr__()
+
+ def merge(self, other: "ConfigKey"):
+ """
+ 合併兩個ConfigKey
+ 1. 如果自身沒有默認值,則會采用other的
+ 2. 會采取other的数据。
+ 3. 會采取other的option
+ @param other: 另一個ConfigKey
+ @return: 合併後的ConfigKey
+ """
+ if self.__default is __get_type_default__(type(other.value)):
+ self.__default = other.get_default()
+
+ self.value = other.value
+
+ if other.has_option():
+ self.__option = other.get_options()
+
+ if other.is_optional():
+ self.__optional = True
+
+ def __eq__(self, other: Union["ConfigKey", py_data_type]):
+ if isinstance(other, ConfigKey):
+ return self.value == other.value
+ else:
+ return self.value == other
+
+
+class ConfigTree:
+ """
+ 配置數,用於表示樹形結構配置,可以用於配置文件或數據庫查詢等。
+ """
+
+ def __init__(self,
+ value: ConfigKey = None, # 根值
+ is_list: bool = False, # 是否為列表結構
+ build_dict: dict = None, # 字典結構的子節點
+ build_list: list = None, # 列表結構的子節點
+ build_with_default: bool = False, # 是否使用默認值構造子節點
+ allow: list = None,
+ tree_lock=False,
+ schema: "ConfigSchema" = None,
+ *args: any,
+ **kwargs: any):
+ """
+ 此方法表示配置的树形结构
+ @param value: 根植
+ @param is_list: 是否为列表,一个树的键值对结构和列表的顺序结构是不同的
+ @param build_dict: 此项目用于通过字典构建一个配置树
+ @param build_list: 此项目用于通过列表构建一个配置树
+ @param build_with_default: 在构建ConfigKey的时候,是否选在将提供的数值作为默认值提供,和build_dict和build_list共同使用
+ @param allow: 未实装
+ @param tree_lock: 是否允许对结构进行修改,现已弃用,未实装
+ @param schema: 配置结构验证器,用于自身结构的验证
+ @param args: 作为列表生成配置树的时候可用
+ @param kwargs: 作为键值对生成配置树的时候可用
+ """
+
+ self.schema = schema
+ self.__allow = allow
+ self.__tree_lock = tree_lock
+ self.__value: ConfigKey = value
+ self.__content: dict[str, Union[ConfigKey, ConfigTree]] = {}
+ self.__is_list: bool = is_list
+ self.__list_content: list[Union[ConfigKey, ConfigTree]] = []
+
+ if is_list:
+ self.__build_list(args, default=build_with_default)
+ else:
+ self.__build_dict(kwargs, default=build_with_default)
+
+ if build_dict is not None or build_list is not None:
+ if self.__is_list:
+ self.__build_list([*args, *build_list], default=build_with_default)
+ else:
+ self.__build_dict({**build_dict, **kwargs}, default=build_with_default)
+
+ def __build_list(self, value, default=False):
+ if not self.__is_list:
+ raise TypeError("This is not a list")
+ for i in value:
+ if default:
+ if isinstance(i, ConfigKey):
+ self.__list_content.append(i)
+ elif isinstance(i, dict):
+ self.__list_content.append(ConfigTree(build_dict=i, build_with_default=True))
+ elif isinstance(i, list):
+ self.__list_content.append(ConfigTree(is_list=True, build_list=i, build_with_default=True))
+ elif isinstance(i, ConfigTree):
+ self.__list_content.append(i)
+ else:
+ self.__list_content.append(ConfigKey(default=i))
+ else:
+ if isinstance(i, ConfigKey):
+ self.__list_content.append(i)
+ elif isinstance(i, dict):
+ self.__list_content.append(ConfigTree(build_dict=i))
+ elif isinstance(i, list):
+ self.__list_content.append(ConfigTree(is_list=True, build_list=i))
+ elif isinstance(i, ConfigTree):
+ self.__list_content.append(i)
+ else:
+ self.__list_content.append(ConfigKey(value=i))
+
+ def __build_dict(self, value, default=False):
+ """
+ 内部構造鍵值對樹的函數
+ @param value: 傳入參數
+ @param default: 是否默認
+ @return:
+ """
+ if self.__is_list:
+ raise TypeError("This is not a dict")
+ for key, value in value.items():
+ if default:
+ if isinstance(value, ConfigKey):
+ self.__content[key] = value
+ elif isinstance(value, dict):
+ self.__content[key] = ConfigTree(build_dict=value, build_with_default=True)
+ elif isinstance(value, list):
+ self.__content[key] = ConfigTree()
+ elif isinstance(value, ConfigTree):
+ self.__content[key] = value
+ else:
+ self.__content[key] = ConfigKey(default=value)
+ else:
+ if isinstance(value, ConfigKey):
+ self.__content[key] = value
+ elif isinstance(value, dict):
+ self.__content[key] = ConfigTree(build_dict=value)
+ elif isinstance(value, list):
+ self.__content[key] = ConfigTree(is_list=True, build_list=value)
+ elif isinstance(value, ConfigTree):
+ self.__content[key] = value
+ else:
+ self.__content[key] = ConfigKey(value)
+
+ def get(self, key: str) -> Union[ConfigKey]:
+ # 检查存储内容是否为列表
+ if self.__is_list:
+ # 将键转换为整数并返回对应索引处的值
+ return self.__list_content[int(key)]
+ else:
+ # 对于字典存储,直接使用键来获取值
+ return self.__content[key]
+
+ @staticmethod
+ def has_option() -> bool:
+ return False
+
+ def __getitem__(self, item) -> ConfigKey:
+ return self.get(item)
+
+ def get_value(self) -> ConfigKey:
+ return self.__value
+
+ def read_value(self) -> py_data_type:
+ return self.__value.value
+
+ def __setitem__(self, key: str | int,
+ value: Union[ConfigKey, "ConfigTree"]):
+ self.set(key, value)
+
+ def set(self, key: str | int,
+ value: Union[ConfigKey, "ConfigTree"]):
+ """
+ 設置值
+ @param key: 鍵名
+ @param value: 值
+ @return: None
+ """
+ if self.__is_list:
+ self.__list_content[int(key)] = value
+ else:
+ self.__content[key] = value
+
+ def __iter__(self):
+ if self.__is_list:
+ return iter(self.__list_content)
+ else:
+ return iter(self.__content.items())
+
+ def __str__(self):
+ return str(self.__value)
+
+ def __repr__(self):
+ return (f"ConfigTree(value= {self.__value}, is_list= {self.__is_list},"
+ f"content= {self.__content if not self.__is_list else self.__list_content})")
+
+ def __len__(self):
+ if self.__is_list:
+ return len(self.__list_content)
+ else:
+ return len(self.__content)
+
+ def items(self) -> list:
+ """
+ @return: 一个列表,包含所有的项目,如果是字典的话返回的可能为一个包含元组的字典
+ """
+ if self.__is_list:
+ return self.__list_content
+ else:
+ return list(self.__content.items())
+
+ def keys(self) -> list:
+ """
+ @return:返回键的列表,对于列表来说,这就是它本身
+ """
+ if self.__is_list:
+ return self.__list_content
+ else:
+ return list(self.__content.keys())
+
+ def values(self) -> py_struct_type:
+ """
+ @return: 返回值的列表或字典
+ """
+ if self.__is_list:
+ return self.__list_content
+ else:
+ return self.__content
+
+ def is_dict(self) -> bool:
+ return not self.__is_list
+
+ def is_list(self) -> bool:
+ return self.__is_list
+
+ def to_list(self) -> list:
+ """
+ 將樹轉換爲列表和其他python標準内容
+ @return: 列表
+ """
+ ret = []
+ if not self.is_list():
+ raise TypeError("Cannot convert dict to list")
+ for i in self:
+ if isinstance(i, ConfigTree):
+ if i.is_list():
+ ret.append(i.to_list())
+ else:
+ ret.append(i.to_dict())
+ elif isinstance(i, ConfigKey):
+ ret.append(i.value)
+ else:
+ raise TypeError("???")
+ return ret
+
+ def to_dict(self) -> dict:
+ """
+ 將樹的額所有内容都轉換爲字典和python標準内容
+ @return: 字典
+ """
+ ret = {}
+ if self.is_list():
+ raise TypeError("Cannot convert list to dict")
+ for k, v in self.items():
+ if isinstance(v, ConfigTree):
+ if v.is_list():
+ ret[k] = v.to_list()
+ else:
+ ret[k] = v.to_dict()
+ elif isinstance(v, ConfigKey):
+ ret[k] = v.value
+ else:
+ raise TypeError("??? How can you did it???")
+ return ret
+
+ def merge(self, other: "ConfigTree") -> "ConfigTree":
+ """
+ 合併兩個配置樹
+ @param other: 另一個配置樹
+ @return: 合併後的配置樹
+ """
+ if self.is_list() and other.is_list(): # 兩個都是列表
+ for i in other.values():
+ self.__list_content.append(i) # TODO:此處邏輯仍然有問題
+ elif not self.is_list() and not other.is_list(): # 兩個都是字典
+ for k, v in other.items():
+ if k in self.__content:
+ self.__content[k].merge(v)
+ else:
+ self.set(k, v)
+ else:
+ raise TypeError("Cannot merge list with dict")
+ return self
+
+ def __contains__(self, item):
+ if self.is_list():
+ for i in self.__list_content:
+ if i == item:
+ return True
+ else:
+ for k, v in self.__content.items():
+ if k == item:
+ return True
+ return False
+
+ def __eq__(self, other):
+ if isinstance(other, ConfigTree):
+ if self.is_list() and other.is_list():
+ if len(self.__list_content) != len(other.__list_content):
+ return False
+ for i in range(len(self.__list_content)):
+ if self.__list_content[i] != other.__list_content[i]:
+ return False
+ return True
+ elif not self.is_list() and not other.is_list():
+ if len(self.__content) != len(other.__content):
+ return False
+ for k, v in self.__content.items():
+ if k not in other.__content:
+ return False
+ if v != other.__content[k]:
+ return False
+ return True
+ else:
+ return False
+
+ def append(self, value: ConfigKey, key: str = None):
+ if self.__is_list:
+ self.__list_content.append(value)
+ else:
+ self.set(key, value)
class ConfigRegistrar:
@@ -66,51 +461,98 @@ class ConfigRegistrar:
A config registrar that can be used to register config keys.
"""
+ _inited = False
+
def __init__(self):
- self.config = {}
+ if self._inited:
+ return
+ self._inited = True
+ self.config = ConfigTree()
def register(self, key, value=None, default=None, optional: bool = False, option: list = None):
"""
Register a config key.
@param key: key name
- @param value: config value
- @param default: config default value, if not provided, will use the value type's default value
- @param optional: is this config key optional
+ @param value: config __value
+ @param default: config __default __value, if not provided, will use the __value type's __default __value
+ @param optional: is this config key __optional
@param option: list of valid options for this config key
"""
if key in self.config:
raise ValueError(f"Config key '{key}' already registered")
+
+ if isinstance(value, ConfigTree) or isinstance(value, ConfigKey):
+ self.config[key] = value
+ return
+ elif isinstance(value, dict):
+ self.config[key] = ConfigTree(build_dict=value)
+ return
+ elif isinstance(value, list):
+ self.config[key] = ConfigTree(is_list=True, build_list=value)
+ return
+
+ if isinstance(default, dict):
+ self.config[key] = ConfigTree(build_dict=default, build_with_default=True)
+ return
+ elif isinstance(default, list):
+ self.config[key] = ConfigTree(is_list=True, build_list=default, build_with_default=True)
+ return
+
self.config[key] = ConfigKey(default, optional, value=value, option=option)
def set(self, key, value):
"""
- Set the value of a config key.
+ Set the __value of a config key.
@param key: key name
- @param value: config value
+ @param value: config __value
"""
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- self.config[key].set(value)
+ self.config.get(key).value = value
+
+ def dump(self, value):
+ if isinstance(value, dict):
+ self.config = ConfigTree(build_dict=value)
+ elif isinstance(value, list):
+ self.config = ConfigTree(is_list=True, build_list=value)
+
+ def dump_default(self, value):
+ """
+ 將傳入的作爲默認配置并且合并
+ @param value:
+ @return:
+ """
+ if isinstance(value, dict):
+ self.config = self.config.merge(ConfigTree(build_dict=value))
+ elif isinstance(value, list):
+ self.config = self.config.merge(ConfigTree(is_list=True, build_list=value))
def get(self, key):
"""
- Get the value of a config key.
+ Get the __value of a config key.
@param key: key name
- @return: config value
+ @return: config __value
"""
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- return self.config[key].get()
+ value = self.config.get(key)
+ if isinstance(value, ConfigKey):
+ return value.value
+ elif isinstance(value, ConfigTree):
+ return value
+ else:
+ print("value:", value)
+ return value
- def isoptional(self, key):
+ def is_optional(self, key):
"""
- Check if a config key is optional.
+ Check if a config key is __optional.
@param key: key name
- @return: True if optional, False otherwise
+ @return: True if __optional, False otherwise
"""
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- return self.config[key].isoptional()
+ return self.config.get(key).is_optional()
def get_option(self, key):
"""
@@ -120,12 +562,12 @@ def get_option(self, key):
"""
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- return self.config[key].get_option()
+ return self.config.get(key).get_options()
def is_option(self, key):
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- return self.config[key].is_option()
+ return self.config.get(key).has_option()
def isset(self, key):
"""
@@ -135,14 +577,26 @@ def isset(self, key):
"""
if key not in self.config:
raise ValueError(f"Config key '{key}' not registered")
- return self.config[key].isset()
+ return self.config.get(key).is_set()
+
+ def items(self):
+ return self.config.items()
+
+ def keys(self):
+ return self.config.keys()
+
+ def values(self):
+ return self.config.values()
def to_dict(self):
"""
Convert the config registrar to a dictionary.
@return: dictionary representation of the config registrar
"""
- return {key: self.config[key].get() for key in self.config}
+ return self.config.to_dict()
+
+ def get_config_tree(self):
+ return self.config
def __getitem__(self, key):
return self.get(key)
@@ -158,3 +612,550 @@ def __iter__(self):
def __repr__(self):
return f"ConfigRegistrar({self.config})"
+
+ def __len__(self):
+ return len(self.config)
+
+
+# config schemas
+ic_cfg_type = ConfigTree | ConfigKey
+
+
+class ABConfigSchema(metaclass=ABCMeta):
+ """
+ 抽象架構認證基礎
+ """
+
+ def __init__(self,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ 配置驗證器的抽象構造方法
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ self.not_ = not_
+ self.one_of = one_of
+ self.all_of = all_of
+ self.const = const
+ self.description = description
+ self.title = title
+ self.type_ = None
+ self._comment = _comment
+
+ @abstractmethod
+ def verify(self, other: ic_cfg_type) -> bool:
+ """
+ 驗證
+ @param other: 需要認證的對象
+ @return: 是否成功驗證
+ """
+ ...
+
+ @abstractmethod
+ def able(self, other) -> bool:
+ """
+ 驗證此架構認證器是否適用於特定的類型
+ @param other: 需要認證的對象
+ @return: 此驗證器是否適用於目標對象
+ """
+ ...
+
+ @classmethod
+ def __subclasshook__(cls, par):
+ marathons = ["verify", "able"]
+ for m in marathons:
+ if cls is ConfigSchema:
+ if not any(m in B.__dict__ for B in par.__mro__):
+ return NotImplemented
+ return True
+
+
+class ConfigSchema(ABConfigSchema):
+
+ def __init__(self,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ 配置驗證器的構造方法
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.type_ = None
+
+ def verify(self, other) -> bool:
+ if self.const and not other == self.const:
+ return False
+ if self.all_of and not all([o.verify(other) for o in self.all_of]):
+ return False
+ if self.one_of and not any([o.verify(other) for o in self.one_of]):
+ return False
+ if self.not_ and not any([o.verify(other) for o in self.not_]):
+ return False
+ return True
+
+ def able(self, other):
+ if self.type_ == s_dict:
+ return isinstance(other, ConfigTree) and not other.is_list()
+ elif self.type_ == s_list:
+ return isinstance(other, ConfigTree) and other.is_list()
+ elif self.type_ == s_int:
+ return isinstance(other, ConfigKey) and other.get_type() == int
+ elif self.type_ == s_str:
+ return isinstance(other, ConfigKey) and other.get_type() == str
+ elif self.type_ == s_float:
+ return isinstance(other, ConfigKey) and other.get_type() == float
+ elif self.type_ == s_bool:
+ return isinstance(other, ConfigKey) and other.get_type() == bool
+
+
+class StringSchema(ConfigSchema):
+ """
+ 字符串驗證器
+ """
+
+ def __init__(self,
+ max_length=None,
+ min_length=None,
+ pattern=None,
+ format_=None,
+ content_media_type=None,
+ content_encoding=None,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ @param max_length: 最長長度
+ @param min_length: 最短長度
+ @param pattern: 匹配模式
+ @param format_: 格式
+ @param content_media_type:
+ @param content_encoding:
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.content_encoding = content_encoding
+ self.content_media_type = content_media_type
+ self.format_ = format_
+ self.pattern = pattern
+
+ self.min_length = min_length
+ self.max_length = max_length
+
+ self.__re = re.compile(pattern) if pattern else None
+
+ self.type_ = s_str
+
+ def verify(self, other: ConfigKey):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ value: str = other.value
+ if self.min_length and len(value) <= self.min_length:
+ return False
+ if self.max_length and len(value) >= self.max_length:
+ return False
+ if self.__re and not self.__re.match(value):
+ return False
+ if self.format_:
+ pass # TODO
+ return True
+
+
+class IntSchema(ConfigSchema):
+ """對整數進行驗證"""
+
+ def __init__(self,
+ maximum=None,
+ minimum=None,
+ exclusive_maximum=None,
+ exclusive_minimum=None,
+ multiple=None,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ @param maximum: 最大值
+ @param minimum: 最小值
+ @param exclusive_maximum: 最大值(不包含)
+ @param exclusive_minimum: 最小值(不包含)
+ @param multiple: 倍數
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.type_ = s_int
+ self.min_value = minimum
+ self.max_value = maximum
+ self.exclusive_min_value = exclusive_minimum
+ self.exclusive_max_value = exclusive_maximum
+ self.multiple = multiple
+
+ def verify(self, other: ConfigKey):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ value: int = other.value
+ if self.min_value and value < self.min_value:
+ return False
+ if self.max_value and value > self.max_value:
+ return False
+ if self.exclusive_min_value and value <= self.exclusive_min_value:
+ return False
+ if self.exclusive_max_value and value >= self.exclusive_max_value:
+ return False
+ if self.multiple and value % self.multiple != 0:
+ return False
+ return True
+
+
+class FloatSchema(ConfigSchema):
+ """
+ 對浮點數進行驗證
+ """
+
+ def __init__(self,
+ maximum=None,
+ minimum=None,
+ exclusive_maximum=None,
+ exclusive_minimum=None,
+ multiple=None,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ @param maximum: 最大值
+ @param minimum: 最小值
+ @param exclusive_maximum: 最大值(不包含)
+ @param exclusive_minimum: 最小值(不包含)
+ @param multiple: 倍數
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.type_ = s_float
+ self.min_value = minimum
+ self.max_value = maximum
+ self.exclusive_min_value = exclusive_minimum
+ self.exclusive_max_value = exclusive_maximum
+ self.multiple = multiple
+
+ def verify(self, other: ConfigKey):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ value: float = other.value
+ if self.min_value and value < self.min_value:
+ return False
+ if self.max_value and value > self.max_value:
+ return False
+ if self.exclusive_min_value and value <= self.exclusive_min_value:
+ return False
+ if self.exclusive_max_value and value >= self.exclusive_max_value:
+ return False
+ if self.multiple and value % self.multiple != 0:
+ return False
+ return True
+
+
+class BoolSchema(ConfigSchema):
+ def __init__(self,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ic_cfg_type = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.type_ = s_bool
+
+ def verify(self, other: ConfigKey):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ return True
+
+
+class DictSchema(ConfigSchema):
+ """
+ 對於給定Dict類型進行驗證
+ """
+
+ def __init__(self,
+ properties: dict[str, ConfigSchema],
+ pattern_properties: dict[str, ConfigSchema] = None,
+ additional_properties: bool | ConfigSchema = True,
+ required: list[str] = None,
+ min_properties: int = None,
+ max_properties: int = None,
+ property_names: ConfigSchema = None,
+ dependent_required: dict[str, list[str]] = None,
+ dependent_schemas: dict[str, dict[str, ConfigSchema]] = None,
+ if_: ConfigSchema = None,
+ then: ConfigSchema = None,
+ else_: ConfigSchema = None,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ConfigTree | ConfigKey = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ 字典模式匹配,用於驗證k-v格式
+ @param properties: 對象的屬性,對於給定的鍵使用給定的模式驗證
+ @param pattern_properties: 匹配驗證,對於匹配k的屬性使用給定的模式驗證
+ @param additional_properties: 附加屬性,如果為假,則允許存在properties和pattern_properties中不存在的鍵,當指定
+ config schema的時候,則所有附加的鍵都必須與給定的config schema進行驗證
+ @param required: 指定必須的k
+ @param min_properties: 最少的屬性數量
+ @param max_properties: 最多的屬性數量
+ @param property_names: 只驗證名稱而不管它們的值
+ @param dependent_required: 需求列表,儅k存在時,list中指定的所以k都必須存在
+ @param dependent_schemas: 关键字要求当给定的属性存在时,有条件地应用子模式
+ @param if_:
+ @param then:
+ @param else_:
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.else_ = else_
+ self.then = then
+ self.if_ = if_
+ self.dependent_schemas = dependent_schemas
+ if property_names and property_names.type_ != s_str:
+ raise ValueError("property_names must be string")
+ self.dependent_required = dependent_required
+ self.property_names = property_names
+ self.max_properties = max_properties
+ self.min_properties = min_properties
+ self.required = required
+ self.additional_properties = additional_properties
+ self.pattern_properties = pattern_properties
+ self.type_ = s_dict
+ self.properties = properties
+
+ def __find_additional_properties(self, other: ConfigTree) -> dict:
+ k_set: dict = {}
+ for k, v in other.items():
+ if k in self.properties.keys():
+ k_set[k] = v
+ if any([re.match(dk, k) for dk, dv in self.pattern_properties]):
+ k_set[k] = v
+ return k_set
+
+ def verify(self, other: ConfigTree):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ value_: list = other.items()
+ # 最小值和最大值
+ if (self.min_properties
+ and len(value_) < self.min_properties):
+ return False
+ if (self.max_properties
+ and len(value_) > self.max_properties):
+ return False
+ # 检查需求
+ if (self.required
+ and not all([True if k in self.required else False for k, v in value_])):
+ return False
+ if (self.property_names
+ and not any([self.property_names.verify(v) for k, v in value_ if k in self.properties.keys()])):
+ return False
+ # 验证模式匹配
+ if (self.pattern_properties
+ and not all([any([dv.verify(v)
+ for dk, dv in self.pattern_properties if re.match(dk, k)])
+ for k, v in value_])):
+ return False
+ if self.additional_properties and isinstance(self.additional_properties, bool):
+ if not all([any([dv.verify(v)
+ for dk, dv in self.properties if k == dk])
+ for k, v in value_]):
+ return False
+ # TODO
+ # 附加
+ if ((self.additional_properties and self.pattern_properties
+ and isinstance(self.additional_properties, ConfigSchema)
+ and not any(
+ [self.additional_properties.verify(v)
+ for k, v in value_ if k not in self.properties.keys()
+ or k not in [k for dk, dv in self.pattern_properties.items()
+ if re.match(dk, k)]])) or
+ (self.additional_properties and not self.pattern_properties
+ and isinstance(self.additional_properties, ConfigSchema)
+ and not any(
+ [self.additional_properties.verify(v)
+ for k, v in value_ if k not in self.properties.keys()
+ ]))):
+ return False
+ elif self.additional_properties:
+ pass
+ else:
+ if (not all([k in self.properties.keys() for k, v in value_])
+ or not all([k in [k for dk, dv in self.pattern_properties.items() if re.match(dk, k)]
+ for k, v in value_])):
+ return False
+ return True
+
+
+class ListSchema(ConfigSchema):
+
+ def __init__(self,
+ items: list[ABConfigSchema] | ABConfigSchema = None,
+ min_items: int = None,
+ max_items: int = None,
+ unique_items: bool = None,
+ title: str = "",
+ description: str = "",
+ _comment: str = "",
+ const: ConfigTree | ConfigKey = None,
+ all_of: list["ABConfigSchema"] = None,
+ any_of: list["ABConfigSchema"] = None,
+ one_of: list["ABConfigSchema"] = None,
+ not_: list["ABConfigSchema"] = None,
+ ):
+ """
+ 列表模式匹配,用於驗證k-v格式
+ @param items: 對象的屬性
+ @param min_items: 最多包含的物件的數量
+ @param max_items: 最少包含物件的數量
+ @param unique_items: 需求的物件的數量
+ @param title: 標題
+ @param description: 描述
+ @param _comment: 注釋
+ @param const: 常量,代表如果傳入的配置必須和const完全一致
+ @param all_of: 必須對所有子模式都有效
+ @param any_of: 必须对任何子模式有效
+ @param one_of: 必须对恰好一个子模式有效
+ @param not_: 不能对给定的模式有效
+ """
+ super().__init__(title, description, _comment, const, all_of, any_of, one_of, not_)
+ self.type_ = s_list
+ self.items = items
+ self.min_items = min_items
+ self.max_items = max_items
+ self.unique_items = unique_items
+
+ def verify(self, other: ConfigTree):
+ if not super().verify(other):
+ return False
+ if not self.able(other):
+ return False
+ value_: list = other.items()
+ if (self.min_items
+ and len(value_) < self.min_items):
+ return False
+ if (self.max_items
+ and len(value_) > self.max_items):
+ return False
+ if (self.unique_items
+ and len(value_) != len(set(value_))):
+ return False
+ if not all([self.items[i].verify(v) for i, v in enumerate(value_)]):
+ return False
+ return True
+
+
+def __get_type_default__(type_: type):
+ """
+ 獲取類型的默認值,支持str,boo,int,float,其他的樹形結構請使用ConfigTree
+ :param type_: 類型
+ :return: 默認值
+ """
+ if type_ == str or type_ == bool or type_ == int or type_ == float:
+ return type_()
+ else:
+ raise ValueError(f"Unsupported type: {type_},may be you can use ConfigTree to build a tree")
diff --git a/ictye-live-dm/src/ictye_live_dm/depends/configs.py b/ictye-live-dm/src/ictye_live_dm/depends/configs.py
index c1cb75a..d817831 100644
--- a/ictye-live-dm/src/ictye_live_dm/depends/configs.py
+++ b/ictye-live-dm/src/ictye_live_dm/depends/configs.py
@@ -7,31 +7,32 @@
cfgdir: str = ""
-def regist_default(_config_registrar: config_registrar.ConfigRegistrar):
+def register_default(_config_registrar: config_registrar.ConfigRegistrar):
"""
注册默认配置
"""
_config_registrar.register("port", default=8083)
_config_registrar.register("host", default="127.0.0.1")
- _config_registrar.register("web", default={"index": "./web/living room dm.html"})
+ _config_registrar.register("web", default=[{"index": "./web/living room dm.html"}])
_config_registrar.register("GUI", default=False)
- _config_registrar.register("plugins", default={})
+ _config_registrar.register("plugins", default=[])
_config_registrar.register("debug", default=False)
- _config_registrar.register("loglevel", default="INFO", option=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"])
+ _config_registrar.register("loglevel", default="INFO",
+ option=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL", "FATAL"])
_config_registrar.register("logfile", default={"open": True, "name": "latestlog"})
_config_registrar.register("dev", default=False)
_config_registrar.register("use_local_plugin", default=False)
+ _config_registrar.register("style", default="CommonStyle")
class ConfigManager:
_instance = None
_register: config_registrar.ConfigRegistrar = config_registrar.ConfigRegistrar()
- _default: dict = {}
_inited = False
def __init__(self):
if not self._inited:
- regist_default(self._register)
+ register_default(self._register)
self._inited = True
def __new__(cls, *args, **kwargs):
@@ -50,14 +51,29 @@ def set(self, attr: str, default=None):
def read_default(self, path: str):
default = self.__load(path)
- for k, v in default.items():
- self._register.set(k, v)
+ self._register.dump_default(default)
def load_config(self, path: str):
- if not self._register:
- self._register = self.__load(path)
+ value = self.__load(path)
+ self._register.dump(value)
- def __load(self, path: str):
+ def keys(self):
+ return self._register.keys()
+
+ def values(self):
+ return self._register.values()
+
+ def items(self):
+ return self._register.items()
+
+ def get_register(self) -> config_registrar.ConfigRegistrar:
+ return self._register
+
+ def get_config_tree(self) -> config_registrar.ConfigTree:
+ return self._register.get_config_tree()
+
+ @staticmethod
+ def __load(path: str):
if path.endswith(".yml") or path.endswith(".yaml"):
with open(path, "r", encoding="utf-8") as f:
return yaml.load(f.read(), Loader=yaml.FullLoader)
diff --git a/ictye-live-dm/src/ictye_live_dm/depends/logger.py b/ictye-live-dm/src/ictye_live_dm/depends/logger.py
index 460ef40..6ff6352 100644
--- a/ictye-live-dm/src/ictye_live_dm/depends/logger.py
+++ b/ictye-live-dm/src/ictye_live_dm/depends/logger.py
@@ -35,7 +35,7 @@ def setup_logging(unportable: bool = False, window=None):
os.makedirs(log_path)
fh = logging.FileHandler(
- os.path.join(log_path, config["logfile"]["name"] + time.strftime("%Y%m%d_%H%M%S", time.localtime()) + ".log"),
+ os.path.join(log_path, config["logfile"]["name"].get() + time.strftime("%Y%m%d_%H%M%S", time.localtime()) + ".log"),
encoding="utf-8")
fh.setLevel(level_dic[config["loglevel"]])
diff --git a/ictye-live-dm/src/ictye_live_dm/depends/msgs.py b/ictye-live-dm/src/ictye_live_dm/depends/msgs.py
index 4e7fede..b9d8271 100644
--- a/ictye-live-dm/src/ictye_live_dm/depends/msgs.py
+++ b/ictye-live-dm/src/ictye_live_dm/depends/msgs.py
@@ -4,6 +4,7 @@
所有的函数都应该又to_dict方法来将其转为字典,相反的这个函数能输出打包好的字典
"""
+
class ConnectOk:
"""
连接认证消息
diff --git a/ictye-live-dm/src/ictye_live_dm/depends/pluginmain.py b/ictye-live-dm/src/ictye_live_dm/depends/pluginmain.py
index 7b5a9ab..503a870 100644
--- a/ictye-live-dm/src/ictye_live_dm/depends/pluginmain.py
+++ b/ictye-live-dm/src/ictye_live_dm/depends/pluginmain.py
@@ -1,47 +1,83 @@
import asyncio
import typing
+from abc import abstractmethod, ABCMeta
+from . import config_registrar
from . import configs
-from . import plugin_errors, msgs
+from . import msgs
-class PluginMain:
+class PluginConfig:
- def __init__(self):
- self.stop: bool = False
- """停止标志"""
+ def __init__(self, plugin_name: str):
+ self.__config = config_registrar.ConfigTree()
+
+ def __get__(self, instance, owner: "PluginMain"):
+ return self.__config
+
+ def register(self, value, name: str, default: typing.Any = None, schema: config_registrar.ConfigSchema = None):
+ ...
+
+
+class PluginMain(metaclass=ABCMeta):
+ _instance = None
+ _initialized = False
+
+ plugin_js_sprit_support: bool = False
+ """js插件支持"""
+
+ plugin_desc: str = "No description"
+ """插件描述"""
+
+ plugin_version: str = "0.0.0"
+ """插件版本号"""
- self.plugin_js_sprit_support: bool = False
- """js插件支持"""
+ plugin_dev_beach: str = ""
+ """插件开发分支"""
- self.plugin_desc: str = "No description"
- """插件描述"""
+ plugin_author: str = "Costume Author"
+ """插件作者"""
- self.plugin_js_sprit: str = ""
- """js插件"""
+ plugin_name: str = ""
+ """插件名称"""
- self.type: str = str()
- """插件类型"""
+ plugin_js_sprit: str = ""
+ """js插件"""
- self.config: dict = dict()
- """配置字典"""
+ type: str = str()
+ """插件类型"""
- self.spirit_cgi_support = False
- """插件cgi支持"""
+ config: dict = dict()
+ """配置字典"""
- self.sprit_cgi_lists: dict = dict()
- """cgi列表"""
+ spirit_cgi_support = False
+ """插件cgi支持"""
- self.plugin_name: str = ""
- """插件名称"""
+ sprit_cgi_lists: dict = dict()
+ """cgi列表"""
+
+ plugin_config = PluginConfig(plugin_name)
+
+ def __new__(cls, *args, **kwargs):
+ if not cls._instance:
+ cls._instance = super().__new__(cls)
+ return cls._instance
+
+ def __init__(self):
+ if self._initialized:
+ return
+
+ self.stop: bool = False
+ """停止标志"""
if self.plugin_type() == "message":
self.message_list = []
def plugin_setup(self):
"""初始化插件"""
- pass
+ ...
+ @abstractmethod
def plugin_init(self) -> str:
"""
插件开始被加载时调用
@@ -51,21 +87,20 @@ def plugin_init(self) -> str:
return:如果是”message“则表示这是个消息提供插件,如果是”analyzer“则表示这个插件是用来获取中间消息并且进行处理的。
"""
- self.stop = 0
- raise plugin_errors.UnexpectedPluginMessage('插件入口方法没有实现')
+ ...
+ @abstractmethod
async def plugin_main(self):
"""
插件的主方法,此方法停止时插件也会被视为运行完毕
"""
- pass
+ ...
async def message_filter(self, message) -> msgs.msg_box:
"""
消息过滤器,用于自动处理消息,比如翻译或者敏感词过滤
:param message:待处理的消息
:return 消息
-
"""
return message
@@ -73,7 +108,7 @@ async def message_analyzer(self, message):
"""
消息分析
"""
- pass
+ ...
async def sprit_cgi(self, request):
"""
@@ -81,8 +116,7 @@ async def sprit_cgi(self, request):
:param request:请求对象
:return 响应,用aiohttp的就行(已经封装为self.web)
"""
- if self.spirit_cgi_support:
- raise plugin_errors.UnexpectedPluginMather("未实现的插件方法")
+ ...
def dm_iter(self, params: dict) -> object:
"""
@@ -132,8 +166,7 @@ def plugin_callback(self):
"""
插件回调
"""
-
- print(f"plugin is done")
+ ...
@typing.final
def plugin_getconfig(self) -> configs.ConfigManager:
@@ -152,3 +185,24 @@ def plugin_type(self) -> str:
if not self.type:
self.type = self.plugin_init()
return self.type
+
+ @classmethod
+ def __subclasshook__(cls, par):
+ if not any("type" in B.__dict__ for B in par.__mro__):
+ return NotImplemented
+
+ marathons = ["plugin_init"]
+ if par.type == "message":
+ marathons.append("dm_iter")
+ elif par.type == "analyzer":
+ marathons.append("message_analyzer")
+ else:
+ return NotImplemented
+
+ if par.spirit_cgi_support:
+ marathons.append("sprit_cgi")
+
+ for m in marathons:
+ if not any(m in B.__dict__ for B in par.__mro__):
+ return NotImplemented
+ return True
diff --git a/ictye-live-dm/src/ictye_live_dm/http_server.py b/ictye-live-dm/src/ictye_live_dm/http_server.py
index 71db3e3..0ab25cb 100644
--- a/ictye-live-dm/src/ictye_live_dm/http_server.py
+++ b/ictye-live-dm/src/ictye_live_dm/http_server.py
@@ -2,6 +2,7 @@
import logging
import os
import sys
+from typing import Optional
from aiohttp import web
@@ -12,15 +13,15 @@
config: configs.ConfigManager = configs.ConfigManager()
plugin_system: pluginsystem.Plugin = pluginsystem.Plugin()
log = logging.getLogger(__name__)
-runner: web.TCPSite
+runner: Optional[web.AppRunner] = None
def return_file(file: str):
- async def healder(request):
+ async def header(request):
nonlocal file
log.info("return for main_page")
return web.FileResponse(path=file, status=200)
- return healder
+ return header
async def http_handler(request: web.Request):
@@ -115,9 +116,9 @@ async def http_server():
web.get("/api/plugin_list", http_api_plugin),
web.get("/cgi/{name}/{page}", http_cgi)
]
- for i in config["web"]:
- file, path = list(i.items())[0]
- route_list.append(web.get(f"/{file}", return_file(path)))
+ for k in config["web"]:
+ file = list(k.keys())[0]
+ route_list.append(web.get(f"/{file}", return_file(k.get(file).get())))
app.add_routes(route_list)
diff --git a/ictye-live-dm/src/ictye_live_dm/main.py b/ictye-live-dm/src/ictye_live_dm/main.py
index 8787b9b..73974ec 100644
--- a/ictye-live-dm/src/ictye_live_dm/main.py
+++ b/ictye-live-dm/src/ictye_live_dm/main.py
@@ -2,6 +2,7 @@
import asyncio
import logging
import os
+import platform
import sys
import traceback
@@ -19,7 +20,6 @@
if not is_copyright_print:
print("GPL 2024 ictye")
is_copyright_print = True
-window = None
def custom_excepthook(exc_type, exc_value, exc_traceback):
@@ -34,7 +34,7 @@ def custom_excepthook(exc_type, exc_value, exc_traceback):
sys.stderr.write(java_style_error)
-sys.excepthook = custom_excepthook
+# sys.excepthook = custom_excepthook
def loop_exception_handler(loop, context):
@@ -45,12 +45,11 @@ def loop_exception_handler(loop, context):
def run_server(loop=asyncio.get_event_loop(), callback=None):
- loop.set_exception_handler(loop_exception_handler)
+ # loop.set_exception_handler(loop_exception_handler)
ser = loop.create_task(http_server.http_server())
loop.create_task(pluginsystem.Plugin().plugin_main_runner())
try:
loop.run_forever()
- res = ser.result()
loop.run_until_complete(http_server.runner.cleanup())
if callback:
callback()
@@ -95,10 +94,12 @@ def parse_args():
def main():
- os.chdir(os.path.dirname(os.path.abspath(__file__)))
-
- parse_args()
+ os.chdir(os.path.dirname(os.path.abspath(__file__))) # 保證在正確的目錄下工作
+ if platform.system() == 'Linux':
+ print('🐧 Linux萬歲!')
+ parse_args() # 參數解析
+ # 檢查GUI啓動
if configs.ConfigManager()["GUI"]:
print("starting gui")
GUI_main.main()
@@ -107,10 +108,10 @@ def main():
# 获取logger
logger.setup_logging()
loggers = logging.getLogger(__name__)
- loggers.info("金克拉,你有了吗?")
- # 启动服务器
+ loggers.info("金克拉,你有了吗?") # 代碼摻了金克拉,一行能當兩行寫(?)
loggers.info("project starting")
- run_server(asyncio.get_event_loop())
+
+ run_server(asyncio.get_event_loop()) # 启动服务器
loggers.info("project already stopped")
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/__init__.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/configs.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/configs.py
deleted file mode 100644
index 5965ef1..0000000
--- a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/configs.py
+++ /dev/null
@@ -1,60 +0,0 @@
-# Copyright (c) 2024 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-import os
-
-import yaml
-
-cfgdir: str = ""
-
-
-def config(cfg: str) -> dict:
- cfgdir = cfg
- if cfg:
- cfgfile = cfg
- else:
- cfgfile = "./config/system/config.yaml"
- with open(cfgfile, "r", encoding="utf-8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- if configs["debug"] == 1:
- print(f"log:already reading config file: {configs}\n")
- return configs
-
-
-def set_config(config_family: str, config: dict) -> bool:
- """
- 设置插件的配置
- """
- try:
- with open(f"./config/plugin/{config_family}/config.yaml", "w", encoding="utf_8") as f:
- yaml.dump(data=config, stream=f, allow_unicode=True)
- except Exception as e:
- print(str(e))
- return False
- finally:
- return True
-
-
-def read_config(config_family: str) -> dict:
- """
- 读取插件的配置
- """
- configs = {}
- if os.path.exists(f"./config/plugin/{config_family}/config.yaml"):
- with open(f"./config/plugin/{config_family}/config.yaml", "r", encoding="utf_8") as f:
- configs = yaml.load(f.read(), Loader=yaml.FullLoader)
- return configs
- else:
- os.makedirs(os.path.dirname(f"./config/plugin/{config_family}/config.yaml"), exist_ok=True)
- with open(f"./config/plugin/{config_family}/config.yaml", 'w+') as f:
- f.write(f"# config for {config_family}")
- return configs
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/logger.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/logger.py
deleted file mode 100644
index 82786d4..0000000
--- a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/logger.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Copyright (c) 2024 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-import logging
-import os
-import time
-
-
-def setup_logging(config: dict, unportable: bool):
- """
- Setup logging configuration
- """
-
- level_dic: dict = {"DEBUG": logging.DEBUG,
- "INFO": logging.INFO,
- "WARNING": logging.WARNING,
- "ERROR": logging.ERROR,
- "CRITICAL": logging.CRITICAL,
- "FATAL": logging.FATAL}
-
- if unportable:
- appdata_path = os.getenv('APPDATA')
- log_path = os.path.join(appdata_path, "ictye_live_dm", "log")
- else:
- log_path = "logs"
- """日志档案路径"""
-
- logger = logging.getLogger() # 获取全局logger
- logger.setLevel(level_dic[config["loglevel"]]) # 设置日志级别
-
- # 创建一个handler,用于写入日志文件
- if not os.path.exists(log_path):
- os.makedirs(log_path)
-
- fh = logging.FileHandler(
- os.path.join(log_path, config["logfile"]["name"] + time.strftime("%Y%m%d_%H%M%S",time.localtime()) + ".log"),
- encoding="utf-8")
-
- fh.setLevel(level_dic[config["loglevel"]])
-
- # 创建一个handler,用于将日志输出到控制台
- ch = logging.StreamHandler()
- ch.setLevel(level_dic[config["loglevel"]])
-
- # 定义handler的输出格式
- formatter = logging.Formatter("[%(asctime)s,%(name)s] %(levelname)s : %(message)s")
- fh.setFormatter(formatter)
- ch.setFormatter(formatter)
-
- # 给logger添加handler
- if config["logfile"]["open"]:
- logger.addHandler(fh)
- logger.addHandler(ch)
-
- tmp_logger = logging.getLogger(__name__)
-
- tmp_logger.info("log path " + log_path)
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/msgs.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/msgs.py
deleted file mode 100644
index 4e83968..0000000
--- a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/msgs.py
+++ /dev/null
@@ -1,119 +0,0 @@
-"""
-这个文件内定义全部的标准消息模板
-
-所有的函数都应该又to_dict方法来将其转为字典,相反的这个函数能输出打包好的字典
-"""
-
-
-# Copyright (c) 2024 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-
-class connect_ok:
- """
- 连接认证消息
- """
- code = 200
- msg = "connect ok"
-
- def to_dict(self):
- return {"code": self.code,
- "msg": self.msg}
-
-
-class dm:
- def __init__(self, msg: str, who: dict):
- """
- params:
- msg:str 消息主体
- who:dict 消息发出者对象(其实是一个字典)
-
- 成员方法:
- to_dict: 输出为字典
- """
- self.msg = msg
- self.who = who
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who}
-
-
-class info:
- def __init__(self,
- msg: str,
- who: str,
- pic: dict):
- self.msg = msg
- self.who = who
- self.pic = pic
-
- def to_dict(self):
- return {"msg": self.msg,
- "who": self.who,
- "pic": self.pic}
-
-
-class socket_responce:
- def __init__(self, config: dict):
- self.code = 200
- self.local = "ws://{}:{}".format(config["host"], config["websocket"]["port"])
-
- def to_dict(self):
- return {"code": self.code,
- "local": self.local}
-
-
-class msg_who:
- def __init__(self,
- _type: int,
- name: str,
- face: str):
- self.type = _type
- self.name = name
- self.face = face
-
- def to_dict(self):
- return {"name": self.name,
- "type": self.type,
- "face": self.face}
-
-
-class pic:
- def __init__(self,
- border: bool,
- pic_url: str):
- self.border = border
- self.pic_url = pic_url
-
- def to_dict(self):
- return {"border": self.border,
- "pic_url": self.pic_url}
-
-
-class msg_box:
- """
- 消息标准封装所用的类
- """
-
- def __init__(self,
- message_class: str,
- msg_type: str,
- message_body: dict):
- self.message_class = message_class
- self.msg_type = msg_type
- self.message_body = message_body
-
- def to_dict(self):
- return {"message_class": self.message_class,
- "msg_type": self.msg_type,
- "message_body": self.message_body}
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/plugin_errors.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/plugin_errors.py
deleted file mode 100644
index 432b9ec..0000000
--- a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/plugin_errors.py
+++ /dev/null
@@ -1,31 +0,0 @@
-# Copyright (c) 2024 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-class PluginTypeError(Exception):
- def __init__(self, message):
- self.message = message
- super().__init__(self.message)
-
-
-class UnexpectedPluginMessage(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMessage, self).__init__(message)
-
-
-class UnexpectedPluginMather(Exception):
- def __init__(self, message):
- super(UnexpectedPluginMather, self).__init__(message)
-
-
-class NoMainMather(Exception):
- def __init__(self, message):
- super(NoMainMather, self).__init__(message)
diff --git a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/pluginmain.py b/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/pluginmain.py
deleted file mode 100644
index 367c1a8..0000000
--- a/ictye-live-dm/src/ictye_live_dm/plugin/bilibili_dm_plugin/depends/pluginmain.py
+++ /dev/null
@@ -1,179 +0,0 @@
-# Copyright (c) 2023-2024 楚天寻箫(ictye)
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-#
-# 此软件基于楚天寻箫非商业开源软件许可协议 1.0发布.
-# 您可以根据该协议的规定,在非商业或商业环境中使用、分发和引用此软件.
-# 惟分发此软件副本时,您不得以商业方式获利,并且不得限制用户获取该应用副本的体验.
-# 如果您修改或者引用了此软件,请按协议规定发布您的修改源码.
-#
-# 此软件由版权所有者提供,没有明确的技术支持承诺,使用此软件和源码造成的任何损失,
-# 版权所有者概不负责。如需技术支持,请联系版权所有者或社区获取最新版本。
-#
-# 更多详情请参阅许可协议文档
-
-import asyncio
-import typing
-
-from aiohttp import web
-
-from . import configs as configs
-from . import plugin_errors, msgs
-
-
-class PluginMain:
-
- @typing.final
- def __init__(self):
- """
- 不要用这个而是用plugin_init来进行插件的初始化,这个仅供内部使用
- """
- self.stop: bool = False
- """停止标志"""
-
- self.plugin_js_sprit_support: bool = False
- """js插件支持"""
-
- self.plugin_js_sprit: str = ""
- """js插件"""
-
- self.type: str = str()
- """插件类型"""
-
- self.config: dict = dict()
- """配置字典"""
-
- self.sprit_cgi_support = False
- """插件cgi支持"""
-
- self.sprit_cgi_lists: dict = dict()
- """cgi列表"""
-
- self.plugin_name: str = ""
- """插件名称"""
-
- self.web: web = web
- """web前端模块"""
-
- if self.plugin_type() == "message":
- self.message_list = []
-
- def plugin_init(self) -> str:
- """
- 插件开始被加载时调用
- 父函数本身不实现任何功能
- 需要返回插件类型给插件系统以判断插件类型
- (实际上这个类就是个异步迭代器)
-
- return:如果是”message“则表示这是个消息提供插件,如果是”analyzer“则表示这个插件是用来获取中间消息并且进行处理的。
- """
- self.stop = 0
- raise plugin_errors.UnexpectedPluginMessage('插件入口方法没有实现')
-
- async def plugin_main(self):
- """
- 插件的主方法,此方法停止时插件也会被视为运行完毕
- """
- pass
-
- async def message_filter(self, message) -> msgs.msg_box:
- """
- 消息过滤器,用于自动处理消息,比如翻译或者敏感词过滤
- :param message:待处理的消息
- :return 消息
-
- """
- return message
-
- async def message_anaylazer(self, message):
- """
- 消息分析
- """
- pass
-
- async def sprit_cgi(self, request):
- """
- 脚本cgi接口
- :param request:请求对象
- :return 响应,用aiohttp的就行(已经封装为self.web)
- """
- if self.sprit_cgi_support:
- raise plugin_errors.UnexpectedPluginMather("未实现的插件方法")
-
- def dm_iter(self, params: dict) -> object:
- """
- 返回弹幕迭代对象
- :param params: 前端的get参数
- :return 消息迭代对象
-
- """
- return self
-
- @typing.final
- def update_config(self, config: dict):
- """
- 更新配置,将自身的配置写入文件并且保存在计算机上
- """
- assert self.plugin_name != ""
- configs.set_config(self.plugin_name, config)
-
- @typing.final
- def read_config(self):
- """
- 读取配置
- """
- assert self.plugin_name != ""
- self.config = configs.read_config(self.plugin_name)
-
- def __aiter__(self):
- if self.plugin_type() == "message":
- return self
-
- async def __anext__(self):
- if self.plugin_type() == "message":
- if self.message_list:
- return self.message_list.pop(0)
- else:
- raise StopAsyncIteration()
-
- @typing.final
- def plugin_stop(self):
- """
- 插件停止
- """
- self.stop = 1
- asyncio.current_task().cancel()
-
- def plugin_callback(self):
-
- """
- 插件回调
- """
-
- print(f"plugin is done")
-
- @typing.final
- def plugin_getconfig(self) -> dict:
- """
- 获取配置
- """
- return configs.config(configs.cfgdir)
-
- @typing.final
- def plugin_type(self) -> str:
- """
- 获取插件类型
- """
- # 不存在则初始化插件类型,并返回插件类型给软件以判断插件类型
- # 不存在插件类型则表示插件没有被加载,返回插件没有被加载的错误信息给软件以判断插件是否被加载
- if not self.type:
- self.type = self.plugin_init()
- return self.type
diff --git a/ictye-live-dm/src/ictye_live_dm/pluginsystem.py b/ictye-live-dm/src/ictye_live_dm/pluginsystem.py
index 90c32bd..47bd711 100644
--- a/ictye-live-dm/src/ictye_live_dm/pluginsystem.py
+++ b/ictye-live-dm/src/ictye_live_dm/pluginsystem.py
@@ -6,7 +6,7 @@
import aiohttp.web as web
-from .depends import pluginmain, plugin_errors, configs
+from .depends import pluginmain, plugin_errors, configs,config_registrar
config: configs.ConfigManager = configs.ConfigManager() # 配置
@@ -39,12 +39,14 @@ def __registry_plugin__(self, plugin_module):
raise plugin_errors.NoMainMather("函数未实现主方法或者主方法名称错误")
plugin_class = getattr(plugin_module, "PluginMain")
+ if not issubclass(plugin_class, pluginmain.PluginMain):
+ raise plugin_errors.PluginTypeError("插件主类继承PluginMain类")
plugin_interface: pluginmain.PluginMain = plugin_class()
# 获取插件类型
- if plugin_interface.plugin_type() == "message":
+ if plugin_interface.type == "message":
self.message_plugin_list.append(plugin_interface)
- elif plugin_interface.plugin_type() == "analyzer":
+ elif plugin_interface.type == "analyzer":
self.analyzer_plugin_list.append(plugin_interface)
else:
raise plugin_errors.PluginTypeError("未知的插件类型,该不会是插件吃了金克拉了吧?")
@@ -58,10 +60,15 @@ def __registry_plugin__(self, plugin_module):
def __lod_extra__(self):
for plugin in config["plugins"]:
- self.logger.info(f"loading extra plugin '{plugin}'...")
- plugin_module = importlib.import_module(f'{plugin}')
+ self.logger.info(f"loading extra plugin '{plugin.get()}'...")
+ plugin_module = importlib.import_module(f'{plugin.get()}')
self.__registry_plugin__(plugin_module)
+ def register_plugin(self, plugin: str):
+ plugin_module = importlib.import_module(plugin)
+ self.__registry_plugin__(plugin_module)
+ config["plugins"].append(config_registrar.ConfigKey(plugin))
+
def __lod_init_plugin__(self):
self.logger.info("loading local plugin...")
plugin_name = ""
diff --git a/ictye-live-dm/src/ictye_live_dm/translate/zh-ch_MainUI.qm b/ictye-live-dm/src/ictye_live_dm/translate/zh-ch_MainUI.qm
new file mode 100644
index 0000000..8923620
Binary files /dev/null and b/ictye-live-dm/src/ictye_live_dm/translate/zh-ch_MainUI.qm differ
diff --git a/ictye-live-dm/src/ictye_live_dm/web/living room dm.html b/ictye-live-dm/src/ictye_live_dm/web/living room dm.html
index dbe227e..049cc21 100644
--- a/ictye-live-dm/src/ictye_live_dm/web/living room dm.html
+++ b/ictye-live-dm/src/ictye_live_dm/web/living room dm.html
@@ -22,7 +22,7 @@
-
-
+
+