164 lines
5.3 KiB
Go
164 lines
5.3 KiB
Go
package service
|
||
|
||
import (
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"regexp"
|
||
"strings"
|
||
"unicode/utf8"
|
||
)
|
||
|
||
const (
|
||
maxAdapterRegexLen = 512
|
||
maxAdapterSources = 20
|
||
)
|
||
|
||
// TimelineGitAdapter 声明式提交导入配置(内置,不可在站点设置中编辑)
|
||
type TimelineGitAdapter struct {
|
||
MaxPages int `json:"max_pages"`
|
||
MaxCommits int `json:"max_commits"`
|
||
Sources []TimelineGitSourceCfg `json:"sources"`
|
||
}
|
||
|
||
// TimelineGitSourceCfg 单个 Git 源
|
||
type TimelineGitSourceCfg struct {
|
||
ID string `json:"id"`
|
||
Host string `json:"host"` // 精确主机或 "*"
|
||
ListPath string `json:"list_path"`
|
||
CommitPath string `json:"commit_path,omitempty"` // 单条 commit 页
|
||
APIURL string `json:"api_url"`
|
||
CommitAPI string `json:"commit_api,omitempty"`
|
||
Query map[string]string `json:"query"`
|
||
Headers map[string]string `json:"headers"`
|
||
Pagination string `json:"pagination"` // link_header | query_page
|
||
Item TimelineGitItemMap `json:"item"`
|
||
}
|
||
|
||
// TimelineGitItemMap JSON 点路径映射
|
||
type TimelineGitItemMap struct {
|
||
SHA string `json:"sha"`
|
||
Date string `json:"date"`
|
||
Message string `json:"message"`
|
||
SourceURL string `json:"source_url"`
|
||
}
|
||
|
||
var allowedAdapterHeaders = map[string]bool{
|
||
"user-agent": true,
|
||
"accept": true,
|
||
"accept-language": true,
|
||
}
|
||
|
||
// DefaultTimelineGitAdapterJSON 内置 GitHub + Gitea 模板
|
||
func DefaultTimelineGitAdapterJSON() string {
|
||
return `{
|
||
"max_pages": 10,
|
||
"max_commits": 100,
|
||
"sources": [
|
||
{
|
||
"id": "github",
|
||
"host": "github.com",
|
||
"list_path": "^/(?P<owner>[^/]+)/(?P<repo>[^/]+)/commits/(?P<ref>[^/]+)/?$",
|
||
"commit_path": "^/(?P<owner>[^/]+)/(?P<repo>[^/]+)/commit/(?P<sha>[0-9a-fA-F]{7,64})/?$",
|
||
"api_url": "https://api.github.com/repos/{owner}/{repo}/commits",
|
||
"commit_api": "https://api.github.com/repos/{owner}/{repo}/commits/{sha}",
|
||
"query": { "sha": "{ref}", "per_page": "100", "page": "{page}" },
|
||
"headers": { "User-Agent": "jiang13-bbs", "Accept": "application/vnd.github+json" },
|
||
"pagination": "link_header",
|
||
"item": {
|
||
"sha": "sha",
|
||
"date": "commit.committer.date",
|
||
"message": "commit.message",
|
||
"source_url": "html_url"
|
||
}
|
||
},
|
||
{
|
||
"id": "gitea",
|
||
"host": "*",
|
||
"list_path": "^/(?P<owner>[^/]+)/(?P<repo>[^/]+)/commits/branch/(?P<ref>.+)/?$",
|
||
"commit_path": "^/(?P<owner>[^/]+)/(?P<repo>[^/]+)/commit/(?P<sha>[0-9a-fA-F]{7,64})/?$",
|
||
"api_url": "https://{host}/api/v1/repos/{owner}/{repo}/commits",
|
||
"commit_api": "https://{host}/api/v1/repos/{owner}/{repo}/git/commits/{sha}",
|
||
"query": { "sha": "{ref}", "limit": "50", "page": "{page}" },
|
||
"headers": { "User-Agent": "jiang13-bbs", "Accept": "application/json" },
|
||
"pagination": "query_page",
|
||
"item": {
|
||
"sha": "sha",
|
||
"date": "created",
|
||
"message": "commit.message",
|
||
"source_url": "html_url"
|
||
}
|
||
}
|
||
]
|
||
}`
|
||
}
|
||
|
||
func validateTimelineGitAdapter(cfg *TimelineGitAdapter) error {
|
||
if cfg.MaxPages < 1 || cfg.MaxPages > 50 {
|
||
return errors.New("max_pages 须在 1–50")
|
||
}
|
||
if cfg.MaxCommits < 1 || cfg.MaxCommits > 500 {
|
||
return errors.New("max_commits 须在 1–500")
|
||
}
|
||
if len(cfg.Sources) == 0 {
|
||
return errors.New("至少配置一个 source")
|
||
}
|
||
if len(cfg.Sources) > maxAdapterSources {
|
||
return errors.New("sources 过多")
|
||
}
|
||
seen := map[string]bool{}
|
||
for i := range cfg.Sources {
|
||
src := &cfg.Sources[i]
|
||
src.ID = strings.TrimSpace(src.ID)
|
||
if src.ID == "" {
|
||
return fmt.Errorf("sources[%d].id 不能为空", i)
|
||
}
|
||
if seen[src.ID] {
|
||
return fmt.Errorf("重复的 source id: %s", src.ID)
|
||
}
|
||
seen[src.ID] = true
|
||
src.Host = strings.ToLower(strings.TrimSpace(src.Host))
|
||
if src.Host == "" {
|
||
return fmt.Errorf("%s.host 不能为空", src.ID)
|
||
}
|
||
if utf8.RuneCountInString(src.ListPath) > maxAdapterRegexLen {
|
||
return fmt.Errorf("%s.list_path 过长", src.ID)
|
||
}
|
||
if _, err := regexp.Compile(src.ListPath); err != nil {
|
||
return fmt.Errorf("%s.list_path 正则无效: %w", src.ID, err)
|
||
}
|
||
if src.CommitPath != "" {
|
||
if _, err := regexp.Compile(src.CommitPath); err != nil {
|
||
return fmt.Errorf("%s.commit_path 正则无效: %w", src.ID, err)
|
||
}
|
||
}
|
||
if strings.TrimSpace(src.APIURL) == "" {
|
||
return fmt.Errorf("%s.api_url 不能为空", src.ID)
|
||
}
|
||
if src.Pagination != "link_header" && src.Pagination != "query_page" {
|
||
return fmt.Errorf("%s.pagination 须为 link_header 或 query_page", src.ID)
|
||
}
|
||
for k := range src.Headers {
|
||
if !allowedAdapterHeaders[strings.ToLower(k)] {
|
||
return fmt.Errorf("%s.headers 不允许字段 %s", src.ID, k)
|
||
}
|
||
}
|
||
if src.Item.SHA == "" || src.Item.Date == "" || src.Item.Message == "" || src.Item.SourceURL == "" {
|
||
return fmt.Errorf("%s.item 须包含 sha/date/message/source_url", src.ID)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (s *SettingService) loadTimelineGitAdapter() (*TimelineGitAdapter, error) {
|
||
raw := DefaultTimelineGitAdapterJSON()
|
||
var cfg TimelineGitAdapter
|
||
if err := json.Unmarshal([]byte(raw), &cfg); err != nil {
|
||
return nil, errors.New("内置提交导入规则无效")
|
||
}
|
||
if err := validateTimelineGitAdapter(&cfg); err != nil {
|
||
return nil, errors.New("内置提交导入规则无效")
|
||
}
|
||
return &cfg, nil
|
||
}
|