400 lines
11 KiB
Go
400 lines
11 KiB
Go
package extension
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
const (
|
|
PluginManifestVersion = "1"
|
|
)
|
|
|
|
var (
|
|
PluginPermissionStorage PluginPermissionScope = "storage" // Allows the plugin to store its own data
|
|
PluginPermissionDatabase PluginPermissionScope = "database" // Allows the plugin to read non-auth data from the database and write to it
|
|
PluginPermissionPlayback PluginPermissionScope = "playback" // Allows the plugin to use the playback manager
|
|
PluginPermissionAnilist PluginPermissionScope = "anilist" // Allows the plugin to use the Anilist client
|
|
PluginPermissionAnilistToken PluginPermissionScope = "anilist-token" // Allows the plugin to see and use the Anilist token
|
|
PluginPermissionSystem PluginPermissionScope = "system" // Allows the plugin to use the OS/Filesystem/Filepath functions. SystemPermissions must be granted additionally.
|
|
PluginPermissionCron PluginPermissionScope = "cron" // Allows the plugin to use the cron manager
|
|
PluginPermissionNotification PluginPermissionScope = "notification" // Allows the plugin to use the notification manager
|
|
PluginPermissionDiscord PluginPermissionScope = "discord" // Allows the plugin to use the discord rpc
|
|
PluginPermissionTorrentClient PluginPermissionScope = "torrent-client" // Allows the plugin to use the torrent client
|
|
)
|
|
|
|
type PluginManifest struct {
|
|
Version string `json:"version"`
|
|
// Permissions is a list of permissions that the plugin is asking for.
|
|
// The user must acknowledge these permissions before the plugin can be loaded.
|
|
Permissions PluginPermissions `json:"permissions,omitempty"`
|
|
}
|
|
|
|
type PluginPermissions struct {
|
|
Scopes []PluginPermissionScope `json:"scopes,omitempty"`
|
|
Allow PluginAllowlist `json:"allow,omitempty"`
|
|
}
|
|
|
|
// PluginAllowlist is a list of system permissions that the plugin is asking for.
|
|
//
|
|
// The user must acknowledge these permissions before the plugin can be loaded.
|
|
type PluginAllowlist struct {
|
|
// ReadPaths is a list of paths that the plugin is allowed to read from.
|
|
ReadPaths []string `json:"readPaths,omitempty"`
|
|
// WritePaths is a list of paths that the plugin is allowed to write to.
|
|
WritePaths []string `json:"writePaths,omitempty"`
|
|
// CommandScopes defines the commands that the plugin is allowed to execute.
|
|
// Each command scope has a unique identifier and configuration.
|
|
CommandScopes []CommandScope `json:"commandScopes,omitempty"`
|
|
}
|
|
|
|
// CommandScope defines a specific command or set of commands that can be executed
|
|
// with specific arguments and validation rules.
|
|
type CommandScope struct {
|
|
// Description explains why this command scope is needed
|
|
Description string `json:"description,omitempty"`
|
|
// Command is the executable program
|
|
Command string `json:"command"`
|
|
// Args defines the allowed arguments for this command
|
|
// If nil or empty, no arguments are allowed
|
|
// If contains "$ARGS", any arguments are allowed at that position
|
|
Args []CommandArg `json:"args,omitempty"`
|
|
}
|
|
|
|
// CommandArg represents an argument for a command
|
|
type CommandArg struct {
|
|
// Value is the fixed argument value
|
|
// If empty, Validator must be set
|
|
Value string `json:"value,omitempty"`
|
|
// Validator is a Perl compatible regex pattern to validate dynamic argument values
|
|
// Special values:
|
|
// - "$ARGS" allows any arguments at this position
|
|
// - "$PATH" allows any valid file path
|
|
Validator string `json:"validator,omitempty"`
|
|
}
|
|
|
|
// ReadAllowCommands returns a human-readable representation of the commands
|
|
// that the plugin is allowed to execute.
|
|
func (p *PluginAllowlist) ReadAllowCommands() []string {
|
|
if p == nil {
|
|
return []string{}
|
|
}
|
|
|
|
result := make([]string, 0)
|
|
|
|
// Add commands from CommandScopes
|
|
if len(p.CommandScopes) > 0 {
|
|
for _, scope := range p.CommandScopes {
|
|
cmd := scope.Command
|
|
|
|
// Build argument string
|
|
args := ""
|
|
for i, arg := range scope.Args {
|
|
if i > 0 {
|
|
args += " "
|
|
}
|
|
|
|
if arg.Value != "" {
|
|
args += arg.Value
|
|
} else if arg.Validator == "$ARGS" {
|
|
args += "[any arguments]"
|
|
} else if arg.Validator == "$PATH" {
|
|
args += "[any path]"
|
|
} else if arg.Validator != "" {
|
|
args += "[matching: " + arg.Validator + "]"
|
|
}
|
|
}
|
|
|
|
if args != "" {
|
|
cmd += " " + args
|
|
}
|
|
|
|
// Add description if available
|
|
if scope.Description != "" {
|
|
cmd += " - " + scope.Description
|
|
}
|
|
|
|
result = append(result, cmd)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func (p *PluginPermissions) GetHash() string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
|
|
if len(p.Scopes) == 0 &&
|
|
len(p.Allow.ReadPaths) == 0 &&
|
|
len(p.Allow.WritePaths) == 0 &&
|
|
len(p.Allow.CommandScopes) == 0 {
|
|
return ""
|
|
}
|
|
|
|
h := sha256.New()
|
|
|
|
// Hash scopes
|
|
for _, scope := range p.Scopes {
|
|
h.Write([]byte(scope))
|
|
}
|
|
|
|
// Hash allowlist read paths
|
|
for _, path := range p.Allow.ReadPaths {
|
|
h.Write([]byte("read:" + path))
|
|
}
|
|
|
|
// Hash allowlist write paths
|
|
for _, path := range p.Allow.WritePaths {
|
|
h.Write([]byte("write:" + path))
|
|
}
|
|
|
|
// Hash command scopes
|
|
for _, cmd := range p.Allow.CommandScopes {
|
|
h.Write([]byte("cmd:" + cmd.Command + ":" + cmd.Description))
|
|
for _, arg := range cmd.Args {
|
|
h.Write([]byte("arg:" + arg.Value + ":" + arg.Validator))
|
|
}
|
|
}
|
|
|
|
return fmt.Sprintf("%x", h.Sum(nil))
|
|
}
|
|
|
|
func (p *PluginPermissions) GetDescription() string {
|
|
if p == nil {
|
|
return ""
|
|
}
|
|
|
|
// Check if any permissions exist
|
|
if len(p.Scopes) == 0 &&
|
|
len(p.Allow.ReadPaths) == 0 &&
|
|
len(p.Allow.WritePaths) == 0 &&
|
|
len(p.Allow.CommandScopes) == 0 {
|
|
return "No permissions requested."
|
|
}
|
|
|
|
var desc strings.Builder
|
|
|
|
// Add scopes section if any exist
|
|
if len(p.Scopes) > 0 {
|
|
desc.WriteString("Application:\n")
|
|
for _, scope := range p.Scopes {
|
|
desc.WriteString("• ")
|
|
switch scope {
|
|
case PluginPermissionStorage:
|
|
desc.WriteString("Storage: Store plugin data locally\n")
|
|
case PluginPermissionDatabase:
|
|
desc.WriteString("Database: Read and write non-auth data\n")
|
|
case PluginPermissionPlayback:
|
|
desc.WriteString("Playback: Control media playback and media players\n")
|
|
case PluginPermissionAnilist:
|
|
desc.WriteString("AniList: View and edit your AniList lists\n")
|
|
case PluginPermissionAnilistToken:
|
|
desc.WriteString("AniList Token: View and use your AniList token\n")
|
|
case PluginPermissionSystem:
|
|
desc.WriteString("System: Access OS functions (accessing files, running commands, etc.)\n")
|
|
case PluginPermissionCron:
|
|
desc.WriteString("Cron: Schedule automated tasks\n")
|
|
case PluginPermissionNotification:
|
|
desc.WriteString("Notification: Send system notifications\n")
|
|
case PluginPermissionDiscord:
|
|
desc.WriteString("Discord: Set Discord Rich Presence\n")
|
|
case PluginPermissionTorrentClient:
|
|
desc.WriteString("Torrent Client: Control torrent clients\n")
|
|
default:
|
|
desc.WriteString(string(scope) + "\n")
|
|
}
|
|
}
|
|
desc.WriteString("\n")
|
|
}
|
|
|
|
// Add file permissions if any exist
|
|
hasFilePaths := len(p.Allow.ReadPaths) > 0 || len(p.Allow.WritePaths) > 0
|
|
if hasFilePaths {
|
|
desc.WriteString("File System:\n")
|
|
|
|
if len(p.Allow.ReadPaths) > 0 {
|
|
desc.WriteString("• Read from:\n")
|
|
for _, path := range p.Allow.ReadPaths {
|
|
desc.WriteString("\t - " + explainPath(path) + "\n")
|
|
}
|
|
}
|
|
|
|
if len(p.Allow.WritePaths) > 0 {
|
|
desc.WriteString("• Write to:\n")
|
|
for _, path := range p.Allow.WritePaths {
|
|
desc.WriteString("\t - " + explainPath(path) + "\n")
|
|
}
|
|
}
|
|
desc.WriteString("\n")
|
|
}
|
|
|
|
// Add command permissions if any exist
|
|
if len(p.Allow.CommandScopes) > 0 {
|
|
desc.WriteString("Commands:\n")
|
|
for _, cmd := range p.Allow.CommandScopes {
|
|
cmdDesc := "• " + cmd.Command
|
|
|
|
// Format arguments
|
|
if len(cmd.Args) > 0 {
|
|
argsDesc := ""
|
|
for _, arg := range cmd.Args {
|
|
if arg.Value != "" {
|
|
argsDesc += " " + arg.Value
|
|
} else if arg.Validator == "$ARGS" {
|
|
argsDesc += " [any arguments]"
|
|
} else if arg.Validator == "$PATH" {
|
|
argsDesc += " [any file path]"
|
|
} else if arg.Validator != "" {
|
|
argsDesc += " [pattern: " + arg.Validator + "]"
|
|
}
|
|
}
|
|
cmdDesc += argsDesc
|
|
}
|
|
|
|
// Add command description if available
|
|
if cmd.Description != "" {
|
|
cmdDesc += "\n\t Purpose: " + cmd.Description
|
|
}
|
|
|
|
desc.WriteString(cmdDesc + "\n")
|
|
}
|
|
}
|
|
|
|
return strings.TrimSpace(desc.String())
|
|
}
|
|
|
|
// explainPath adds human-readable descriptions to paths containing environment variables
|
|
func explainPath(path string) string {
|
|
environmentVars := map[string]string{
|
|
"$SEANIME_ANIME_LIBRARY": "Your anime library directories",
|
|
"$HOME": "Your system's Home directory",
|
|
"$CACHE": "Your system's Cache directory",
|
|
"$TEMP": "Your system's Temporary directory",
|
|
"$CONFIG": "Your system's Config directory",
|
|
"$DOWNLOAD": "Your system's Downloads directory",
|
|
"$DESKTOP": "Your system's Desktop directory",
|
|
"$DOCUMENT": "Your system's Documents directory",
|
|
}
|
|
|
|
result := path
|
|
|
|
// Check if we need to add an explanation
|
|
needsExplanation := false
|
|
explanation := ""
|
|
|
|
for envVar, description := range environmentVars {
|
|
if strings.Contains(path, envVar) {
|
|
if explanation != "" {
|
|
explanation += ", "
|
|
}
|
|
explanation += fmt.Sprintf("%s = %s", envVar, description)
|
|
needsExplanation = true
|
|
}
|
|
}
|
|
|
|
if needsExplanation {
|
|
result += " (" + explanation + ")"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type PluginExtension interface {
|
|
BaseExtension
|
|
GetPermissionHash() string
|
|
}
|
|
|
|
type PluginExtensionImpl struct {
|
|
ext *Extension
|
|
}
|
|
|
|
func NewPluginExtension(ext *Extension) PluginExtension {
|
|
return &PluginExtensionImpl{
|
|
ext: ext,
|
|
}
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetPermissionHash() string {
|
|
if m.ext.Plugin == nil {
|
|
return ""
|
|
}
|
|
|
|
return m.ext.Plugin.Permissions.GetHash()
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetExtension() *Extension {
|
|
return m.ext
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetType() Type {
|
|
return m.ext.Type
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetID() string {
|
|
return m.ext.ID
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetName() string {
|
|
return m.ext.Name
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetVersion() string {
|
|
return m.ext.Version
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetManifestURI() string {
|
|
return m.ext.ManifestURI
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetLanguage() Language {
|
|
return m.ext.Language
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetLang() string {
|
|
return GetExtensionLang(m.ext.Lang)
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetDescription() string {
|
|
return m.ext.Description
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetAuthor() string {
|
|
return m.ext.Author
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetPayload() string {
|
|
return m.ext.Payload
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetWebsite() string {
|
|
return m.ext.Website
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetIcon() string {
|
|
return m.ext.Icon
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetPermissions() []string {
|
|
return m.ext.Permissions
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetUserConfig() *UserConfig {
|
|
return m.ext.UserConfig
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetSavedUserConfig() *SavedUserConfig {
|
|
return m.ext.SavedUserConfig
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetPayloadURI() string {
|
|
return m.ext.PayloadURI
|
|
}
|
|
|
|
func (m *PluginExtensionImpl) GetIsDevelopment() bool {
|
|
return m.ext.IsDevelopment
|
|
}
|