mirror of
https://github.com/LiteyukiStudio/LiteyukiBot.git
synced 2024-11-23 03:17:37 +08:00
📦 docs: 资源商店新增发布资源功能
This commit is contained in:
parent
72e71124b8
commit
b5b15c82f8
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
44
.github/workflows/ISSUE_TEMPLATE.md
vendored
@ -1,44 +0,0 @@
|
|||||||
# 问题反馈
|
|
||||||
|
|
||||||
## **请确保**
|
|
||||||
|
|
||||||
- 已认真阅读[文档]("https://bot.liteyuki.icu"),该问题不是文档提及的或你自己操作不当造成的
|
|
||||||
- 你的问题是在最新版本的代码上测试的
|
|
||||||
- 请勿重复提交相同或类似的issue
|
|
||||||
|
|
||||||
|
|
||||||
## **描述问题**
|
|
||||||
|
|
||||||
请在此简单描述问题
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **如何复现**
|
|
||||||
|
|
||||||
请阐述一下如何重现这个问题
|
|
||||||
### 预期
|
|
||||||
|
|
||||||
描述你期望发生的事情
|
|
||||||
|
|
||||||
### 实际
|
|
||||||
|
|
||||||
描述实际发生的事情
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## **日志或截图**
|
|
||||||
```
|
|
||||||
日志内容
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## **设备信息**
|
|
||||||
- **系统**: [例如 Ubuntu 22.04]
|
|
||||||
- **CPU**: [例如 Intel i7-7700K]
|
|
||||||
- **内存**: [例如 16GB]
|
|
||||||
- **Python**: [例如CPython 3.10.7]
|
|
||||||
|
|
||||||
|
|
||||||
**补充内容**
|
|
||||||
|
|
||||||
可选,推荐提供`pip freeze`的输出,以及其他相关信息,以及你的建议
|
|
3
liteyuki_flow/__init__.py
Normal file
3
liteyuki_flow/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
60
liteyuki_flow/__main__.py
Normal file
60
liteyuki_flow/__main__.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from github import Github
|
||||||
|
from argparse import ArgumentParser
|
||||||
|
|
||||||
|
from liteyuki_flow.const import PLUGIN_PREFIX, RESOURCE_PREFIX
|
||||||
|
from liteyuki_flow.typ import err, nil # type: ignore
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = ArgumentParser()
|
||||||
|
parser.add_argument("--handle", action="store_true") # 处理issue
|
||||||
|
|
||||||
|
parser.add_argument("-p", "--parse", action="store_true") # 解析markdown文件
|
||||||
|
parser.add_argument("-i", "--input", type=str, help="Path to the markdown file.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.handle:
|
||||||
|
print("Starting the issue handler module...")
|
||||||
|
ISSUE_NUMBER = os.getenv("ISSUE_NUMBER")
|
||||||
|
REPOSITORY = os.getenv("REPOSITORY")
|
||||||
|
ACT_TYPE = os.getenv("ACT_TYPE") # opened, edited, closed, reopened
|
||||||
|
if ISSUE_NUMBER is None or REPOSITORY is None or ACT_TYPE is None:
|
||||||
|
raise ValueError("Issue number, repository and action type are required.")
|
||||||
|
|
||||||
|
g = Github(os.getenv("GITHUB_TOKEN"))
|
||||||
|
repo = g.get_repo(REPOSITORY)
|
||||||
|
issue = g.get_repo(REPOSITORY).get_issue(int(ISSUE_NUMBER))
|
||||||
|
|
||||||
|
# 审资源
|
||||||
|
if issue.title.strip().startswith(RESOURCE_PREFIX):
|
||||||
|
from resource_handler import handle_resource # type: ignore
|
||||||
|
handle_resource(github=g, issue=issue, repo=repo, act_type=ACT_TYPE)
|
||||||
|
|
||||||
|
# 审插件
|
||||||
|
elif issue.title.strip().startswith(PLUGIN_PREFIX):
|
||||||
|
from plugin_handler import handle_plugin # type: ignore
|
||||||
|
pass
|
||||||
|
|
||||||
|
else:
|
||||||
|
print("No handler found for the issue.")
|
||||||
|
|
||||||
|
elif args.parse:
|
||||||
|
print("Starting the markdown parser module...")
|
||||||
|
from .markdown_parser import MarkdownParser # type: ignore
|
||||||
|
|
||||||
|
if args.input is None:
|
||||||
|
raise ValueError("Input file is required.")
|
||||||
|
with open(args.input, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
md_parser = MarkdownParser(content) # type: ignore
|
||||||
|
err = md_parser.parse_front_matters() # type: ignore
|
||||||
|
if err != nil:
|
||||||
|
print(f"Err: {err}")
|
||||||
|
for k, v in md_parser.front_matters.content.items():
|
||||||
|
print(f"{k}: {v}")
|
||||||
|
else:
|
||||||
|
print("No module specified.")
|
15
liteyuki_flow/const.py
Normal file
15
liteyuki_flow/const.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
||||||
|
|
||||||
|
OPENED = "opened"
|
||||||
|
EDITED = "edited"
|
||||||
|
CLOSED = "closed"
|
||||||
|
REOPENED = "reopened"
|
||||||
|
RESOURCE_PREFIX = "Resource: "
|
||||||
|
PLUGIN_PREFIX = "Plugin: "
|
||||||
|
|
||||||
|
PLUGIN_JSON = "docs/publics/plugins.json"
|
||||||
|
RESOURCE_JSON = "docs/publics/resources.json"
|
||||||
|
|
||||||
|
edit_tip = "若要修改请编辑这段front matter,不要编辑正文/If you want to modify, please edit the front matter, do not edit the body"
|
136
liteyuki_flow/markdown_parser.py
Normal file
136
liteyuki_flow/markdown_parser.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
从markdown提取插件/资源信息
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from liteyuki_flow.typ import Nil, err, nil # type: ignore
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# # xxx
|
||||||
|
class Header:
|
||||||
|
def __init__(self, level: int, content: str):
|
||||||
|
self.level = level
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'Header({self.level}, {self.content})'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
# - xxx
|
||||||
|
class List:
|
||||||
|
def __init__(self, level: int, content: str):
|
||||||
|
self.level = level
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'List({self.level}, {self.content})'
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return self.__str__()
|
||||||
|
|
||||||
|
|
||||||
|
class FrontMatter:
|
||||||
|
def __init__(self, content: dict[str, str]):
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
def __setitem__(self, key: str, value: str):
|
||||||
|
self.content[key] = value
|
||||||
|
|
||||||
|
def get(self, key, default=None) -> Any:
|
||||||
|
return self.content.get(key, default)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "\n".join([f'{k}: {v}' for k, v in self.content.items()])
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownParser:
|
||||||
|
def __init__(self, content: str):
|
||||||
|
self.content = content
|
||||||
|
self.content_lines = content.split('\n')
|
||||||
|
self.front_matters: FrontMatter = FrontMatter({})
|
||||||
|
|
||||||
|
self._content_list: list[Any] = [self.front_matters]
|
||||||
|
|
||||||
|
self.lineno = 0
|
||||||
|
|
||||||
|
self._parsed = False
|
||||||
|
|
||||||
|
def parse_front_matters(self) -> err:
|
||||||
|
if self.content_lines[self.lineno].strip() != '---':
|
||||||
|
return ValueError('Invalid front matter')
|
||||||
|
while self.lineno < len(self.content_lines):
|
||||||
|
self._next_line()
|
||||||
|
line = self.content_lines[self.lineno]
|
||||||
|
if line.strip() == '---':
|
||||||
|
break
|
||||||
|
if line.strip().startswith('#'):
|
||||||
|
# fm注释
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
key, value = line.split(':', 1)
|
||||||
|
except ValueError:
|
||||||
|
return Exception(f'Invalid front matter: {line}')
|
||||||
|
self.front_matters[key.strip()] = value.strip()
|
||||||
|
return nil
|
||||||
|
|
||||||
|
def _parse_content(self) -> tuple[list[Any], err]:
|
||||||
|
content: list[Any] = []
|
||||||
|
while self.lineno < len(self.content_lines):
|
||||||
|
item, e = self._parse_line()
|
||||||
|
if e != nil:
|
||||||
|
return nil, e
|
||||||
|
content.append(item)
|
||||||
|
return content, nil
|
||||||
|
|
||||||
|
def _parse_line(self) -> tuple[Any, err]:
|
||||||
|
line = self.content_lines[self.lineno]
|
||||||
|
if line.startswith('#'):
|
||||||
|
# 计算start有几个#
|
||||||
|
start = 0
|
||||||
|
while line[start] == '#':
|
||||||
|
start += 1
|
||||||
|
return Header(start, line[start:].strip()), nil
|
||||||
|
elif line.startswith('-'):
|
||||||
|
start = 0
|
||||||
|
while line[start] == '-':
|
||||||
|
start += 1
|
||||||
|
return List(start, line[start:].strip()), nil
|
||||||
|
|
||||||
|
# 处理<!--注释 continue
|
||||||
|
elif line.strip().startswith('<!--'):
|
||||||
|
while not line.strip().endswith('-->'):
|
||||||
|
self._next_line()
|
||||||
|
line = self.content_lines[self.lineno]
|
||||||
|
return None, nil
|
||||||
|
# 处理[//]: # (注释) continue
|
||||||
|
elif line.strip().startswith('[//]: #'):
|
||||||
|
self._next_line()
|
||||||
|
return None, nil
|
||||||
|
|
||||||
|
self._next_line()
|
||||||
|
return nil, ValueError(f'Invalid line: {line}')
|
||||||
|
|
||||||
|
def _next_line(self):
|
||||||
|
self.lineno += 1
|
||||||
|
|
||||||
|
def parse(self) -> tuple[list[Any] | Nil, err]:
|
||||||
|
if self._parsed:
|
||||||
|
return self._content_list, nil
|
||||||
|
|
||||||
|
e = self.parse_front_matters()
|
||||||
|
if e != nil:
|
||||||
|
return nil, e
|
||||||
|
|
||||||
|
ls, e = self._parse_content()
|
||||||
|
if e != nil:
|
||||||
|
return nil, e
|
||||||
|
|
||||||
|
self._content_list.extend(ls)
|
||||||
|
self._parsed = True
|
||||||
|
|
||||||
|
return self._content_list, nil
|
7
liteyuki_flow/plugin_handler.py
Normal file
7
liteyuki_flow/plugin_handler.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_handler():
|
||||||
|
pass
|
3
liteyuki_flow/requirements.txt
Normal file
3
liteyuki_flow/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
PyGithub==2.4.0
|
||||||
|
requests==2.31.0
|
||||||
|
pyyaml==6.0.2
|
131
liteyuki_flow/resource_handler.py
Normal file
131
liteyuki_flow/resource_handler.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
||||||
|
import requests # type: ignore
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
from github import Github, InputGitTreeElement, GitTree
|
||||||
|
from github.Issue import Issue
|
||||||
|
from github.Repository import Repository
|
||||||
|
import json
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from liteyuki_flow.const import OPENED, EDITED, CLOSED, REOPENED, RESOURCE_JSON
|
||||||
|
from liteyuki_flow.markdown_parser import MarkdownParser
|
||||||
|
from liteyuki_flow.typ import err, nil
|
||||||
|
|
||||||
|
|
||||||
|
# opened: 创建新的资源包,预审核
|
||||||
|
# edited: 编辑资源包信息,需重新审核
|
||||||
|
# closed: 审核通过,修改json并提交
|
||||||
|
# reopened: 重新打开,无操作
|
||||||
|
|
||||||
|
# opened | edited
|
||||||
|
def pre_check(github: Github, issue: Issue, repo: Repository) -> err:
|
||||||
|
parser = MarkdownParser(issue.body)
|
||||||
|
parser.parse_front_matters()
|
||||||
|
name = parser.front_matters.get("name")
|
||||||
|
desc = parser.front_matters.get("desc")
|
||||||
|
url = parser.front_matters.get("url")
|
||||||
|
homepage = parser.front_matters.get("homepage") # optional
|
||||||
|
author = parser.front_matters.get("author")
|
||||||
|
if not all((name, desc, url, author)):
|
||||||
|
issue.create_comment("Name, desc, url, homepage and author are required.")
|
||||||
|
return ValueError("Name, desc, url, homepage and author are required.")
|
||||||
|
|
||||||
|
# 下载并解析资源包
|
||||||
|
r = requests.get(url)
|
||||||
|
if r.status_code != 200:
|
||||||
|
issue.create_comment("Download failed.")
|
||||||
|
return ValueError("Download failed.")
|
||||||
|
|
||||||
|
with open(f"tmp/{name}.zip", "wb") as f:
|
||||||
|
f.write(r.content)
|
||||||
|
# 解压
|
||||||
|
with zipfile.ZipFile(f"tmp/{name}.zip", "r") as z:
|
||||||
|
z.extractall(f"tmp/{name}")
|
||||||
|
|
||||||
|
# 检测包内metadata.yml文件
|
||||||
|
try:
|
||||||
|
data = yaml.load(open(f"tmp/{name}/metadata.yml"), Loader=yaml.SafeLoader)
|
||||||
|
except Exception:
|
||||||
|
issue.create_comment("metadata.yml not found or invalid.")
|
||||||
|
return ValueError("metadata.yml not found or invalid.")
|
||||||
|
|
||||||
|
# 检测必要字段 name,description,version
|
||||||
|
if not all((data.get("name"), data.get("description"), data.get("version"))):
|
||||||
|
issue.create_comment("name, description and version are required in metadata.yml.")
|
||||||
|
return ValueError("name, description and version are required in metadata.yml.")
|
||||||
|
|
||||||
|
# 不检测重复资源包,因为资源包可能有多个版本
|
||||||
|
# 检测通过,编辑原issue
|
||||||
|
metadata_markdown = f"**名称**: {data.get('name')}\n**描述**: {data.get('description')}\n**版本**: {data.get('version')}\n"
|
||||||
|
for k, v in data.items():
|
||||||
|
if k not in ("name", "description", "version"):
|
||||||
|
metadata_markdown += f"**{k}**: {v}\n"
|
||||||
|
|
||||||
|
new_issue_body = f"---\nname: {name}\ndesc: {desc}\nurl: {url}\nhomepage: {homepage}\nauthor: {author}\n---\n"
|
||||||
|
new_issue_body += f"# Resource: {name}\n"
|
||||||
|
new_issue_body += f"## 发布信息\n{desc}\n"
|
||||||
|
new_issue_body += f"**名称**: {name}\n"
|
||||||
|
new_issue_body += f"**描述**: {desc}\n"
|
||||||
|
new_issue_body += f"**作者**: {author}\n"
|
||||||
|
new_issue_body += f"**主页**: {homepage}\n"
|
||||||
|
new_issue_body += f"**下载**: {url}\n"
|
||||||
|
# 遍历其他字段
|
||||||
|
for k, v in data.items():
|
||||||
|
if k not in ("name", "description", "version"):
|
||||||
|
new_issue_body += f"**{k}**: {v}\n"
|
||||||
|
|
||||||
|
issue.edit(new_issue_body)
|
||||||
|
issue.create_comment("✅ 预检查通过\n## 元数据\n" + metadata_markdown)
|
||||||
|
return nil
|
||||||
|
|
||||||
|
|
||||||
|
# closed
|
||||||
|
def add_resource(github: Github, issue: Issue, repo: Repository):
|
||||||
|
parser = MarkdownParser(issue.body)
|
||||||
|
parser.parse_front_matters()
|
||||||
|
name = parser.front_matters.get("name")
|
||||||
|
desc = parser.front_matters.get("desc")
|
||||||
|
url = parser.front_matters.get("url")
|
||||||
|
homepage = parser.front_matters.get("homepage") # optional
|
||||||
|
author = parser.front_matters.get("author")
|
||||||
|
|
||||||
|
# 编辑仓库内的json文件
|
||||||
|
resources = json.load(open(RESOURCE_JSON))
|
||||||
|
resources.append({
|
||||||
|
"name" : name,
|
||||||
|
"desc" : desc,
|
||||||
|
"url" : url,
|
||||||
|
"homepage": homepage,
|
||||||
|
"author" : author
|
||||||
|
})
|
||||||
|
ref = repo.get_git_ref("heads/main")
|
||||||
|
tree = repo.create_git_tree(
|
||||||
|
base_tree=repo.get_git_commit(ref.object.sha).tree,
|
||||||
|
tree=[
|
||||||
|
InputGitTreeElement(
|
||||||
|
path=RESOURCE_JSON,
|
||||||
|
mode="100644",
|
||||||
|
type="blob",
|
||||||
|
content=json.dumps(resources, indent=4, ensure_ascii=False)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
commit = repo.create_git_commit(
|
||||||
|
message=f":package: 发布资源: {name}",
|
||||||
|
tree=tree,
|
||||||
|
parents=[repo.get_git_commit(ref.object.sha)]
|
||||||
|
)
|
||||||
|
ref.edit(commit.sha)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_resource(github: Github, issue: Issue, repo: Repository, act_type: str):
|
||||||
|
if act_type in (OPENED, EDITED):
|
||||||
|
pre_check(github, issue, repo)
|
||||||
|
|
||||||
|
elif act_type == CLOSED:
|
||||||
|
add_resource(github, issue, repo)
|
||||||
|
else:
|
||||||
|
print("No operation found for the issue: ", act_type)
|
26
liteyuki_flow/ts.md
Normal file
26
liteyuki_flow/ts.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
# 请修改此处内容/Please modify the content here
|
||||||
|
# 不要修改正文中的内容/Do not modify the content in the body
|
||||||
|
name: PluginName
|
||||||
|
desc: 插件描述
|
||||||
|
author: 作者名
|
||||||
|
link: 下载链接
|
||||||
|
homepage: 主页链接
|
||||||
|
---
|
||||||
|
<!-- 以下字段自动生成,请勿编辑 -->
|
||||||
|
# Plugin: PluginName
|
||||||
|
|
||||||
|
## 插件信息
|
||||||
|
|
||||||
|
- **包名**: PluginName
|
||||||
|
- **描述**: 插件描述
|
||||||
|
- **作者**: AuthorName
|
||||||
|
- **主页**: https://a.b.c
|
||||||
|
|
||||||
|
## 资源包信息
|
||||||
|
|
||||||
|
- **名称**: PluginName
|
||||||
|
- **描述**: 插件描述
|
||||||
|
- **作者**: AuthorName
|
||||||
|
- **主页**: https://a.b.c
|
||||||
|
- **下载**: https://a.b.c
|
20
liteyuki_flow/typ.py
Normal file
20
liteyuki_flow/typ.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Module docs
|
||||||
|
"""
|
||||||
|
from typing import TypeAlias
|
||||||
|
|
||||||
|
|
||||||
|
class Nil():
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, Nil):
|
||||||
|
return True
|
||||||
|
return other is None
|
||||||
|
|
||||||
|
# 不等于
|
||||||
|
def __ne__(self, other):
|
||||||
|
return not self.__eq__(other)
|
||||||
|
|
||||||
|
|
||||||
|
nil = Nil()
|
||||||
|
|
||||||
|
err: TypeAlias = Exception | Nil
|
Loading…
Reference in New Issue
Block a user