99 lines
3.4 KiB
Go
99 lines
3.4 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
)
|
|
|
|
// IntegrationPolicyKey is the operator's global on/off switch for external services that
|
|
// have no configuration document of their own.
|
|
//
|
|
// It is deliberately *not* a second copy of every integration's switch. Sonarr and Radarr
|
|
// already store theirs in the arr integration policy and MDBList stores its own in the
|
|
// ratings settings; moving those here would mean a migration and, worse, a window in which
|
|
// two documents disagreed about whether a service was on. The rule is one stored truth per
|
|
// integration, kept where that integration's other configuration already lives — and this
|
|
// document is that home for the ones that have nowhere else, which today is Tracearr.
|
|
//
|
|
// The API's integrationEnabled/setIntegrationEnabled pair is the single reader and writer,
|
|
// so the console never has to know which document answers for which service.
|
|
const IntegrationPolicyKey = "integration_policy"
|
|
|
|
// IntegrationPolicy is a map of integration id to whether it is switched on.
|
|
//
|
|
// Absence means on. A household that upgrades into this feature has every service it had
|
|
// configured still working, which is the only safe reading: the alternative silently turns
|
|
// off recommendation imports on the day the console gained a switch for them.
|
|
type IntegrationPolicy struct {
|
|
Disabled map[string]bool `json:"disabled"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
// Enabled reports whether one integration is switched on. A service nobody has ever
|
|
// touched has no entry, and no entry is on.
|
|
func (p IntegrationPolicy) Enabled(id string) bool {
|
|
return !p.Disabled[strings.TrimSpace(id)]
|
|
}
|
|
|
|
func (s *Store) IntegrationPolicy(ctx context.Context) (IntegrationPolicy, error) {
|
|
empty := IntegrationPolicy{Disabled: map[string]bool{}}
|
|
var raw []byte
|
|
err := s.pool.QueryRow(ctx,
|
|
`SELECT value FROM app_settings WHERE key = $1`, IntegrationPolicyKey).Scan(&raw)
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return empty, nil
|
|
}
|
|
if err != nil {
|
|
return empty, fmt.Errorf("store: read integration policy: %w", err)
|
|
}
|
|
var policy IntegrationPolicy
|
|
if err := json.Unmarshal(raw, &policy); err != nil {
|
|
return empty, fmt.Errorf("store: decode integration policy: %w", err)
|
|
}
|
|
if policy.Disabled == nil {
|
|
policy.Disabled = map[string]bool{}
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// SetIntegrationEnabled records one service's switch, leaving every other entry alone.
|
|
//
|
|
// Read-modify-write rather than a whole-document put, because the console sends one
|
|
// switch at a time and a put would let a page rendered before another integration existed
|
|
// silently re-enable it.
|
|
func (s *Store) SetIntegrationEnabled(ctx context.Context, id string, enabled bool) error {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
return fmt.Errorf("store: integration policy needs an id")
|
|
}
|
|
policy, err := s.IntegrationPolicy(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if enabled {
|
|
delete(policy.Disabled, id)
|
|
} else {
|
|
policy.Disabled[id] = true
|
|
}
|
|
policy.UpdatedAt = time.Now().UTC()
|
|
raw, err := json.Marshal(policy)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = s.pool.Exec(ctx, `
|
|
INSERT INTO app_settings (key, value, updated_at)
|
|
VALUES ($1, $2::jsonb, now())
|
|
ON CONFLICT (key) DO UPDATE SET value = EXCLUDED.value, updated_at = now()`,
|
|
IntegrationPolicyKey, string(raw))
|
|
if err != nil {
|
|
return fmt.Errorf("store: write integration policy: %w", err)
|
|
}
|
|
return nil
|
|
}
|