182 lines
4.8 KiB
Go
182 lines
4.8 KiB
Go
package settings
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
)
|
|
|
|
type SystemState string
|
|
|
|
const (
|
|
StateUninitialized SystemState = "uninitialized"
|
|
StateCredentialsSaved SystemState = "credentials_saved"
|
|
StateDingTalkVerified SystemState = "dingtalk_verified"
|
|
StateWaitingAdmin SystemState = "waiting_first_admin"
|
|
StateActive SystemState = "active"
|
|
StateCredentialError SystemState = "credential_error"
|
|
)
|
|
|
|
type Snapshot struct {
|
|
ClientID string
|
|
SecretConfigured bool
|
|
CredentialsVerified bool
|
|
AdministratorCount int
|
|
}
|
|
|
|
func DetermineState(s Snapshot) SystemState {
|
|
id := strings.TrimSpace(s.ClientID) != ""
|
|
if !id && !s.SecretConfigured {
|
|
return StateUninitialized
|
|
}
|
|
if !id || !s.SecretConfigured {
|
|
return StateCredentialError
|
|
}
|
|
if !s.CredentialsVerified {
|
|
return StateCredentialsSaved
|
|
}
|
|
if s.AdministratorCount < 1 {
|
|
return StateWaitingAdmin
|
|
}
|
|
return StateActive
|
|
}
|
|
|
|
type Command struct {
|
|
Name string
|
|
Args []string
|
|
}
|
|
|
|
func ParseCommand(raw string) (Command, bool) {
|
|
fields := strings.Fields(strings.TrimSpace(raw))
|
|
if len(fields) == 0 {
|
|
return Command{}, false
|
|
}
|
|
name := strings.ToLower(fields[0])
|
|
switch name {
|
|
case "/setting", "/设置":
|
|
default:
|
|
return Command{}, false
|
|
}
|
|
return Command{Name: name, Args: fields[1:]}, true
|
|
}
|
|
|
|
type NotableLink struct{ BaseID, SheetID, ViewID string }
|
|
|
|
func ParseNotableLink(raw string) (NotableLink, error) {
|
|
if len(raw) > 4096 {
|
|
return NotableLink{}, errors.New("AI Table link is too long")
|
|
}
|
|
input := strings.TrimSpace(raw)
|
|
u, err := url.Parse(input)
|
|
if err != nil {
|
|
return NotableLink{}, errors.New("invalid AI Table link")
|
|
}
|
|
if u.Scheme == "" {
|
|
if u.Host != "" || u.User != nil || strings.Contains(input, "\\") {
|
|
return NotableLink{}, errors.New("invalid AI Table relative path")
|
|
}
|
|
relativePath := "/" + strings.TrimPrefix(u.Path, "/")
|
|
if !strings.HasPrefix(relativePath, "/i/nodes/") || path.Clean(relativePath) != relativePath {
|
|
return NotableLink{}, errors.New("invalid AI Table relative path")
|
|
}
|
|
u = &url.URL{Scheme: "https", Host: "alidocs.dingtalk.com", Path: relativePath, RawQuery: u.RawQuery}
|
|
}
|
|
if u.Scheme != "https" {
|
|
return NotableLink{}, errors.New("invalid AI Table link")
|
|
}
|
|
host := strings.ToLower(u.Hostname())
|
|
if host != "alidocs.dingtalk.com" && host != "docs.dingtalk.com" {
|
|
return NotableLink{}, errors.New("untrusted AI Table host")
|
|
}
|
|
base := path.Base(strings.TrimRight(u.Path, "/"))
|
|
q := u.Query()
|
|
sheetID, err := uniqueNotableQueryValue(q, "sheetId")
|
|
if err != nil {
|
|
return NotableLink{}, err
|
|
}
|
|
viewID, err := uniqueNotableQueryValue(q, "viewId")
|
|
if err != nil {
|
|
return NotableLink{}, err
|
|
}
|
|
iframeQuery, err := uniqueNotableQueryValue(q, "iframeQuery")
|
|
if err != nil {
|
|
return NotableLink{}, err
|
|
}
|
|
if iframeQuery != "" {
|
|
nested, parseErr := url.ParseQuery(iframeQuery)
|
|
if parseErr != nil {
|
|
return NotableLink{}, errors.New("invalid AI Table iframeQuery")
|
|
}
|
|
nestedSheetID, valueErr := uniqueNotableQueryValue(nested, "sheetId")
|
|
if valueErr != nil {
|
|
return NotableLink{}, valueErr
|
|
}
|
|
nestedViewID, valueErr := uniqueNotableQueryValue(nested, "viewId")
|
|
if valueErr != nil {
|
|
return NotableLink{}, valueErr
|
|
}
|
|
if sheetID != "" && nestedSheetID != "" && sheetID != nestedSheetID {
|
|
return NotableLink{}, errors.New("conflicting AI Table sheet IDs")
|
|
}
|
|
if viewID != "" && nestedViewID != "" && viewID != nestedViewID {
|
|
return NotableLink{}, errors.New("conflicting AI Table view IDs")
|
|
}
|
|
if sheetID == "" {
|
|
sheetID = nestedSheetID
|
|
}
|
|
if viewID == "" {
|
|
viewID = nestedViewID
|
|
}
|
|
}
|
|
result := NotableLink{BaseID: base, SheetID: sheetID, ViewID: viewID}
|
|
if result.BaseID == "" || result.BaseID == "." || result.BaseID == ".." || result.SheetID == "" {
|
|
return NotableLink{}, errors.New("AI Table link is missing base or sheet")
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func uniqueNotableQueryValue(values url.Values, key string) (string, error) {
|
|
items := values[key]
|
|
if len(items) > 1 {
|
|
return "", errors.New("conflicting AI Table query values")
|
|
}
|
|
if len(items) == 0 {
|
|
return "", nil
|
|
}
|
|
return strings.TrimSpace(items[0]), nil
|
|
}
|
|
|
|
type Runtime interface {
|
|
Start(context.Context, string) error
|
|
Stop(context.Context) error
|
|
}
|
|
type Switcher struct {
|
|
runtime Runtime
|
|
active string
|
|
}
|
|
|
|
func NewSwitcher(r Runtime, active string) *Switcher { return &Switcher{runtime: r, active: active} }
|
|
func (s *Switcher) Switch(ctx context.Context, candidate string) error {
|
|
if s == nil || s.runtime == nil {
|
|
return errors.New("runtime is required")
|
|
}
|
|
candidate = strings.TrimSpace(candidate)
|
|
if candidate == "" {
|
|
return errors.New("candidate is required")
|
|
}
|
|
old := s.active
|
|
if err := s.runtime.Stop(ctx); err != nil {
|
|
return err
|
|
}
|
|
if err := s.runtime.Start(ctx, candidate); err != nil {
|
|
if old != "" {
|
|
_ = s.runtime.Start(ctx, old)
|
|
}
|
|
return err
|
|
}
|
|
s.active = candidate
|
|
return nil
|
|
}
|