Skip to content

Commit 6efe4e2

Browse files
committed
add desktop login import
1 parent ce244c4 commit 6efe4e2

6 files changed

Lines changed: 279 additions & 7 deletions

File tree

cmd/bbctl/authconfig.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ type EnvConfig struct {
3232
}
3333

3434
func (ec *EnvConfig) HasCredentials() bool {
35-
return strings.HasPrefix(ec.AccessToken, "syt_")
35+
return strings.HasPrefix(ec.AccessToken, "syt_") || strings.HasPrefix(ec.AccessToken, "bat_")
3636
}
3737

3838
type EnvConfigs map[string]*EnvConfig

cmd/bbctl/desktopauth.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"errors"
7+
"fmt"
8+
"net/url"
9+
"os"
10+
"path/filepath"
11+
"runtime"
12+
"strings"
13+
14+
"github.com/urfave/cli/v2"
15+
"go.mau.fi/util/dbutil"
16+
"maunium.net/go/mautrix/id"
17+
18+
"github.com/beeper/bridge-manager/api/beeperapi"
19+
20+
_ "go.mau.fi/util/dbutil/litestream"
21+
)
22+
23+
var loginDesktopCommand = &cli.Command{
24+
Name: "login-desktop",
25+
Usage: "Import Beeper Desktop's Matrix credentials into bbctl config",
26+
Action: loginDesktop,
27+
Flags: desktopLoginFlags(),
28+
}
29+
30+
func desktopLoginFlags() []cli.Flag {
31+
return []cli.Flag{
32+
&cli.StringFlag{
33+
Name: "profile",
34+
EnvVars: []string{"BEEPER_PROFILE"},
35+
Usage: "Beeper Desktop profile name, equivalent to BEEPER_PROFILE in Desktop",
36+
},
37+
&cli.StringFlag{
38+
Name: "desktop-data-dir",
39+
EnvVars: []string{"BBCTL_DESKTOP_DATA_DIR"},
40+
Usage: "Read Matrix credentials from this Beeper Desktop user data directory",
41+
},
42+
&cli.StringFlag{
43+
Name: "desktop-account-db",
44+
EnvVars: []string{"BBCTL_DESKTOP_ACCOUNT_DB"},
45+
Usage: "Read Matrix credentials from this Beeper Desktop account.db",
46+
},
47+
}
48+
}
49+
50+
type DesktopAccount struct {
51+
UserID id.UserID
52+
DeviceID id.DeviceID
53+
AccessToken string
54+
Homeserver string
55+
}
56+
57+
func getDesktopAccountDBPath(ctx *cli.Context) (string, bool) {
58+
if dbPath := ctx.String("desktop-account-db"); dbPath != "" {
59+
return dbPath, true
60+
}
61+
if dataDir := ctx.String("desktop-data-dir"); dataDir != "" {
62+
return filepath.Join(dataDir, "account.db"), true
63+
}
64+
return "", false
65+
}
66+
67+
func resolveDesktopDataDir(profile string) (string, error) {
68+
appName := "BeeperTexts"
69+
if profile != "" {
70+
appName += "-" + profile
71+
}
72+
switch runtime.GOOS {
73+
case "darwin":
74+
home, err := os.UserHomeDir()
75+
if err != nil {
76+
return "", err
77+
}
78+
return filepath.Join(home, "Library", "Application Support", appName), nil
79+
case "windows":
80+
if appData := os.Getenv("APPDATA"); appData != "" {
81+
return filepath.Join(appData, appName), nil
82+
}
83+
home, err := os.UserHomeDir()
84+
if err != nil {
85+
return "", err
86+
}
87+
return filepath.Join(home, appName), nil
88+
default:
89+
configHome := os.Getenv("XDG_CONFIG_HOME")
90+
if configHome == "" {
91+
home, err := os.UserHomeDir()
92+
if err != nil {
93+
return "", err
94+
}
95+
configHome = filepath.Join(home, ".config")
96+
}
97+
return filepath.Join(configHome, appName), nil
98+
}
99+
}
100+
101+
func getLoginDesktopAccountDBPath(ctx *cli.Context) (string, error) {
102+
if dbPath, ok := getDesktopAccountDBPath(ctx); ok {
103+
return dbPath, nil
104+
}
105+
dataDir, err := resolveDesktopDataDir(ctx.String("profile"))
106+
if err != nil {
107+
return "", fmt.Errorf("failed to resolve desktop data directory: %w", err)
108+
}
109+
return filepath.Join(dataDir, "account.db"), nil
110+
}
111+
112+
func readDesktopAccount(ctx context.Context, dbPath string) (*DesktopAccount, error) {
113+
dbURI := (&url.URL{
114+
Scheme: "file",
115+
Path: dbPath,
116+
RawQuery: "mode=ro",
117+
}).String()
118+
db, err := dbutil.NewWithDialect(dbURI, "sqlite3-fk-wal")
119+
if err != nil {
120+
return nil, fmt.Errorf("failed to open desktop account database: %w", err)
121+
}
122+
defer db.Close()
123+
124+
var account DesktopAccount
125+
err = db.QueryRow(ctx, "SELECT user_id, device_id, access_token, homeserver FROM account LIMIT 1").
126+
Scan(&account.UserID, &account.DeviceID, &account.AccessToken, &account.Homeserver)
127+
if errors.Is(err, sql.ErrNoRows) {
128+
return nil, fmt.Errorf("desktop account database has no logged-in account")
129+
} else if err != nil {
130+
return nil, fmt.Errorf("failed to read desktop account database: %w", err)
131+
} else if account.UserID == "" || account.AccessToken == "" {
132+
return nil, fmt.Errorf("desktop account database has incomplete credentials")
133+
}
134+
return &account, nil
135+
}
136+
137+
func desktopAccountHomeserverDomain(account *DesktopAccount) (string, error) {
138+
if account.Homeserver == "" {
139+
return "", nil
140+
}
141+
parsed, err := url.Parse(account.Homeserver)
142+
if err != nil {
143+
return "", fmt.Errorf("desktop account has invalid homeserver URL %q: %w", account.Homeserver, err)
144+
}
145+
return strings.TrimPrefix(parsed.Host, "matrix."), nil
146+
}
147+
148+
func envForHomeserverDomain(domain string) string {
149+
for env, envDomain := range envs {
150+
if domain == envDomain {
151+
return env
152+
}
153+
}
154+
return ""
155+
}
156+
157+
func saveDesktopLogin(ctx *cli.Context, account *DesktopAccount) (string, string, error) {
158+
homeserver, err := desktopAccountHomeserverDomain(account)
159+
if err != nil {
160+
return "", "", err
161+
}
162+
env := ctx.String("env")
163+
if homeserverEnv := envForHomeserverDomain(homeserver); homeserverEnv != "" {
164+
env = homeserverEnv
165+
homeserver = envs[env]
166+
} else if homeserver == "" {
167+
homeserver = ctx.String("homeserver")
168+
}
169+
170+
whoami, err := beeperapi.Whoami(homeserver, account.AccessToken)
171+
if err != nil {
172+
return "", "", fmt.Errorf("failed to verify desktop credentials with whoami: %w", err)
173+
}
174+
175+
cfg := GetConfig(ctx)
176+
envCfg := cfg.Environments.Get(env)
177+
envCfg.ClusterID = whoami.UserInfo.BridgeClusterID
178+
envCfg.Username = whoami.UserInfo.Username
179+
envCfg.AccessToken = account.AccessToken
180+
envCfg.BridgeDataDir = filepath.Join(UserDataDir, "bbctl", env)
181+
err = cfg.Save()
182+
if err != nil {
183+
return "", "", fmt.Errorf("failed to save config: %w", err)
184+
}
185+
186+
return env, homeserver, nil
187+
}
188+
189+
func loginDesktop(ctx *cli.Context) error {
190+
dbPath, err := getLoginDesktopAccountDBPath(ctx)
191+
if err != nil {
192+
return err
193+
}
194+
195+
account, err := readDesktopAccount(ctx.Context, dbPath)
196+
if err != nil {
197+
return err
198+
}
199+
200+
env, homeserver, err := saveDesktopLogin(ctx, account)
201+
if err != nil {
202+
return err
203+
}
204+
205+
fmt.Printf("Imported Desktop login for @%s into bbctl env %q (%s)\n", account.UserID, env, homeserver)
206+
return nil
207+
}

cmd/bbctl/login-email.go

Lines changed: 62 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,29 +11,85 @@ import (
1111
"maunium.net/go/mautrix"
1212

1313
"github.com/beeper/bridge-manager/api/beeperapi"
14-
"github.com/beeper/bridge-manager/cli/interactive"
1514
)
1615

1716
var loginCommand = &cli.Command{
1817
Name: "login",
1918
Aliases: []string{"l"},
2019
Usage: "Log into the Beeper server",
21-
Before: interactive.Ask,
2220
Action: beeperLogin,
2321
Flags: []cli.Flag{
24-
interactive.Flag{Flag: &cli.StringFlag{
22+
&cli.StringFlag{
2523
Name: "email",
2624
EnvVars: []string{"BEEPER_EMAIL"},
2725
Usage: "The Beeper account email to log in with",
28-
}, Survey: &survey.Input{
29-
Message: "Email:",
30-
}},
26+
},
27+
&cli.BoolFlag{
28+
Name: "no-desktop",
29+
EnvVars: []string{"BBCTL_NO_DESKTOP_LOGIN"},
30+
Usage: "Skip checking for an existing Beeper Desktop login",
31+
},
3132
},
3233
}
3334

35+
func init() {
36+
loginCommand.Flags = append(loginCommand.Flags, desktopLoginFlags()...)
37+
}
38+
39+
func maybeUseDesktopLogin(ctx *cli.Context) (bool, error) {
40+
if ctx.Bool("no-desktop") {
41+
return false, nil
42+
}
43+
dbPath, err := getLoginDesktopAccountDBPath(ctx)
44+
if err != nil {
45+
return false, err
46+
}
47+
account, err := readDesktopAccount(ctx.Context, dbPath)
48+
if err != nil {
49+
if ctx.IsSet("desktop-account-db") || ctx.IsSet("desktop-data-dir") {
50+
return false, err
51+
}
52+
return false, nil
53+
}
54+
55+
useDesktop := false
56+
err = survey.AskOne(&survey.Confirm{
57+
Message: fmt.Sprintf("Use Beeper Desktop login for %s?", account.UserID),
58+
Default: true,
59+
}, &useDesktop)
60+
if err != nil {
61+
return false, err
62+
}
63+
if !useDesktop {
64+
return false, nil
65+
}
66+
67+
env, homeserver, err := saveDesktopLogin(ctx, account)
68+
if err != nil {
69+
return false, err
70+
}
71+
fmt.Printf("Imported Desktop login for %s into bbctl env %q (%s)\n", account.UserID, env, homeserver)
72+
return true, nil
73+
}
74+
3475
func beeperLogin(ctx *cli.Context) error {
76+
didLogin, err := maybeUseDesktopLogin(ctx)
77+
if err != nil {
78+
return err
79+
} else if didLogin {
80+
return nil
81+
}
82+
3583
homeserver := ctx.String("homeserver")
3684
email := ctx.String("email")
85+
if email == "" {
86+
err = survey.AskOne(&survey.Input{
87+
Message: "Email:",
88+
}, &email)
89+
if err != nil {
90+
return err
91+
}
92+
}
3793

3894
startLogin, err := beeperapi.StartLogin(homeserver)
3995
if err != nil {

cmd/bbctl/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ var app = &cli.App{
140140
Before: prepareApp,
141141
Commands: []*cli.Command{
142142
loginCommand,
143+
loginDesktopCommand,
143144
loginPasswordCommand,
144145
logoutCommand,
145146
registerCommand,

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ require (
2424
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
2525
github.com/mattn/go-colorable v0.1.14 // indirect
2626
github.com/mattn/go-isatty v0.0.20 // indirect
27+
github.com/mattn/go-sqlite3 v1.14.34 // indirect
2728
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
29+
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
2830
github.com/rivo/uniseg v0.4.7 // indirect
2931
github.com/russross/blackfriday/v2 v2.1.0 // indirect
3032
github.com/tidwall/match v1.1.1 // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
22
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
33
github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ=
44
github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo=
5+
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
6+
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
57
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s=
68
github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w=
79
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
@@ -34,10 +36,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
3436
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
3537
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
3638
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
39+
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
40+
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
3741
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
3842
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
3943
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
4044
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
45+
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
46+
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
4147
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
4248
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
4349
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

0 commit comments

Comments
 (0)