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
138 lines
3.9 KiB
Go
138 lines
3.9 KiB
Go
package apply
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gotd/td/tg"
|
|
|
|
"github.com/tegwick/fluid-telegram/internal/secrets"
|
|
tgc "github.com/tegwick/fluid-telegram/internal/tg"
|
|
)
|
|
|
|
// Surface is the Telegram operations apply needs. The real client is one
|
|
// implementation; tests supply another so apply can be checked without a session.
|
|
type Surface interface {
|
|
OpenBotFather(ctx context.Context) (BotConversation, error)
|
|
ResolveBot(ctx context.Context, username string) (id int64, err error)
|
|
CreateChannel(ctx context.Context, title, about string) (chatID int64, err error)
|
|
ClaimUsername(ctx context.Context, chatID int64, username string) (bool, error)
|
|
PromoteBot(ctx context.Context, chatID int64, botUsername string) error
|
|
EditChannelTitle(ctx context.Context, chatID int64, title string) error
|
|
EditChannelAbout(ctx context.Context, chatID int64, about string) error
|
|
}
|
|
|
|
// BotConversation is the BotFather exchange apply drives.
|
|
type BotConversation interface {
|
|
RegisterBot(ctx context.Context, displayName, username string) (issuedUsername, token string, err error)
|
|
SetProfile(ctx context.Context, username, name, about, description string) error
|
|
SetAvatar(ctx context.Context, username, path string) error
|
|
}
|
|
|
|
// SecretStore is the OpenBao operations apply needs. *secrets.Store satisfies it.
|
|
type SecretStore interface {
|
|
CreateIfAbsent(ctx context.Context, key string, fields map[string]string) (bool, error)
|
|
Put(ctx context.Context, key string, fields map[string]string) error
|
|
Ref(key string) string
|
|
}
|
|
|
|
var (
|
|
_ Surface = (*live)(nil)
|
|
_ BotConversation = (*tgc.Conversation)(nil)
|
|
_ SecretStore = (*secrets.Store)(nil)
|
|
)
|
|
|
|
type live struct {
|
|
c *tgc.Client
|
|
bot tg.InputUserClass
|
|
chats map[int64]tgc.Channel
|
|
}
|
|
|
|
// Live wraps a connected MTProto client.
|
|
func Live(c *tgc.Client) Surface {
|
|
return &live{c: c, chats: map[int64]tgc.Channel{}}
|
|
}
|
|
|
|
func (s *live) OpenBotFather(ctx context.Context) (BotConversation, error) {
|
|
return s.c.BotFather(ctx)
|
|
}
|
|
|
|
func (s *live) ResolveBot(ctx context.Context, username string) (int64, error) {
|
|
res, err := s.c.API().ContactsResolveUsername(ctx, &tg.ContactsResolveUsernameRequest{
|
|
Username: username,
|
|
})
|
|
if err != nil {
|
|
return 0, fmt.Errorf("resolve @%s: %w", username, err)
|
|
}
|
|
for _, u := range res.Users {
|
|
if user, ok := u.(*tg.User); ok {
|
|
s.bot = &tg.InputUser{UserID: user.ID, AccessHash: user.AccessHash}
|
|
return user.ID, nil
|
|
}
|
|
}
|
|
return 0, fmt.Errorf("@%s did not resolve to a user", username)
|
|
}
|
|
|
|
func (s *live) CreateChannel(ctx context.Context, title, about string) (int64, error) {
|
|
ch, err := s.c.CreateChannel(ctx, title, about)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
s.chats[ch.ChatID] = ch
|
|
return ch.ChatID, nil
|
|
}
|
|
|
|
func (s *live) channel(ctx context.Context, chatID int64) (tgc.Channel, error) {
|
|
if ch, ok := s.chats[chatID]; ok {
|
|
return ch, nil
|
|
}
|
|
ch, err := s.c.ResolveChannel(ctx, chatID)
|
|
if err != nil {
|
|
return tgc.Channel{}, err
|
|
}
|
|
s.chats[chatID] = ch
|
|
return ch, nil
|
|
}
|
|
|
|
func (s *live) ClaimUsername(ctx context.Context, chatID int64, username string) (bool, error) {
|
|
ch, err := s.channel(ctx, chatID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
ok, err := s.c.SetUsername(ctx, ch, username)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if ok {
|
|
ch.Username = username
|
|
s.chats[chatID] = ch
|
|
}
|
|
return ok, nil
|
|
}
|
|
|
|
func (s *live) PromoteBot(ctx context.Context, chatID int64, botUsername string) error {
|
|
if s.bot == nil {
|
|
return fmt.Errorf("promote @%s: bot has not been resolved", botUsername)
|
|
}
|
|
ch, err := s.channel(ctx, chatID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.c.PromoteBot(ctx, ch, s.bot, botUsername)
|
|
}
|
|
|
|
func (s *live) EditChannelTitle(ctx context.Context, chatID int64, title string) error {
|
|
ch, err := s.channel(ctx, chatID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.c.EditTitle(ctx, ch, title)
|
|
}
|
|
|
|
func (s *live) EditChannelAbout(ctx context.Context, chatID int64, about string) error {
|
|
ch, err := s.channel(ctx, chatID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.c.EditAbout(ctx, ch, about)
|
|
}
|