plan --check treated BotFather profile reassertion as drift, so a scheduled check could never go quiet. Reassertion is now its own kind; a changed spec digest is the drift signal. apply reasserts the profile, updates channel copy when the spec changes, and is tested against a fake Telegram surface. The adapter reads its chat id from resolved state and its token from OpenBao, never the operator session. IEI-2 records that an approved provisioning plan is not autonomous Daimon action. Assistant: grok Assistant-Session: 01a09d5a-92c1-7802-915b-92bee57fc9f0
86 lines
3 KiB
Go
86 lines
3 KiB
Go
// Package adaptercfg derives the adapter's configuration from the resolved
|
|
// presence and OpenBao.
|
|
//
|
|
// The adapter publishes. It holds a bot token with post_messages and a single
|
|
// chat id, and it has no MTProto session and no provisioning code path. That
|
|
// separation is the point of running two planes: a compromised adapter cannot
|
|
// create, rename, delete or re-permission anything.
|
|
package adaptercfg
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/tegwick/fluid-telegram/internal/secrets"
|
|
"github.com/tegwick/fluid-telegram/internal/spec"
|
|
"github.com/tegwick/fluid-telegram/internal/state"
|
|
)
|
|
|
|
// Config is what one adapter instance needs to publish to one channel.
|
|
type Config struct {
|
|
Campaign string
|
|
Channel string
|
|
ChatID int64
|
|
TokenRef string
|
|
}
|
|
|
|
// FromResolved maps the committed state onto one adapter instance. It never
|
|
// reads a secret: the token stays in OpenBao, and TokenRef is where to ask.
|
|
func FromResolved(rs *state.Resolved, channel, tokenRef string) (Config, error) {
|
|
if rs == nil || rs.Bot.ID == 0 {
|
|
return Config{}, fmt.Errorf("presence is not provisioned; run `provision apply` first")
|
|
}
|
|
switch channel {
|
|
case spec.Test, spec.Live:
|
|
case "":
|
|
return Config{}, fmt.Errorf("--channel is required (%q or %q)", spec.Test, spec.Live)
|
|
default:
|
|
return Config{}, fmt.Errorf("channel %q is not %q or %q", channel, spec.Test, spec.Live)
|
|
}
|
|
ch, ok := rs.Channels[channel]
|
|
if !ok {
|
|
if channel == spec.Live && !rs.TestChannelVerified(spec.Test) {
|
|
return Config{}, fmt.Errorf("the public channel is not provisioned yet; " +
|
|
"it is held until the test channel has a checked rendering")
|
|
}
|
|
return Config{}, fmt.Errorf("channel %q is not in the resolved state", channel)
|
|
}
|
|
if ch.ChatID == 0 {
|
|
return Config{}, fmt.Errorf("channel %q has no chat id in the resolved state", channel)
|
|
}
|
|
return Config{
|
|
Campaign: rs.Campaign,
|
|
Channel: channel,
|
|
ChatID: ch.ChatID,
|
|
TokenRef: tokenRef,
|
|
}, nil
|
|
}
|
|
|
|
// EnvFile is the non-secret configuration an adapter process is started with.
|
|
// The token is named, not emitted: the runtime injects it from OpenBao.
|
|
func (c Config) EnvFile() string {
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "# adapter configuration for %s / %s\n", c.Campaign, c.Channel)
|
|
fmt.Fprintf(&b, "# derived from presence/resolved/%s.yaml; no secrets in this file\n", c.Campaign)
|
|
fmt.Fprintf(&b, "TELEGRAM_CHANNEL_ID=%d\n", c.ChatID)
|
|
fmt.Fprintf(&b, "TELEGRAM_BOT_TOKEN_REF=%s\n", c.TokenRef)
|
|
return b.String()
|
|
}
|
|
|
|
// LoadToken reads the bot token and nothing else. The operator session is a
|
|
// full-account credential; the adapter must never request it.
|
|
func LoadToken(ctx context.Context, store *secrets.Store) (string, error) {
|
|
fields, found, err := store.Get(ctx, secrets.KeyBotToken)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if !found {
|
|
return "", fmt.Errorf("no bot token at %s; run `provision apply`", store.Ref(secrets.KeyBotToken))
|
|
}
|
|
token := fields["token"]
|
|
if token == "" {
|
|
return "", fmt.Errorf("bot token at %s is empty", store.Ref(secrets.KeyBotToken))
|
|
}
|
|
return token, nil
|
|
}
|