package main import ( "bufio" "context" "flag" "fmt" "os" "strings" "syscall" "github.com/gotd/td/tg" "golang.org/x/term" "github.com/tegwick/fluid-telegram/internal/secrets" tgc "github.com/tegwick/fluid-telegram/internal/tg" ) // prompt is the only interactive path in this tool. It exists so the // interactive part is bounded and named rather than spread through the process: // Telegram sends the login code out of band by design, and no amount of // automation removes that. type prompt struct{ in *bufio.Reader } func newPrompt() *prompt { return &prompt{in: bufio.NewReader(os.Stdin)} } func (p *prompt) ask(label string) (string, error) { fmt.Fprintf(os.Stderr, "%s: ", label) line, err := p.in.ReadString('\n') if err != nil { return "", err } return strings.TrimSpace(line), nil } func (p *prompt) askSecret(label string) (string, error) { fmt.Fprintf(os.Stderr, "%s: ", label) b, err := term.ReadPassword(int(syscall.Stdin)) fmt.Fprintln(os.Stderr) if err != nil { return "", err } return strings.TrimSpace(string(b)), nil } func (p *prompt) Phone(context.Context) (string, error) { return p.ask("operator phone number (international format)") } func (p *prompt) Code(_ context.Context, _ *tg.AuthSentCode) (string, error) { return p.ask("login code Telegram just sent") } func (p *prompt) Password(context.Context) (string, error) { return p.askSecret("two-factor password") } func cmdSession(args []string) error { if len(args) == 0 { return fmt.Errorf("session needs a subcommand: bootstrap or check") } fs := flag.NewFlagSet("session", flag.ExitOnError) campaign := fs.String("campaign", "", "campaign slug (names the OpenBao subtree)") fs.Parse(args[1:]) if *campaign == "" { return fmt.Errorf("--campaign is required") } store, err := secrets.NewFromEnv(*campaign) if err != nil { return err } ctx := context.Background() creds, err := tgc.LoadCredentials(ctx, store) if err != nil { return err } client := tgc.New(store, creds) switch args[0] { case "bootstrap": fmt.Fprintf(os.Stderr, "Minting an operator session for %q.\n"+ "The session is a full-account credential: it goes straight to %s\n"+ "and is never written to disk.\n\n", *campaign, store.Ref(secrets.KeyOperatorSession)) return client.Run(ctx, newPrompt(), func(ctx context.Context, c *tgc.Client) error { self, err := c.Self(ctx) if err != nil { return err } fmt.Printf("session stored for %s (id %d)\n", displayName(self), self.ID) return nil }) case "check": // No authenticator: a check must fail if the stored session is unusable, // not quietly prompt for a new one and call that success. return client.Run(ctx, nil, func(ctx context.Context, c *tgc.Client) error { self, err := c.Self(ctx) if err != nil { return err } fmt.Printf("session valid: %s (id %d)\n", displayName(self), self.ID) return nil }) } return fmt.Errorf("unknown session subcommand %q", args[0]) } func displayName(u *tg.User) string { if u.Username != "" { return "@" + u.Username } name := strings.TrimSpace(u.FirstName + " " + u.LastName) if name == "" { return "(unnamed account)" } return name }