// Package tg wraps the MTProto operations the provisioner needs. // // The Bot API cannot create a bot or a channel: both are client capabilities, // reachable only through MTProto with a user account (Canon INT-03). So this // package acts as the designated operator account -- messaging BotFather the way // a person would, and calling channels.* directly. // // It is used by the provisioning plane only. The adapter never imports it and // never holds a session. package tg import ( "context" "fmt" "strconv" "time" "github.com/gotd/td/telegram" "github.com/gotd/td/telegram/auth" "github.com/gotd/td/tg" "github.com/tegwick/fluid-telegram/internal/secrets" ) type Client struct { client *telegram.Client api *tg.Client store *secrets.Store } // Authenticator supplies what only a person can: the login code Telegram sends // out of band, and the 2FA password. Interactive during bootstrap; on every // later run the stored session means neither is asked for. type Authenticator interface { Phone(ctx context.Context) (string, error) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) Password(ctx context.Context) (string, error) } // New builds a client backed by the session in OpenBao. func New(store *secrets.Store, creds secrets.AppCredentials) *Client { c := telegram.NewClient(creds.AppID, creds.AppHash, telegram.Options{ SessionStorage: secrets.SessionStorage{Store: store}, }) return &Client{client: c, store: store} } // LoadCredentials reads api_id/api_hash. They are issued by a web form and // cannot be provisioned, so a missing pair is a runbook step, not a bug. func LoadCredentials(ctx context.Context, store *secrets.Store) (secrets.AppCredentials, error) { fields, found, err := store.Get(ctx, secrets.KeyOperatorApp) if err != nil { return secrets.AppCredentials{}, err } if !found { return secrets.AppCredentials{}, fmt.Errorf( "no app credentials at %s -- complete step 2 of docs/seeding-runbook.md", store.Ref(secrets.KeyOperatorApp)) } id, err := strconv.Atoi(fields["api_id"]) if err != nil { return secrets.AppCredentials{}, fmt.Errorf("api_id at %s is not a number", store.Ref(secrets.KeyOperatorApp)) } hash := fields["api_hash"] if hash == "" { return secrets.AppCredentials{}, fmt.Errorf("api_hash is missing at %s", store.Ref(secrets.KeyOperatorApp)) } return secrets.AppCredentials{AppID: id, AppHash: hash}, nil } // Run connects and executes f. Authentication happens only if the stored session // is absent or no longer valid. func (c *Client) Run(ctx context.Context, a Authenticator, f func(context.Context, *Client) error) error { return c.client.Run(ctx, func(ctx context.Context) error { c.api = c.client.API() if a != nil { if err := c.client.Auth().IfNecessary(ctx, auth.NewFlow( authAdapter{a}, auth.SendCodeOptions{}, )); err != nil { return fmt.Errorf("authenticate operator account: %w", err) } } else if _, err := c.client.Self(ctx); err != nil { return fmt.Errorf("the stored operator session is not usable; re-run "+ "`provision session bootstrap` (%w)", err) } return f(ctx, c) }) } // Self returns the account the session belongs to, for `session check`. func (c *Client) Self(ctx context.Context) (*tg.User, error) { return c.client.Self(ctx) } // API exposes the raw client for operations this package does not wrap. func (c *Client) API() *tg.Client { return c.api } // authAdapter bridges our Authenticator to gotd's flow. SignUp is refused: // the operator account is registered by a person on a device, and a tool that // can create accounts is a tool that can create them by accident. type authAdapter struct{ a Authenticator } func (x authAdapter) Phone(ctx context.Context) (string, error) { return x.a.Phone(ctx) } func (x authAdapter) Password(ctx context.Context) (string, error) { return x.a.Password(ctx) } func (x authAdapter) Code(ctx context.Context, sentCode *tg.AuthSentCode) (string, error) { return x.a.Code(ctx, sentCode) } func (x authAdapter) AcceptTermsOfService(ctx context.Context, tos tg.HelpTermsOfService) error { return fmt.Errorf("this account has not accepted Telegram's terms of service; " + "sign in on a device once and accept them there") } func (x authAdapter) SignUp(ctx context.Context) (auth.UserInfo, error) { return auth.UserInfo{}, fmt.Errorf( "this phone number has no Telegram account; register it on a device first " + "(docs/seeding-runbook.md step 1). This tool does not create accounts") } // pause keeps BotFather conversations at human pace. Automating a user account // is not what Telegram's terms are written around, and a burst of requests is // what draws a limit. func pause() { time.Sleep(1200 * time.Millisecond) }