-
Notifications
You must be signed in to change notification settings - Fork 90
Add Beeper Desktop login support #75
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3aacb0d
c479795
d2167a3
3fb98b2
e860569
e7f774a
b2ef08d
0e66b49
7227de4
76a0cb7
4cb3037
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,186 @@ | ||||||||||||||||||||||||||||||
| package main | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||
| "database/sql" | ||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||
| "net/url" | ||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| "github.com/urfave/cli/v2" | ||||||||||||||||||||||||||||||
| "go.mau.fi/util/dbutil" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| "github.com/beeper/bridge-manager/api/beeperapi" | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| _ "go.mau.fi/util/dbutil/litestream" | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func desktopLoginFlags() []cli.Flag { | ||||||||||||||||||||||||||||||
| return []cli.Flag{ | ||||||||||||||||||||||||||||||
| &cli.StringFlag{ | ||||||||||||||||||||||||||||||
| Name: "profile", | ||||||||||||||||||||||||||||||
| EnvVars: []string{"BEEPER_PROFILE"}, | ||||||||||||||||||||||||||||||
| Usage: "Beeper Desktop profile name, equivalent to BEEPER_PROFILE in Desktop", | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| &cli.StringFlag{ | ||||||||||||||||||||||||||||||
| Name: "desktop-data-dir", | ||||||||||||||||||||||||||||||
| EnvVars: []string{"BBCTL_DESKTOP_DATA_DIR"}, | ||||||||||||||||||||||||||||||
| Usage: "Read credentials from this Beeper Desktop user data directory", | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| type DesktopAccount struct { | ||||||||||||||||||||||||||||||
| UserID string | ||||||||||||||||||||||||||||||
| AccessToken string | ||||||||||||||||||||||||||||||
| Homeserver string | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func getDesktopDataDir(ctx *cli.Context) (string, error) { | ||||||||||||||||||||||||||||||
| if dataDir := ctx.String("desktop-data-dir"); dataDir != "" { | ||||||||||||||||||||||||||||||
| return dataDir, nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return resolveDesktopDataDir(ctx.String("profile")) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func resolveDesktopDataDir(profile string) (string, error) { | ||||||||||||||||||||||||||||||
| appName := "BeeperTexts" | ||||||||||||||||||||||||||||||
| if profile != "" { | ||||||||||||||||||||||||||||||
| appName += "-" + profile | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| dataDir, err := os.UserConfigDir() | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return filepath.Join(dataDir, appName), nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func getLoginDesktopAccountDBPath(ctx *cli.Context) (string, error) { | ||||||||||||||||||||||||||||||
| dataDir, err := getDesktopDataDir(ctx) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", fmt.Errorf("failed to resolve desktop data directory: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return filepath.Join(dataDir, "account.db"), nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func readDesktopAccount(ctx context.Context, dbPath string) (account *DesktopAccount, err error) { | ||||||||||||||||||||||||||||||
| dbURI := (&url.URL{ | ||||||||||||||||||||||||||||||
| Scheme: "file", | ||||||||||||||||||||||||||||||
| Path: filepath.ToSlash(dbPath), | ||||||||||||||||||||||||||||||
| RawQuery: "mode=ro", | ||||||||||||||||||||||||||||||
| }).String() | ||||||||||||||||||||||||||||||
| db, err := dbutil.NewWithDialect(dbURI, "sqlite3-fk-wal") | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to open desktop account database: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| defer func() { | ||||||||||||||||||||||||||||||
| if closeErr := db.Close(); closeErr != nil { | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| err = fmt.Errorf("%w; failed to close desktop account database: %v", err, closeErr) | ||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||
| err = fmt.Errorf("failed to close desktop account database: %w", closeErr) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }() | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| var desktopAccount DesktopAccount | ||||||||||||||||||||||||||||||
| err = db.QueryRow(ctx, "SELECT user_id, access_token, homeserver FROM account LIMIT 1"). | ||||||||||||||||||||||||||||||
| Scan(&desktopAccount.UserID, &desktopAccount.AccessToken, &desktopAccount.Homeserver) | ||||||||||||||||||||||||||||||
| if errors.Is(err, sql.ErrNoRows) { | ||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("desktop account database has no logged-in account") | ||||||||||||||||||||||||||||||
| } else if err != nil { | ||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("failed to read desktop account database: %w", err) | ||||||||||||||||||||||||||||||
| } else if desktopAccount.UserID == "" || desktopAccount.AccessToken == "" { | ||||||||||||||||||||||||||||||
| return nil, fmt.Errorf("desktop account database has incomplete credentials") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return &desktopAccount, nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func desktopAccountHomeserverDomain(account *DesktopAccount) (string, error) { | ||||||||||||||||||||||||||||||
| if account.Homeserver == "" { | ||||||||||||||||||||||||||||||
| return "", nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| parsed, err := url.Parse(account.Homeserver) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", fmt.Errorf("desktop account has invalid homeserver URL %q: %w", account.Homeserver, err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return strings.TrimPrefix(parsed.Host, "matrix."), nil | ||||||||||||||||||||||||||||||
|
Comment on lines
+106
to
+110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 Result: Yes. In Go’s net/url package, URL.Host includes the port when it is present (it is documented as “host” or “host:port”). The method URL.Hostname strips any valid port from URL.Host. Examples (using Go’s behavior): 1) Hostname strips an explicit port - Parse: https://example.com:8443/path - u.Host == "example.com:8443" - u.Hostname == "example.com" - u.Port == "8443" 2) Hostname on an IPv6 literal with a port - Parse: http://[::1]:8080/ - u.Host == "[::1]:8080" - u.Hostname == "::1" (brackets removed) - u.Port == "8080" 3) When there is no port, Hostname returns the whole host - Parse: https://example.com/path - u.Host == "example.com" - u.Hostname == "example.com" - u.Port == "" (empty because there’s no numeric port in Host) Authoritative notes from the net/url docs: - The URL.Host field “contains the host and port subcomponents” and “when the port is present, it is separated … with a colon” (and IPv6 is bracketed as “[ip]:port”). - URL.Hostname “returns u.Host, stripping any valid port number if present.” Citations:
🏁 Script executed: # Find and examine the desktopauth.go file
fd -t f 'desktopauth.go'Repository: beeper/bridge-manager Length of output: 90 🏁 Script executed: # Look at the specific function and surrounding context
cat -n cmd/bbctl/desktopauth.go | sed -n '130,150p'Repository: beeper/bridge-manager Length of output: 721 🏁 Script executed: # Search for envForHomeserverDomain to understand how this value is used
rg -n 'envForHomeserverDomain' --type goRepository: beeper/bridge-manager Length of output: 254 🏁 Script executed: # Search for desktopAccountHomeserverDomain usage
rg -n 'desktopAccountHomeserverDomain' --type goRepository: beeper/bridge-manager Length of output: 351 🏁 Script executed: # Look at the envs map to understand what domains are expected
rg -n 'var envs|const envs' --type go -A 10 cmd/bbctl/desktopauth.goRepository: beeper/bridge-manager Length of output: 47 🏁 Script executed: # Look at the full context around line 153-158 where desktopAccountHomeserverDomain is called
cat -n cmd/bbctl/desktopauth.go | sed -n '150,165p'Repository: beeper/bridge-manager Length of output: 669 🏁 Script executed: # Look at the desktopAuth function to understand the full flow
cat -n cmd/bbctl/desktopauth.go | sed -n '150,210p'Repository: beeper/bridge-manager Length of output: 2370 Use
Proposed fix func desktopAccountHomeserverDomain(account *DesktopAccount) (string, error) {
if account.Homeserver == "" {
return "", nil
}
parsed, err := url.Parse(account.Homeserver)
if err != nil {
return "", fmt.Errorf("desktop account has invalid homeserver URL %q: %w", account.Homeserver, err)
}
- return strings.TrimPrefix(parsed.Host, "matrix."), nil
+ host := parsed.Hostname()
+ if host == "" {
+ return "", fmt.Errorf("desktop account homeserver URL %q has no host", account.Homeserver)
+ }
+ return strings.TrimPrefix(host, "matrix."), nil
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. beeper hostnames are matrix.beeper.localtest.me, matrix.beeper-dev.com, matrix.beeper-staging.com, matrix.beeper.com There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Feel free to disregard the port concern. The empty-host guard is still a minor defensive improvement, but it's entirely up to you whether it's worth the added lines. (´・ω・`) ~ curious bunny learns new things ✏️ Learnings added
coderabbitai[bot] marked this conversation as resolved.
|
||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func envForHomeserverDomain(domain string) string { | ||||||||||||||||||||||||||||||
| for env, envDomain := range envs { | ||||||||||||||||||||||||||||||
| if domain == envDomain { | ||||||||||||||||||||||||||||||
| return env | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func configureDesktopLogin(ctx *cli.Context, account *DesktopAccount) (string, string, error) { | ||||||||||||||||||||||||||||||
| homeserver, err := desktopAccountHomeserverDomain(account) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", "", err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| env := ctx.String("env") | ||||||||||||||||||||||||||||||
| if homeserverEnv := envForHomeserverDomain(homeserver); homeserverEnv != "" { | ||||||||||||||||||||||||||||||
| env = homeserverEnv | ||||||||||||||||||||||||||||||
| homeserver = envs[env] | ||||||||||||||||||||||||||||||
| } else if homeserver == "" { | ||||||||||||||||||||||||||||||
| homeserver = ctx.String("homeserver") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| whoami, err := beeperapi.Whoami(homeserver, account.AccessToken) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", "", fmt.Errorf("failed to verify desktop credentials with whoami: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| cfg := GetConfig(ctx) | ||||||||||||||||||||||||||||||
| envCfg := cfg.Environments.Get(env) | ||||||||||||||||||||||||||||||
| envCfg.ClusterID = whoami.UserInfo.BridgeClusterID | ||||||||||||||||||||||||||||||
| envCfg.Username = whoami.UserInfo.Username | ||||||||||||||||||||||||||||||
| envCfg.AccessToken = account.AccessToken | ||||||||||||||||||||||||||||||
| envCfg.BridgeDataDir = filepath.Join(UserDataDir, "bbctl", env) | ||||||||||||||||||||||||||||||
| dataDir, err := getDesktopDataDir(ctx) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", "", fmt.Errorf("failed to resolve desktop data directory: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| envCfg.DesktopDataDir = dataDir | ||||||||||||||||||||||||||||||
| err = cfg.Save() | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return "", "", fmt.Errorf("failed to save config: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return env, homeserver, nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| func loadDesktopLogin(ctx *cli.Context, envConfig *EnvConfig) error { | ||||||||||||||||||||||||||||||
| if envConfig.DesktopDataDir == "" { | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| dbPath := filepath.Join(envConfig.DesktopDataDir, "account.db") | ||||||||||||||||||||||||||||||
| account, err := readDesktopAccount(ctx.Context, dbPath) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| homeserver, err := desktopAccountHomeserverDomain(account) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| if homeserver == "" { | ||||||||||||||||||||||||||||||
| homeserver = ctx.String("homeserver") | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| whoami, err := beeperapi.Whoami(homeserver, account.AccessToken) | ||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||
| return fmt.Errorf("failed to verify desktop credentials with whoami: %w", err) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| envConfig.ClusterID = whoami.UserInfo.BridgeClusterID | ||||||||||||||||||||||||||||||
| envConfig.Username = whoami.UserInfo.Username | ||||||||||||||||||||||||||||||
| envConfig.AccessToken = account.AccessToken | ||||||||||||||||||||||||||||||
| if envConfig.BridgeDataDir == "" { | ||||||||||||||||||||||||||||||
| envConfig.BridgeDataDir = filepath.Join(UserDataDir, "bbctl", ctx.String("env")) | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Broaden
HasCredentials()to accept imported Desktop tokens.RequiresAuth()and the startup path inmain.goboth rely on this gate. Limiting it tosyt_/bat_still treats other non-empty access tokens as logged out, so a successful Desktop import can be ignored by later commands.Suggested fix
func (ec *EnvConfig) HasCredentials() bool { - return strings.HasPrefix(ec.AccessToken, "syt_") || strings.HasPrefix(ec.AccessToken, "bat_") + return ec.AccessToken != "" }📝 Committable suggestion
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
desktop token is also a matrix token
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@batuhan, thanks for the clarification! Since the Desktop access token is a standard Matrix token, it will already carry asyt_orbat_prefix, so the current prefix-based check inHasCredentials()is correct and my suggestion was unnecessary. I'll withdraw it.✏️ Learnings added