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) }