From 4c558ba08a34cf2527b5694ba453c4171063dc96 Mon Sep 17 00:00:00 2001 From: Ludvig Rhodin Date: Sat, 7 Feb 2026 11:02:05 -0800 Subject: [PATCH 1/3] Add imessage-v2 bridge type for rustpush/bridgev2 iMessage bridge Add support for the community-built iMessage bridge at github.com/lrhodin/imessage which uses rustpush for native iMessage integration on macOS with the bridgev2 framework. Changes: - Add imessage-v2.tpl.yaml: bridgev2 config template with iMessage network settings - Register imessage-v2 as an official bridge type with websocket support - Add askParams stub (no interactive params needed) - Handle imessage-v2 in config generation (startup command, install link) - Handle imessage-v2 in run/compile (binary name, git repo URL) --- bridgeconfig/imessage-v2.tpl.yaml | 23 +++++++++++++++++++++++ cmd/bbctl/bridgeutil.go | 2 ++ cmd/bbctl/config.go | 7 +++++++ cmd/bbctl/run.go | 7 ++++++- 4 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 bridgeconfig/imessage-v2.tpl.yaml diff --git a/bridgeconfig/imessage-v2.tpl.yaml b/bridgeconfig/imessage-v2.tpl.yaml new file mode 100644 index 0000000..0f17f9d --- /dev/null +++ b/bridgeconfig/imessage-v2.tpl.yaml @@ -0,0 +1,23 @@ +# Network-specific config options for iMessage (rustpush / bridgev2) +network: + # Displayname template for iMessage users. + displayname_template: '{{`{{if .FirstName}}{{.FirstName}}{{if .LastName}} {{.LastName}}{{end}}{{else if .Nickname}}{{.Nickname}}{{else if .Phone}}{{.Phone}}{{else if .Email}}{{.Email}}{{else}}{{.ID}}{{end}}`}}' + # Should SMS chats always be in the same room as iMessage chats with the same phone number? + disable_sms_portals: false + # Send captions in the same message as images using MSC2530? + caption_in_message: true + # Should we convert heif images to jpeg before re-uploading? + convert_heif: false + # Should we convert tiff images to jpeg before re-uploading? + convert_tiff: true + # How many days back to look for chats during initial sync (default: 365) + initial_sync_days: 365 + +{{ setfield . "CommandPrefix" "!im" -}} +{{ setfield . "DatabaseFileName" "mautrix-imessage" -}} +{{ setfield . "BridgeTypeName" "iMessage" -}} +{{ setfield . "BridgeTypeIcon" "mxc://maunium.net/tManJEpANASZvDVzvRvhILdX" -}} +{{ setfield . "DefaultPickleKey" "mautrix.bridge.e2ee" -}} +{{ setfield . "MaxInitialMessages" 100 -}} +{{ setfield . "MaxBackwardMessages" 100 -}} +{{ template "bridgev2.tpl.yaml" . }} diff --git a/cmd/bbctl/bridgeutil.go b/cmd/bbctl/bridgeutil.go index 4a7eaf8..555ef44 100644 --- a/cmd/bbctl/bridgeutil.go +++ b/cmd/bbctl/bridgeutil.go @@ -24,6 +24,7 @@ var officialBridges = []bridgeTypeToNames{ {"discord", []string{"discord"}}, {"meta", []string{"meta", "instagram", "facebook"}}, {"googlechat", []string{"googlechat", "gchat"}}, + {"imessage-v2", []string{"imessage-v2"}}, {"imessagego", []string{"imessagego"}}, {"imessage", []string{"imessage"}}, {"linkedin", []string{"linkedin"}}, @@ -46,6 +47,7 @@ var websocketBridges = map[string]bool{ "gvoice": true, "heisenbridge": true, "imessage": true, + "imessage-v2": true, "imessagego": true, "signal": true, "bridgev2": true, diff --git a/cmd/bbctl/config.go b/cmd/bbctl/config.go index 0620b26..640f005 100644 --- a/cmd/bbctl/config.go +++ b/cmd/bbctl/config.go @@ -118,6 +118,10 @@ var askParams = map[string]func(string, map[string]string) (bool, error){ } return didAddParams, nil }, + "imessage-v2": func(bridgeName string, extraParams map[string]string) (bool, error) { + // imessage-v2 is a bridgev2 bridge with rustpush, no connector selection needed + return false, nil + }, "imessage": func(bridgeName string, extraParams map[string]string) (bool, error) { platform := extraParams["imessage_platform"] barcelonaPath := extraParams["barcelona_path"] @@ -346,6 +350,9 @@ func generateBridgeConfig(ctx *cli.Context) error { } var startupCommand, installInstructions string switch cfg.BridgeType { + case "imessage-v2": + startupCommand = "mautrix-imessage-v2 -c " + outputPath + installInstructions = "https://github.com/lrhodin/imessage" case "imessage", "whatsapp", "discord", "slack", "gmessages", "gvoice", "signal", "meta", "twitter", "bluesky", "linkedin": startupCommand = fmt.Sprintf("mautrix-%s", cfg.BridgeType) if outputPath != "config.yaml" && outputPath != "" { diff --git a/cmd/bbctl/run.go b/cmd/bbctl/run.go index 408c28f..ce3b337 100644 --- a/cmd/bbctl/run.go +++ b/cmd/bbctl/run.go @@ -134,6 +134,8 @@ func compileGoBridge(ctx context.Context, buildDir, binaryPath, bridgeType strin repo := fmt.Sprintf("https://github.com/mautrix/%s.git", bridgeType) if bridgeType == "imessagego" { repo = "https://github.com/beeper/imessage.git" + } else if bridgeType == "imessage-v2" { + repo = "https://github.com/lrhodin/imessage.git" } log.Printf("Cloning [cyan]%s[reset] to [cyan]%s[reset]", repo, buildDir) err = makeCmd(ctx, buildDirParent, "git", "clone", repo, buildDir).Run() @@ -308,7 +310,7 @@ func runBridge(ctx *cli.Context) error { var bridgeArgs []string var needsWebsocketProxy bool switch cfg.BridgeType { - case "imessage", "imessagego", "whatsapp", "discord", "slack", "gmessages", "gvoice", + case "imessage", "imessagego", "imessage-v2", "whatsapp", "discord", "slack", "gmessages", "gvoice", "signal", "meta", "twitter", "bluesky", "linkedin", "telegram": ciBridgeType := cfg.BridgeType binaryName := fmt.Sprintf("mautrix-%s", cfg.BridgeType) @@ -337,6 +339,9 @@ func runBridge(ctx *cli.Context) error { return fmt.Errorf("failed to compile bridge: %w", err) } } else if overrideBridgeCmd == "" { + if cfg.BridgeType == "imessage-v2" { + return UserError{"imessage-v2 does not have prebuilt binaries. Use --compile to build locally, or see https://github.com/lrhodin/imessage"} + } err = updateGoBridge(ctx.Context, bridgeCmd, ciBridgeType, ciV2, ctx.Bool("no-update")) if errors.Is(err, gitlab.ErrNotBuiltInCI) { return UserError{fmt.Sprintf("Binaries for %s are not built in the CI. Use --compile to tell bbctl to build the bridge locally.", binaryName)} From 529bd0134cda16fea49325c08675cb43dc20733e Mon Sep 17 00:00:00 2001 From: Ludvig Rhodin Date: Sat, 7 Feb 2026 12:53:26 -0800 Subject: [PATCH 2/3] Fix iMessage display in Beeper UI and add rustpush connector option - register.go: Map imessage-v2 bridge type to 'imessage' when posting bridge state so Beeper's UI recognizes it correctly. - config.go: Add 'rustpush' as a connector option (default) under the existing imessage type, with proper config template and websocket routing. Co-authored-by: David Brustein --- cmd/bbctl/config.go | 16 +++++++++++----- cmd/bbctl/register.go | 8 +++++++- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/cmd/bbctl/config.go b/cmd/bbctl/config.go index 640f005..67f4ef6 100644 --- a/cmd/bbctl/config.go +++ b/cmd/bbctl/config.go @@ -135,13 +135,14 @@ var askParams = map[string]func(string, map[string]string) (bool, error){ if platform == "" { err := survey.AskOne(&survey.Select{ Message: "Select iMessage connector:", - Options: []string{"mac", "mac-nosip", "bluebubbles"}, + Options: []string{"rustpush", "mac", "mac-nosip", "bluebubbles"}, Description: simpleDescriptions(map[string]string{ + "rustpush": "Connect directly to Apple's iMessage servers via rustpush (macOS 14.2+, recommended)", "mac": "Use AppleScript to send messages and read chat.db for incoming data - only requires Full Disk Access (from system settings)", "mac-nosip": "Use Barcelona to interact with private APIs - requires disabling SIP and AMFI", "bluebubbles": "Connect to a BlueBubbles instance", }), - Default: "mac", + Default: "rustpush", }, &platform) if err != nil { return didAddParams, err @@ -281,13 +282,18 @@ func doGenerateBridgeConfig(ctx *cli.Context, bridge string) (*generatedBridgeCo if dbPrefix != "" { dbPrefix = filepath.Join(dbPrefix, bridge+"-") } - websocket := websocketBridges[bridgeType] + // When rustpush is selected under the imessage type, use the imessage-v2 template. + configTemplate := bridgeType + if bridgeType == "imessage" && extraParams["imessage_platform"] == "rustpush" { + configTemplate = "imessage-v2" + } + websocket := websocketBridges[bridgeType] || configTemplate == "imessage-v2" var listenAddress string var listenPort uint16 if !websocket { listenAddress, listenPort, reg.Registration.URL = getBridgeWebsocketProxyConfig(bridge, bridgeType) } - cfg, err := bridgeconfig.Generate(bridgeType, bridgeconfig.Params{ + cfg, err := bridgeconfig.Generate(configTemplate, bridgeconfig.Params{ HungryAddress: reg.HomeserverURL, BeeperDomain: ctx.String("homeserver"), Websocket: websocket, @@ -306,7 +312,7 @@ func doGenerateBridgeConfig(ctx *cli.Context, bridge string) (*generatedBridgeCo ProvisioningSecret: whoami.User.AsmuxData.LoginToken, }) return &generatedBridgeConfig{ - BridgeType: bridgeType, + BridgeType: configTemplate, Config: cfg, RegisterJSON: reg, }, err diff --git a/cmd/bbctl/register.go b/cmd/bbctl/register.go index 634da11..8b4c9dd 100644 --- a/cmd/bbctl/register.go +++ b/cmd/bbctl/register.go @@ -112,12 +112,18 @@ func doRegisterBridge(ctx *cli.Context, bridge, bridgeType string, onlyGet bool) state = status.StateStarting } + // Beeper's UI recognizes "imessage" — map imessage-v2 so it displays correctly. + displayBridgeType := bridgeType + if bridgeType == "imessage-v2" { + displayBridgeType = "imessage" + } + if !ctx.Bool("no-state") { err = beeperapi.PostBridgeState(ctx.String("homeserver"), GetEnvConfig(ctx).Username, bridge, resp.AppToken, beeperapi.ReqPostBridgeState{ StateEvent: state, Reason: "SELF_HOST_REGISTERED", IsSelfHosted: true, - BridgeType: bridgeType, + BridgeType: displayBridgeType, }) if err != nil { return nil, fmt.Errorf("failed to mark bridge as RUNNING: %w", err) From 581e96ee8a7a5a229a0fa739201d25a5ec9b59e9 Mon Sep 17 00:00:00 2001 From: Ludvig Rhodin Date: Sun, 8 Feb 2026 18:04:01 -0800 Subject: [PATCH 3/3] Fix bbctl config --type flag being ignored when bridge already registered The CLI --type flag should take precedence over the server-reported bridgeType. Previously, if the server already had a bridge registered, its BridgeState.BridgeType was always used, making the --type flag a no-op. This caused config generation to pick the wrong bridge type (e.g. "imessage" instead of "imessage-v2"). Fix: check ctx.String("type") first before falling back to the server-reported type. --- cmd/bbctl/config.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/bbctl/config.go b/cmd/bbctl/config.go index 67f4ef6..486d358 100644 --- a/cmd/bbctl/config.go +++ b/cmd/bbctl/config.go @@ -238,7 +238,9 @@ func doGenerateBridgeConfig(ctx *cli.Context, bridge string) (*generatedBridgeCo } existingBridge, ok := whoami.User.Bridges[bridge] var bridgeType string - if ok && existingBridge.BridgeState.BridgeType != "" { + if cliType := ctx.String("type"); cliType != "" { + bridgeType = cliType + } else if ok && existingBridge.BridgeState.BridgeType != "" { bridgeType = existingBridge.BridgeState.BridgeType } else { bridgeType, err = guessOrAskBridgeType(bridge, ctx.String("type"))