diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c0add220..deaa1f1d 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -15,7 +15,7 @@ jobs: - name: Set up Go uses: actions/setup-go@v6 with: - go-version: "1.26" + go-version: "1.25" cache: true - name: Install libolm @@ -35,8 +35,8 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.25", "1.26"] - name: Build (${{ matrix.go-version == '1.26' && 'latest' || 'old' }}, libolm) + go-version: ["1.24", "1.25"] + name: Build (${{ matrix.go-version == '1.25' && 'latest' || 'old' }}, libolm) steps: - uses: actions/checkout@v6 @@ -62,6 +62,7 @@ jobs: run: go test -json -v ./... 2>&1 | gotestfmt - name: Test (jsonv2) + if: matrix.go-version == '1.25' env: GOEXPERIMENT: jsonv2 run: go test -json -v ./... 2>&1 | gotestfmt @@ -71,8 +72,8 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.25", "1.26"] - name: Build (${{ matrix.go-version == '1.26' && 'latest' || 'old' }}, goolm) + go-version: ["1.24", "1.25"] + name: Build (${{ matrix.go-version == '1.25' && 'latest' || 'old' }}, goolm) steps: - uses: actions/checkout@v6 diff --git a/CHANGELOG.md b/CHANGELOG.md index f2829199..dbc7c494 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,24 +1,3 @@ -## v0.26.3 (2026-02-16) - -* Bumped minimum Go version to 1.25. -* *(client)* Added fields for sending [MSC4354] sticky events. -* *(bridgev2)* Added automatic message request accepting when sending message. -* *(mediaproxy)* Added support for federation thumbnail endpoint. -* *(crypto/ssss)* Improved support for recovery keys with slightly broken - metadata. -* *(crypto)* Changed key import to call session received callback even for - sessions that already exist in the database. -* *(appservice)* Fixed building websocket URL accidentally using file path - separators instead of always `/`. -* *(crypto)* Fixed key exports not including the `sender_claimed_keys` field. -* *(client)* Fixed incorrect context usage in async uploads. -* *(crypto)* Fixed panic when passing invalid input to megolm message index - parser used for debugging. -* *(bridgev2/provisioning)* Fixed completed or failed logins not being cleaned - up properly. - -[MSC4354]: https://github.com/matrix-org/matrix-spec-proposals/pull/4354 - ## v0.26.2 (2026-01-16) * *(bridgev2)* Added chunked portal deletion to avoid database locks when diff --git a/README.md b/README.md index b1a2edf8..ac41ca78 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,8 @@ # mautrix-go [![GoDoc](https://pkg.go.dev/badge/maunium.net/go/mautrix)](https://pkg.go.dev/maunium.net/go/mautrix) -A Golang Matrix framework. Used by [gomuks](https://gomuks.app), -[go-neb](https://github.com/matrix-org/go-neb), -[mautrix-whatsapp](https://github.com/mautrix/whatsapp) +A Golang Matrix framework. Used by [gomuks](https://matrix.org/docs/projects/client/gomuks), +[go-neb](https://github.com/matrix-org/go-neb), [mautrix-whatsapp](https://github.com/mautrix/whatsapp) and others. Matrix room: [`#go:maunium.net`](https://matrix.to/#/#go:maunium.net) @@ -14,10 +13,9 @@ The original project is licensed under [Apache 2.0](https://github.com/matrix-or In addition to the basic client API features the original project has, this framework also has: * Appservice support (Intent API like mautrix-python, room state storage, etc) -* End-to-end encryption support (incl. key backup, cross-signing, interactive verification, etc) +* End-to-end encryption support (incl. interactive SAS verification) * High-level module for building puppeting bridges -* Partial federation module (making requests, PDU processing and event authorization) -* A media proxy server which can be used to expose anything as a Matrix media repo +* High-level module for building chat clients * Wrapper functions for the Synapse admin API * Structs for parsing event content * Helpers for parsing and generating Matrix HTML diff --git a/appservice/intent.go b/appservice/intent.go index 5d43f190..e4d8e100 100644 --- a/appservice/intent.go +++ b/appservice/intent.go @@ -51,7 +51,7 @@ func (as *AppService) NewIntentAPI(localpart string) *IntentAPI { } func (intent *IntentAPI) Register(ctx context.Context) error { - _, err := intent.Client.MakeRequest(ctx, http.MethodPost, intent.BuildClientURL("v3", "register"), &mautrix.ReqRegister[any]{ + _, err := intent.Client.MakeRequest(ctx, http.MethodPost, intent.BuildClientURL("v3", "register"), &mautrix.ReqRegister{ Username: intent.Localpart, Type: mautrix.AuthTypeAppservice, InhibitLogin: true, @@ -222,17 +222,6 @@ func (intent *IntentAPI) SendMessageEvent(ctx context.Context, roomID id.RoomID, return intent.Client.SendMessageEvent(ctx, roomID, eventType, contentJSON, extra...) } -func (intent *IntentAPI) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...mautrix.ReqSendEvent) (*mautrix.RespSendEvent, error) { - if err := intent.EnsureJoined(ctx, roomID); err != nil { - return nil, err - } - if !intent.SpecVersions.Supports(mautrix.BeeperFeatureEphemeralEvents) { - return nil, mautrix.MUnrecognized.WithMessage("Homeserver does not advertise com.beeper.ephemeral support") - } - contentJSON = intent.AddDoublePuppetValue(contentJSON) - return intent.Client.BeeperSendEphemeralEvent(ctx, roomID, eventType, contentJSON, extra...) -} - // Deprecated: use SendMessageEvent with mautrix.ReqSendEvent.Timestamp instead func (intent *IntentAPI) SendMassagedMessageEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON interface{}, ts int64) (*mautrix.RespSendEvent, error) { return intent.SendMessageEvent(ctx, roomID, eventType, contentJSON, mautrix.ReqSendEvent{Timestamp: ts}) diff --git a/appservice/websocket.go b/appservice/websocket.go index ef65e65a..4f2538bf 100644 --- a/appservice/websocket.go +++ b/appservice/websocket.go @@ -14,7 +14,7 @@ import ( "io" "net/http" "net/url" - "path" + "path/filepath" "strings" "sync" "sync/atomic" @@ -374,7 +374,7 @@ func (as *AppService) StartWebsocket(ctx context.Context, baseURL string, onConn copiedURL := *as.hsURLForClient parsed = &copiedURL } - parsed.Path = path.Join(parsed.Path, "_matrix/client/unstable/fi.mau.as_sync") + parsed.Path = filepath.Join(parsed.Path, "_matrix/client/unstable/fi.mau.as_sync") if parsed.Scheme == "http" { parsed.Scheme = "ws" } else if parsed.Scheme == "https" { diff --git a/bridgev2/bridgeconfig/config.go b/bridgev2/bridgeconfig/config.go index bd6b9c06..8b9aa019 100644 --- a/bridgev2/bridgeconfig/config.go +++ b/bridgev2/bridgeconfig/config.go @@ -62,40 +62,38 @@ type CleanupOnLogouts struct { } type BridgeConfig struct { - CommandPrefix string `yaml:"command_prefix"` - PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"` - PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` - AsyncEvents bool `yaml:"async_events"` - SplitPortals bool `yaml:"split_portals"` - ResendBridgeInfo bool `yaml:"resend_bridge_info"` - NoBridgeInfoStateKey bool `yaml:"no_bridge_info_state_key"` - BridgeStatusNotices string `yaml:"bridge_status_notices"` - UnknownErrorAutoReconnect time.Duration `yaml:"unknown_error_auto_reconnect"` - UnknownErrorMaxAutoReconnects int `yaml:"unknown_error_max_auto_reconnects"` - BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"` - BridgeNotices bool `yaml:"bridge_notices"` - TagOnlyOnCreate bool `yaml:"tag_only_on_create"` - OnlyBridgeTags []event.RoomTag `yaml:"only_bridge_tags"` - MuteOnlyOnCreate bool `yaml:"mute_only_on_create"` - DeduplicateMatrixMessages bool `yaml:"deduplicate_matrix_messages"` - CrossRoomReplies bool `yaml:"cross_room_replies"` - OutgoingMessageReID bool `yaml:"outgoing_message_re_id"` - RevertFailedStateChanges bool `yaml:"revert_failed_state_changes"` - KickMatrixUsers bool `yaml:"kick_matrix_users"` - CleanupOnLogout CleanupOnLogouts `yaml:"cleanup_on_logout"` - Relay RelayConfig `yaml:"relay"` - Permissions PermissionConfig `yaml:"permissions"` - Backfill BackfillConfig `yaml:"backfill"` + CommandPrefix string `yaml:"command_prefix"` + PersonalFilteringSpaces bool `yaml:"personal_filtering_spaces"` + PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` + AsyncEvents bool `yaml:"async_events"` + SplitPortals bool `yaml:"split_portals"` + ResendBridgeInfo bool `yaml:"resend_bridge_info"` + NoBridgeInfoStateKey bool `yaml:"no_bridge_info_state_key"` + BridgeStatusNotices string `yaml:"bridge_status_notices"` + UnknownErrorAutoReconnect time.Duration `yaml:"unknown_error_auto_reconnect"` + BridgeMatrixLeave bool `yaml:"bridge_matrix_leave"` + BridgeNotices bool `yaml:"bridge_notices"` + TagOnlyOnCreate bool `yaml:"tag_only_on_create"` + OnlyBridgeTags []event.RoomTag `yaml:"only_bridge_tags"` + MuteOnlyOnCreate bool `yaml:"mute_only_on_create"` + DeduplicateMatrixMessages bool `yaml:"deduplicate_matrix_messages"` + CrossRoomReplies bool `yaml:"cross_room_replies"` + OutgoingMessageReID bool `yaml:"outgoing_message_re_id"` + RevertFailedStateChanges bool `yaml:"revert_failed_state_changes"` + KickMatrixUsers bool `yaml:"kick_matrix_users"` + CleanupOnLogout CleanupOnLogouts `yaml:"cleanup_on_logout"` + Relay RelayConfig `yaml:"relay"` + Permissions PermissionConfig `yaml:"permissions"` + Backfill BackfillConfig `yaml:"backfill"` } type MatrixConfig struct { - MessageStatusEvents bool `yaml:"message_status_events"` - DeliveryReceipts bool `yaml:"delivery_receipts"` - MessageErrorNotices bool `yaml:"message_error_notices"` - SyncDirectChatList bool `yaml:"sync_direct_chat_list"` - FederateRooms bool `yaml:"federate_rooms"` - UploadFileThreshold int64 `yaml:"upload_file_threshold"` - GhostExtraProfileInfo bool `yaml:"ghost_extra_profile_info"` + MessageStatusEvents bool `yaml:"message_status_events"` + DeliveryReceipts bool `yaml:"delivery_receipts"` + MessageErrorNotices bool `yaml:"message_error_notices"` + SyncDirectChatList bool `yaml:"sync_direct_chat_list"` + FederateRooms bool `yaml:"federate_rooms"` + UploadFileThreshold int64 `yaml:"upload_file_threshold"` } type AnalyticsConfig struct { diff --git a/bridgev2/bridgeconfig/upgrade.go b/bridgev2/bridgeconfig/upgrade.go index 92515ea0..a0278672 100644 --- a/bridgev2/bridgeconfig/upgrade.go +++ b/bridgev2/bridgeconfig/upgrade.go @@ -33,7 +33,6 @@ func doUpgrade(helper up.Helper) { helper.Copy(up.Bool, "bridge", "no_bridge_info_state_key") helper.Copy(up.Str|up.Null, "bridge", "bridge_status_notices") helper.Copy(up.Str|up.Int|up.Null, "bridge", "unknown_error_auto_reconnect") - helper.Copy(up.Int, "bridge", "unknown_error_max_auto_reconnects") helper.Copy(up.Bool, "bridge", "bridge_matrix_leave") helper.Copy(up.Bool, "bridge", "bridge_notices") helper.Copy(up.Bool, "bridge", "tag_only_on_create") @@ -101,7 +100,6 @@ func doUpgrade(helper up.Helper) { helper.Copy(up.Bool, "matrix", "sync_direct_chat_list") helper.Copy(up.Bool, "matrix", "federate_rooms") helper.Copy(up.Int, "matrix", "upload_file_threshold") - helper.Copy(up.Bool, "matrix", "ghost_extra_profile_info") helper.Copy(up.Str|up.Null, "analytics", "token") helper.Copy(up.Str|up.Null, "analytics", "url") diff --git a/bridgev2/bridgestate.go b/bridgev2/bridgestate.go index 96d9fd5c..babbccab 100644 --- a/bridgev2/bridgestate.go +++ b/bridgev2/bridgestate.go @@ -37,8 +37,6 @@ type BridgeStateQueue struct { stopChan chan struct{} stopReconnect atomic.Pointer[context.CancelFunc] - - unknownErrorReconnects int } func (br *Bridge) SendGlobalBridgeState(state status.BridgeState) { @@ -194,14 +192,8 @@ func (bsq *BridgeStateQueue) unknownErrorReconnect(triggeredBy status.BridgeStat } else if prevUnsent.StateEvent != status.StateUnknownError || prev.StateEvent != status.StateUnknownError { log.Debug().Msg("Not reconnecting as the previous state was not an unknown error") return - } else if bsq.unknownErrorReconnects > bsq.bridge.Config.UnknownErrorMaxAutoReconnects { - log.Warn().Msg("Not reconnecting as the maximum number of unknown error reconnects has been reached") - return } - bsq.unknownErrorReconnects++ - log.Info(). - Int("reconnect_num", bsq.unknownErrorReconnects). - Msg("Disconnecting and reconnecting login due to unknown error") + log.Info().Msg("Disconnecting and reconnecting login due to unknown error") bsq.login.Disconnect() log.Debug().Msg("Disconnection finished, recreating client and reconnecting") err := bsq.login.recreateClient(ctx) diff --git a/bridgev2/commands/login.go b/bridgev2/commands/login.go index 96d62d3e..80a7c733 100644 --- a/bridgev2/commands/login.go +++ b/bridgev2/commands/login.go @@ -121,7 +121,6 @@ func fnLogin(ce *Event) { ce.Reply("Failed to start login: %v", err) return } - ce.Log.Debug().Any("first_step", nextStep).Msg("Created login process") nextStep = checkLoginCommandDirectParams(ce, login, nextStep) if nextStep != nil { @@ -252,19 +251,14 @@ func sendQR(ce *Event, qr string, prevEventID *id.EventID) error { return fmt.Errorf("failed to upload image: %w", err) } content := &event.MessageEventContent{ - MsgType: event.MsgImage, - FileName: "qr.png", - URL: qrMXC, - File: qrFile, + MsgType: event.MsgImage, + FileName: "qr.png", + URL: qrMXC, + File: qrFile, + Body: qr, Format: event.FormatHTML, FormattedBody: fmt.Sprintf("
%s
", html.EscapeString(qr)), - Info: &event.FileInfo{ - MimeType: "image/png", - Width: qrSizePx, - Height: qrSizePx, - Size: len(qrData), - }, } if *prevEventID != "" { content.SetEdit(*prevEventID) @@ -279,36 +273,6 @@ func sendQR(ce *Event, qr string, prevEventID *id.EventID) error { return nil } -func sendUserInputAttachments(ce *Event, atts []*bridgev2.LoginUserInputAttachment) error { - for _, att := range atts { - if att.FileName == "" { - return fmt.Errorf("missing attachment filename") - } - mxc, file, err := ce.Bot.UploadMedia(ce.Ctx, ce.RoomID, att.Content, att.FileName, att.Info.MimeType) - if err != nil { - return fmt.Errorf("failed to upload attachment %q: %w", att.FileName, err) - } - content := &event.MessageEventContent{ - MsgType: att.Type, - FileName: att.FileName, - URL: mxc, - File: file, - Info: &event.FileInfo{ - MimeType: att.Info.MimeType, - Width: att.Info.Width, - Height: att.Info.Height, - Size: att.Info.Size, - }, - Body: att.FileName, - } - _, err = ce.Bot.SendMessage(ce.Ctx, ce.RoomID, event.EventMessage, &event.Content{Parsed: content}, nil) - if err != nil { - return nil - } - } - return nil -} - type contextKey int const ( @@ -500,7 +464,6 @@ func maybeURLDecodeCookie(val string, field *bridgev2.LoginCookieField) string { } func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginStep, override *bridgev2.UserLogin) { - ce.Log.Debug().Any("next_step", step).Msg("Got next login step") if step.Instructions != "" { ce.Reply(step.Instructions) } @@ -515,10 +478,6 @@ func doLoginStep(ce *Event, login bridgev2.LoginProcess, step *bridgev2.LoginSte Override: override, }).prompt(ce) case bridgev2.LoginStepTypeUserInput: - err := sendUserInputAttachments(ce, step.UserInputParams.Attachments) - if err != nil { - ce.Reply("Failed to send attachments: %v", err) - } (&userInputLoginCommandState{ Login: login.(bridgev2.LoginProcessUserInput), RemainingFields: step.UserInputParams.Fields, diff --git a/bridgev2/database/ghost.go b/bridgev2/database/ghost.go index 16af35ca..c32929ad 100644 --- a/bridgev2/database/ghost.go +++ b/bridgev2/database/ghost.go @@ -7,17 +7,12 @@ package database import ( - "bytes" "context" "encoding/hex" - "encoding/json" - "fmt" "go.mau.fi/util/dbutil" - "go.mau.fi/util/exerrors" "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/id" ) @@ -27,55 +22,6 @@ type GhostQuery struct { *dbutil.QueryHelper[*Ghost] } -type ExtraProfile map[string]json.RawMessage - -func (ep *ExtraProfile) Set(key string, value any) error { - if key == "displayname" || key == "avatar_url" { - return fmt.Errorf("cannot set reserved profile key %q", key) - } - marshaled, err := json.Marshal(value) - if err != nil { - return err - } - if *ep == nil { - *ep = make(ExtraProfile) - } - (*ep)[key] = canonicaljson.CanonicalJSONAssumeValid(marshaled) - return nil -} - -func (ep *ExtraProfile) With(key string, value any) *ExtraProfile { - exerrors.PanicIfNotNil(ep.Set(key, value)) - return ep -} - -func canonicalizeIfObject(data json.RawMessage) json.RawMessage { - if len(data) > 0 && (data[0] == '{' || data[0] == '[') { - return canonicaljson.CanonicalJSONAssumeValid(data) - } - return data -} - -func (ep *ExtraProfile) CopyTo(dest *ExtraProfile) (changed bool) { - if len(*ep) == 0 { - return - } - if *dest == nil { - *dest = make(ExtraProfile) - } - for key, val := range *ep { - if key == "displayname" || key == "avatar_url" { - continue - } - existing, exists := (*dest)[key] - if !exists || !bytes.Equal(canonicalizeIfObject(existing), val) { - (*dest)[key] = val - changed = true - } - } - return -} - type Ghost struct { BridgeID networkid.BridgeID ID networkid.UserID @@ -89,14 +35,13 @@ type Ghost struct { ContactInfoSet bool IsBot bool Identifiers []string - ExtraProfile ExtraProfile Metadata any } const ( getGhostBaseQuery = ` SELECT bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc, - name_set, avatar_set, contact_info_set, is_bot, identifiers, extra_profile, metadata + name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata FROM ghost ` getGhostByIDQuery = getGhostBaseQuery + `WHERE bridge_id=$1 AND id=$2` @@ -104,14 +49,13 @@ const ( insertGhostQuery = ` INSERT INTO ghost ( bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc, - name_set, avatar_set, contact_info_set, is_bot, identifiers, extra_profile, metadata + name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata ) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) ` updateGhostQuery = ` UPDATE ghost SET name=$3, avatar_id=$4, avatar_hash=$5, avatar_mxc=$6, - name_set=$7, avatar_set=$8, contact_info_set=$9, is_bot=$10, - identifiers=$11, extra_profile=$12, metadata=$13 + name_set=$7, avatar_set=$8, contact_info_set=$9, is_bot=$10, identifiers=$11, metadata=$12 WHERE bridge_id=$1 AND id=$2 ` ) @@ -142,7 +86,7 @@ func (g *Ghost) Scan(row dbutil.Scannable) (*Ghost, error) { &g.BridgeID, &g.ID, &g.Name, &g.AvatarID, &avatarHash, &g.AvatarMXC, &g.NameSet, &g.AvatarSet, &g.ContactInfoSet, &g.IsBot, - dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: &g.ExtraProfile}, dbutil.JSON{Data: g.Metadata}, + dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: g.Metadata}, ) if err != nil { return nil, err @@ -172,6 +116,6 @@ func (g *Ghost) sqlVariables() []any { g.BridgeID, g.ID, g.Name, g.AvatarID, avatarHash, g.AvatarMXC, g.NameSet, g.AvatarSet, g.ContactInfoSet, g.IsBot, - dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: g.ExtraProfile}, dbutil.JSON{Data: g.Metadata}, + dbutil.JSON{Data: &g.Identifiers}, dbutil.JSON{Data: g.Metadata}, } } diff --git a/bridgev2/database/message.go b/bridgev2/database/message.go index 4fd599a8..43f33666 100644 --- a/bridgev2/database/message.go +++ b/bridgev2/database/message.go @@ -68,8 +68,8 @@ const ( getFirstMessagePartByIDQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND (room_receiver=$2 OR room_receiver='') AND id=$3 ORDER BY part_id ASC LIMIT 1` getMessagesBetweenTimeQuery = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND timestamp>$4 AND timestamp<=$5` getOldestMessageInPortal = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 ORDER BY timestamp ASC, part_id ASC LIMIT 1` - getFirstMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY thread_root_id NULLS FIRST, timestamp ASC, part_id ASC LIMIT 1` - getLastMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY thread_root_id NULLS LAST, timestamp DESC, part_id DESC LIMIT 1` + getFirstMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY timestamp ASC, part_id ASC LIMIT 1` + getLastMessageInThread = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 AND (id=$4 OR thread_root_id=$4) ORDER BY timestamp DESC, part_id DESC LIMIT 1` getLastNInPortal = getMessageBaseQuery + `WHERE bridge_id=$1 AND room_id=$2 AND room_receiver=$3 ORDER BY timestamp DESC, part_id DESC LIMIT $4` getLastMessagePartAtOrBeforeTimeQuery = getMessageBaseQuery + `WHERE bridge_id = $1 AND room_id=$2 AND room_receiver=$3 AND timestamp<=$4 ORDER BY timestamp DESC, part_id DESC LIMIT 1` diff --git a/bridgev2/database/upgrades/00-latest.sql b/bridgev2/database/upgrades/00-latest.sql index 6092dc24..b193d314 100644 --- a/bridgev2/database/upgrades/00-latest.sql +++ b/bridgev2/database/upgrades/00-latest.sql @@ -1,4 +1,4 @@ --- v0 -> v27 (compatible with v9+): Latest revision +-- v0 -> v26 (compatible with v9+): Latest revision CREATE TABLE "user" ( bridge_id TEXT NOT NULL, mxid TEXT NOT NULL, @@ -80,7 +80,6 @@ CREATE TABLE ghost ( contact_info_set BOOLEAN NOT NULL, is_bot BOOLEAN NOT NULL, identifiers jsonb NOT NULL, - extra_profile jsonb, metadata jsonb NOT NULL, PRIMARY KEY (bridge_id, id) diff --git a/bridgev2/database/upgrades/27-ghost-extra-profile.sql b/bridgev2/database/upgrades/27-ghost-extra-profile.sql deleted file mode 100644 index e8e0549a..00000000 --- a/bridgev2/database/upgrades/27-ghost-extra-profile.sql +++ /dev/null @@ -1,2 +0,0 @@ --- v27 (compatible with v9+): Add column for extra ghost profile metadata -ALTER TABLE ghost ADD COLUMN extra_profile jsonb; diff --git a/bridgev2/database/userlogin.go b/bridgev2/database/userlogin.go index 00ff01c9..9fa6569a 100644 --- a/bridgev2/database/userlogin.go +++ b/bridgev2/database/userlogin.go @@ -116,7 +116,7 @@ func (u *UserLogin) ensureHasMetadata(metaType MetaTypeCreator) *UserLogin { func (u *UserLogin) sqlVariables() []any { var remoteProfile dbutil.JSON - if !u.RemoteProfile.IsZero() { + if !u.RemoteProfile.IsEmpty() { remoteProfile.Data = &u.RemoteProfile } return []any{u.BridgeID, u.UserMXID, u.ID, u.RemoteName, remoteProfile, dbutil.StrPtr(u.SpaceRoom), dbutil.JSON{Data: u.Metadata}} diff --git a/bridgev2/errors.go b/bridgev2/errors.go index f6677d2e..514dc238 100644 --- a/bridgev2/errors.go +++ b/bridgev2/errors.go @@ -75,7 +75,6 @@ var ( ErrMediaConvertFailed error = WrapErrorInStatus(errors.New("failed to convert media")).WithMessage("failed to convert media").WithIsCertain(true).WithSendNotice(true) ErrMembershipNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group membership")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported) ErrDeleteChatNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support deleting chats")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported) - ErrBeeperAIStreamNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support Beeper AI stream events")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported) ErrPowerLevelsNotSupported error = WrapErrorInStatus(errors.New("this bridge does not support changing group power levels")).WithIsCertain(true).WithErrorAsMessage().WithSendNotice(false).WithErrorReason(event.MessageStatusUnsupported) ErrRemoteEchoTimeout = WrapErrorInStatus(errors.New("remote echo timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld) ErrRemoteAckTimeout = WrapErrorInStatus(errors.New("remote ack timed out")).WithIsCertain(false).WithSendNotice(true).WithErrorReason(event.MessageStatusTooOld) diff --git a/bridgev2/ghost.go b/bridgev2/ghost.go index 590dd1dc..6cef6f06 100644 --- a/bridgev2/ghost.go +++ b/bridgev2/ghost.go @@ -9,15 +9,12 @@ package bridgev2 import ( "context" "crypto/sha256" - "encoding/json" "fmt" - "maps" "net/http" - "slices" "github.com/rs/zerolog" - "go.mau.fi/util/exerrors" "go.mau.fi/util/exmime" + "golang.org/x/exp/slices" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" @@ -137,11 +134,10 @@ func (a *Avatar) Reupload(ctx context.Context, intent MatrixAPI, currentHash [32 } type UserInfo struct { - Identifiers []string - Name *string - Avatar *Avatar - IsBot *bool - ExtraProfile database.ExtraProfile + Identifiers []string + Name *string + Avatar *Avatar + IsBot *bool ExtraUpdates ExtraUpdater[*Ghost] } @@ -189,9 +185,9 @@ func (ghost *Ghost) UpdateAvatar(ctx context.Context, avatar *Avatar) bool { return true } -func (ghost *Ghost) getExtraProfileMeta() any { +func (ghost *Ghost) getExtraProfileMeta() *event.BeeperProfileExtra { bridgeName := ghost.Bridge.Network.GetName() - baseExtra := &event.BeeperProfileExtra{ + return &event.BeeperProfileExtra{ RemoteID: string(ghost.ID), Identifiers: ghost.Identifiers, Service: bridgeName.BeeperBridgeType, @@ -199,35 +195,23 @@ func (ghost *Ghost) getExtraProfileMeta() any { IsBridgeBot: false, IsNetworkBot: ghost.IsBot, } - if len(ghost.ExtraProfile) == 0 { - return baseExtra - } - mergedExtra := maps.Clone(ghost.ExtraProfile) - baseExtraMarshaled := exerrors.Must(json.Marshal(baseExtra)) - exerrors.PanicIfNotNil(json.Unmarshal(baseExtraMarshaled, &mergedExtra)) - return mergedExtra } -func (ghost *Ghost) UpdateContactInfo(ctx context.Context, identifiers []string, isBot *bool, extraProfile database.ExtraProfile) bool { - if !ghost.Bridge.Matrix.GetCapabilities().ExtraProfileMeta { - ghost.ContactInfoSet = false - return false - } +func (ghost *Ghost) UpdateContactInfo(ctx context.Context, identifiers []string, isBot *bool) bool { if identifiers != nil { slices.Sort(identifiers) } - changed := extraProfile.CopyTo(&ghost.ExtraProfile) + if ghost.ContactInfoSet && + (identifiers == nil || slices.Equal(identifiers, ghost.Identifiers)) && + (isBot == nil || *isBot == ghost.IsBot) { + return false + } if identifiers != nil { - changed = changed || !slices.Equal(identifiers, ghost.Identifiers) ghost.Identifiers = identifiers } if isBot != nil { - changed = changed || *isBot != ghost.IsBot ghost.IsBot = *isBot } - if ghost.ContactInfoSet && !changed { - return false - } err := ghost.Intent.SetExtraProfileMeta(ctx, ghost.getExtraProfileMeta()) if err != nil { zerolog.Ctx(ctx).Err(err).Msg("Failed to set extra profile metadata") @@ -250,7 +234,7 @@ func (br *Bridge) allowAggressiveUpdateForType(evtType RemoteEventType) bool { } func (ghost *Ghost) UpdateInfoIfNecessary(ctx context.Context, source *UserLogin, evtType RemoteEventType) { - if ghost.Name != "" && ghost.NameSet && ghost.AvatarSet && !ghost.Bridge.allowAggressiveUpdateForType(evtType) { + if ghost.Name != "" && ghost.NameSet && !ghost.Bridge.allowAggressiveUpdateForType(evtType) { return } info, err := source.Client.GetUserInfo(ctx, ghost) @@ -260,16 +244,12 @@ func (ghost *Ghost) UpdateInfoIfNecessary(ctx context.Context, source *UserLogin zerolog.Ctx(ctx).Debug(). Bool("has_name", ghost.Name != ""). Bool("name_set", ghost.NameSet). - Bool("has_avatar", ghost.AvatarMXC != ""). - Bool("avatar_set", ghost.AvatarSet). Msg("Updating ghost info in IfNecessary call") ghost.UpdateInfo(ctx, info) } else { zerolog.Ctx(ctx).Trace(). Bool("has_name", ghost.Name != ""). Bool("name_set", ghost.NameSet). - Bool("has_avatar", ghost.AvatarMXC != ""). - Bool("avatar_set", ghost.AvatarSet). Msg("No ghost info received in IfNecessary call") } } @@ -297,14 +277,9 @@ func (ghost *Ghost) UpdateInfo(ctx context.Context, info *UserInfo) { } if info.Avatar != nil { update = ghost.UpdateAvatar(ctx, info.Avatar) || update - } else if oldAvatar == "" && !ghost.AvatarSet { - // Special case: nil avatar means we're not expecting one ever, if we don't currently have - // one we flag it as set to avoid constantly refetching in UpdateInfoIfNecessary. - ghost.AvatarSet = true - update = true } - if info.Identifiers != nil || info.IsBot != nil || info.ExtraProfile != nil { - update = ghost.UpdateContactInfo(ctx, info.Identifiers, info.IsBot, info.ExtraProfile) || update + if info.Identifiers != nil || info.IsBot != nil { + update = ghost.UpdateContactInfo(ctx, info.Identifiers, info.IsBot) || update } if info.ExtraUpdates != nil { update = info.ExtraUpdates(ctx, ghost) || update diff --git a/bridgev2/login.go b/bridgev2/login.go index b8321719..4ddbf13e 100644 --- a/bridgev2/login.go +++ b/bridgev2/login.go @@ -13,7 +13,6 @@ import ( "strings" "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/event" ) // LoginProcess represents a single occurrence of a user logging into the remote network. @@ -180,7 +179,6 @@ const ( LoginInputFieldTypeURL LoginInputFieldType = "url" LoginInputFieldTypeDomain LoginInputFieldType = "domain" LoginInputFieldTypeSelect LoginInputFieldType = "select" - LoginInputFieldTypeCaptchaCode LoginInputFieldType = "captcha_code" ) type LoginInputDataField struct { @@ -273,23 +271,6 @@ func (f *LoginInputDataField) FillDefaultValidate() { type LoginUserInputParams struct { // The fields that the user needs to fill in. Fields []LoginInputDataField `json:"fields"` - - // Attachments to display alongside the input fields. - Attachments []*LoginUserInputAttachment `json:"attachments"` -} - -type LoginUserInputAttachment struct { - Type event.MessageType `json:"type,omitempty"` - FileName string `json:"filename,omitempty"` - Content []byte `json:"content,omitempty"` - Info LoginUserInputAttachmentInfo `json:"info,omitempty"` -} - -type LoginUserInputAttachmentInfo struct { - MimeType string `json:"mimetype,omitempty"` - Width int `json:"w,omitempty"` - Height int `json:"h,omitempty"` - Size int `json:"size,omitempty"` } type LoginCompleteParams struct { diff --git a/bridgev2/matrix/connector.go b/bridgev2/matrix/connector.go index 5a2df953..aed6d3bd 100644 --- a/bridgev2/matrix/connector.go +++ b/bridgev2/matrix/connector.go @@ -144,7 +144,6 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) { br.EventProcessor.On(event.EventReaction, br.handleRoomEvent) br.EventProcessor.On(event.EventRedaction, br.handleRoomEvent) br.EventProcessor.On(event.EventEncrypted, br.handleEncryptedEvent) - br.EventProcessor.On(event.EphemeralEventEncrypted, br.handleEncryptedEvent) br.EventProcessor.On(event.StateMember, br.handleRoomEvent) br.EventProcessor.On(event.StatePowerLevels, br.handleRoomEvent) br.EventProcessor.On(event.StateRoomName, br.handleRoomEvent) @@ -157,7 +156,6 @@ func (br *Connector) Init(bridge *bridgev2.Bridge) { br.EventProcessor.On(event.BeeperAcceptMessageRequest, br.handleRoomEvent) br.EventProcessor.On(event.EphemeralEventReceipt, br.handleEphemeralEvent) br.EventProcessor.On(event.EphemeralEventTyping, br.handleEphemeralEvent) - br.EventProcessor.On(event.BeeperEphemeralEventAIStream, br.handleEphemeralEvent) br.Bot = br.AS.BotIntent() br.Crypto = NewCryptoHelper(br) br.Bridge.Commands.(*commands.Processor).AddHandlers( @@ -369,8 +367,6 @@ func (br *Connector) ensureConnection(ctx context.Context) { br.Capabilities.AutoJoinInvites = br.SpecVersions.Supports(mautrix.BeeperFeatureAutojoinInvites) br.Capabilities.BatchSending = br.SpecVersions.Supports(mautrix.BeeperFeatureBatchSending) br.Capabilities.ArbitraryMemberChange = br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryMemberChange) - br.Capabilities.ExtraProfileMeta = br.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) || - (br.SpecVersions.Supports(mautrix.FeatureArbitraryProfileFields) && br.Config.Matrix.GhostExtraProfileInfo) break } } diff --git a/bridgev2/matrix/intent.go b/bridgev2/matrix/intent.go index f7254bd4..3d2692f9 100644 --- a/bridgev2/matrix/intent.go +++ b/bridgev2/matrix/intent.go @@ -9,7 +9,6 @@ package matrix import ( "bytes" "context" - "encoding/json" "errors" "fmt" "io" @@ -28,7 +27,6 @@ import ( "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/bridgeconfig" "maunium.net/go/mautrix/crypto/attachment" - "maunium.net/go/mautrix/crypto/canonicaljson" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "maunium.net/go/mautrix/pushrules" @@ -45,7 +43,6 @@ type ASIntent struct { var _ bridgev2.MatrixAPI = (*ASIntent)(nil) var _ bridgev2.MarkAsDMMatrixAPI = (*ASIntent)(nil) -var _ bridgev2.EphemeralSendingMatrixAPI = (*ASIntent)(nil) func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, extra *bridgev2.MatrixSendExtra) (*mautrix.RespSendEvent, error) { if extra == nil { @@ -87,21 +84,6 @@ func (as *ASIntent) SendMessage(ctx context.Context, roomID id.RoomID, eventType return as.Matrix.SendMessageEvent(ctx, roomID, eventType, content, mautrix.ReqSendEvent{Timestamp: extra.Timestamp.UnixMilli()}) } -func (as *ASIntent) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, txnID string) (*mautrix.RespSendEvent, error) { - if !as.Connector.SpecVersions.Supports(mautrix.BeeperFeatureEphemeralEvents) { - return nil, mautrix.MUnrecognized.WithMessage("Homeserver does not advertise com.beeper.ephemeral support") - } - if encrypted, err := as.Matrix.StateStore.IsEncrypted(ctx, roomID); err != nil { - return nil, fmt.Errorf("failed to check if room is encrypted: %w", err) - } else if encrypted && as.Connector.Crypto != nil { - if err = as.Connector.Crypto.Encrypt(ctx, roomID, eventType, content); err != nil { - return nil, err - } - eventType = event.EventEncrypted - } - return as.Matrix.BeeperSendEphemeralEvent(ctx, roomID, eventType, content, mautrix.ReqSendEvent{TransactionID: txnID}) -} - func (as *ASIntent) fillMemberEvent(ctx context.Context, roomID id.RoomID, userID id.UserID, content *event.Content) { targetContent, ok := content.Parsed.(*event.MemberEventContent) if !ok || targetContent.Displayname != "" || targetContent.AvatarURL != "" { @@ -421,7 +403,6 @@ func (as *ASIntent) UploadMediaStream( removeAndClose(replFile) removeAndClose(tempFile) } - req.AsyncContext = zerolog.Ctx(ctx).WithContext(as.Connector.Bridge.BackgroundCtx) startedAsyncUpload = true var resp *mautrix.RespCreateMXC resp, err = as.Matrix.UploadAsync(ctx, req) @@ -454,7 +435,6 @@ func (as *ASIntent) doUploadReq(ctx context.Context, file *event.EncryptedFileIn as.Connector.uploadSema.Release(int64(len(req.ContentBytes))) } } - req.AsyncContext = zerolog.Ctx(ctx).WithContext(as.Connector.Bridge.BackgroundCtx) var resp *mautrix.RespCreateMXC resp, err = as.Matrix.UploadAsync(ctx, req) if resp != nil { @@ -486,62 +466,11 @@ func (as *ASIntent) SetAvatarURL(ctx context.Context, avatarURL id.ContentURIStr return as.Matrix.SetAvatarURL(ctx, parsedAvatarURL) } -func dataToFields(data any) (map[string]json.RawMessage, error) { - fields, ok := data.(map[string]json.RawMessage) - if ok { - return fields, nil - } - d, err := json.Marshal(data) - if err != nil { - return nil, err - } - d = canonicaljson.CanonicalJSONAssumeValid(d) - err = json.Unmarshal(d, &fields) - return fields, err -} - -func marshalField(val any) json.RawMessage { - data, _ := json.Marshal(val) - if len(data) > 0 && (data[0] == '{' || data[0] == '[') { - return canonicaljson.CanonicalJSONAssumeValid(data) - } - return data -} - -var nullJSON = json.RawMessage("null") - func (as *ASIntent) SetExtraProfileMeta(ctx context.Context, data any) error { - if as.Connector.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) { - return as.Matrix.BeeperUpdateProfile(ctx, data) - } else if as.Connector.SpecVersions.Supports(mautrix.FeatureArbitraryProfileFields) && as.Connector.Config.Matrix.GhostExtraProfileInfo { - fields, err := dataToFields(data) - if err != nil { - return fmt.Errorf("failed to marshal fields: %w", err) - } - currentProfile, err := as.Matrix.GetProfile(ctx, as.Matrix.UserID) - if err != nil { - return fmt.Errorf("failed to get current profile: %w", err) - } - for key, val := range fields { - existing, ok := currentProfile.Extra[key] - if !ok { - if bytes.Equal(val, nullJSON) { - continue - } - err = as.Matrix.SetProfileField(ctx, key, val) - } else if !bytes.Equal(marshalField(existing), val) { - if bytes.Equal(val, nullJSON) { - err = as.Matrix.DeleteProfileField(ctx, key) - } else { - err = as.Matrix.SetProfileField(ctx, key, val) - } - } - if err != nil { - return fmt.Errorf("failed to set profile field %q: %w", key, err) - } - } + if !as.Connector.SpecVersions.Supports(mautrix.BeeperFeatureArbitraryProfileMeta) { + return nil } - return nil + return as.Matrix.BeeperUpdateProfile(ctx, data) } func (as *ASIntent) GetMXID() id.UserID { diff --git a/bridgev2/matrix/matrix.go b/bridgev2/matrix/matrix.go index 954d0ad9..570ae5f1 100644 --- a/bridgev2/matrix/matrix.go +++ b/bridgev2/matrix/matrix.go @@ -68,10 +68,6 @@ func (br *Connector) handleEphemeralEvent(ctx context.Context, evt *event.Event) case event.EphemeralEventTyping: typingContent := evt.Content.AsTyping() typingContent.UserIDs = slices.DeleteFunc(typingContent.UserIDs, br.shouldIgnoreEventFromUser) - case event.BeeperEphemeralEventAIStream: - if br.shouldIgnoreEvent(evt) { - return - } } br.Bridge.QueueMatrixEvent(ctx, evt) } @@ -235,6 +231,7 @@ func (br *Connector) postDecrypt(ctx context.Context, original, decrypted *event go br.sendSuccessCheckpoint(ctx, decrypted, status.MsgStepDecrypted, retryCount) decrypted.Mautrix.CheckpointSent = true decrypted.Mautrix.DecryptionDuration = duration + decrypted.Mautrix.EventSource |= event.SourceDecrypted br.EventProcessor.Dispatch(ctx, decrypted) if errorEventID != nil && *errorEventID != "" { _, _ = br.Bot.RedactEvent(ctx, decrypted.RoomID, *errorEventID) diff --git a/bridgev2/matrix/mxmain/example-config.yaml b/bridgev2/matrix/mxmain/example-config.yaml index ccc81c4b..b0e83696 100644 --- a/bridgev2/matrix/mxmain/example-config.yaml +++ b/bridgev2/matrix/mxmain/example-config.yaml @@ -29,9 +29,6 @@ bridge: # How long after an unknown error should the bridge attempt a full reconnect? # Must be at least 1 minute. The bridge will add an extra ±20% jitter to this value. unknown_error_auto_reconnect: null - # Maximum number of times to do the auto-reconnect above. - # The counter is per login, but is never reset except on logout and restart. - unknown_error_max_auto_reconnects: 10 # Should leaving Matrix rooms be bridged as leaving groups on the remote network? bridge_matrix_leave: false @@ -244,9 +241,6 @@ matrix: # The threshold as bytes after which the bridge should roundtrip uploads via the disk # rather than keeping the whole file in memory. upload_file_threshold: 5242880 - # Should the bridge set additional custom profile info for ghosts? - # This can make a lot of requests, as there's no batch profile update endpoint. - ghost_extra_profile_info: false # Segment-compatible analytics endpoint for tracking some events, like provisioning API login and encryption errors. analytics: diff --git a/bridgev2/matrix/provisioning.go b/bridgev2/matrix/provisioning.go index 243b91da..e3d3a0b4 100644 --- a/bridgev2/matrix/provisioning.go +++ b/bridgev2/matrix/provisioning.go @@ -324,7 +324,7 @@ func (prov *ProvisioningAPI) GetWhoami(w http.ResponseWriter, r *http.Request) { prevState.UserID = "" prevState.RemoteID = "" prevState.RemoteName = "" - prevState.RemoteProfile = status.RemoteProfile{} + prevState.RemoteProfile = nil resp.Logins[i] = RespWhoamiLogin{ StateEvent: prevState.StateEvent, StateTS: prevState.Timestamp, @@ -403,18 +403,10 @@ func (prov *ProvisioningAPI) PostLoginStart(w http.ResponseWriter, r *http.Reque Override: overrideLogin, } prov.loginsLock.Unlock() - zerolog.Ctx(r.Context()).Info(). - Any("first_step", firstStep). - Msg("Created login process") exhttp.WriteJSONResponse(w, http.StatusOK, &RespSubmitLogin{LoginID: loginID, LoginStep: firstStep}) } func (prov *ProvisioningAPI) handleCompleteStep(ctx context.Context, login *ProvLogin, step *bridgev2.LoginStep) { - zerolog.Ctx(ctx).Info(). - Str("step_id", step.StepID). - Str("user_login_id", string(step.CompleteParams.UserLoginID)). - Msg("Login completed successfully") - prov.deleteLogin(login, false) if login.Override == nil || login.Override.ID == step.CompleteParams.UserLoginID { return } @@ -428,15 +420,6 @@ func (prov *ProvisioningAPI) handleCompleteStep(ctx context.Context, login *Prov }, bridgev2.DeleteOpts{LogoutRemote: true}) } -func (prov *ProvisioningAPI) deleteLogin(login *ProvLogin, cancel bool) { - if cancel { - login.Process.Cancel() - } - prov.loginsLock.Lock() - delete(prov.logins, login.ID) - prov.loginsLock.Unlock() -} - func (prov *ProvisioningAPI) PostLoginStep(w http.ResponseWriter, r *http.Request) { loginID := r.PathValue("loginProcessID") prov.loginsLock.RLock() @@ -507,14 +490,11 @@ func (prov *ProvisioningAPI) PostLoginSubmitInput(w http.ResponseWriter, r *http if err != nil { zerolog.Ctx(r.Context()).Err(err).Msg("Failed to submit input") RespondWithError(w, err, "Internal error submitting input") - prov.deleteLogin(login, true) return } login.NextStep = nextStep if nextStep.Type == bridgev2.LoginStepTypeComplete { prov.handleCompleteStep(r.Context(), login, nextStep) - } else { - zerolog.Ctx(r.Context()).Debug().Any("next_step", nextStep).Msg("Returning next login step") } exhttp.WriteJSONResponse(w, http.StatusOK, &RespSubmitLogin{LoginID: login.ID, LoginStep: nextStep}) } @@ -528,14 +508,11 @@ func (prov *ProvisioningAPI) PostLoginWait(w http.ResponseWriter, r *http.Reques if err != nil { zerolog.Ctx(r.Context()).Err(err).Msg("Failed to wait") RespondWithError(w, err, "Internal error waiting for login") - prov.deleteLogin(login, true) return } login.NextStep = nextStep if nextStep.Type == bridgev2.LoginStepTypeComplete { prov.handleCompleteStep(r.Context(), login, nextStep) - } else { - zerolog.Ctx(r.Context()).Debug().Any("next_step", nextStep).Msg("Returning next login step") } exhttp.WriteJSONResponse(w, http.StatusOK, &RespSubmitLogin{LoginID: login.ID, LoginStep: nextStep}) } diff --git a/bridgev2/matrix/provisioning.yaml b/bridgev2/matrix/provisioning.yaml index 26068db4..d19a7e83 100644 --- a/bridgev2/matrix/provisioning.yaml +++ b/bridgev2/matrix/provisioning.yaml @@ -740,41 +740,6 @@ components: description: For fields of type select, the valid options. items: type: string - attachments: - type: array - description: A list of media attachments to show the user alongside the form fields. - items: - type: object - description: A media attachment to show the user. - required: [ type, filename, content ] - properties: - type: - type: string - description: The type of media attachment, using the same media type identifiers as Matrix attachments. Only some are supported. - enum: [ m.image, m.audio ] - filename: - type: string - description: The filename for the media attachment. - content: - type: string - description: The raw file content for the attachment encoded in base64. - info: - type: object - description: Optional but recommended metadata for the attachment. Can generally be derived from the raw content if omitted. - properties: - mimetype: - type: string - description: The MIME type for the media content. - examples: [ image/png, audio/mpeg ] - w: - type: number - description: The width of the media in pixels. Only applicable for images and videos. - h: - type: number - description: The height of the media in pixels. Only applicable for images and videos. - size: - type: number - description: The size of the media content in number of bytes. Strongly recommended to include. - description: Cookie login step required: [ type, cookies ] properties: diff --git a/bridgev2/matrixinterface.go b/bridgev2/matrixinterface.go index be26db49..57f786bb 100644 --- a/bridgev2/matrixinterface.go +++ b/bridgev2/matrixinterface.go @@ -28,7 +28,6 @@ type MatrixCapabilities struct { AutoJoinInvites bool BatchSending bool ArbitraryMemberChange bool - ExtraProfileMeta bool } type MatrixConnector interface { @@ -218,8 +217,3 @@ type MarkAsDMMatrixAPI interface { MatrixAPI MarkAsDM(ctx context.Context, roomID id.RoomID, otherUser id.UserID) error } - -type EphemeralSendingMatrixAPI interface { - MatrixAPI - BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, content *event.Content, txnID string) (*mautrix.RespSendEvent, error) -} diff --git a/bridgev2/networkinterface.go b/bridgev2/networkinterface.go index b706aedb..0e9a8543 100644 --- a/bridgev2/networkinterface.go +++ b/bridgev2/networkinterface.go @@ -726,11 +726,6 @@ type MessageRequestAcceptingNetworkAPI interface { HandleMatrixAcceptMessageRequest(ctx context.Context, msg *MatrixAcceptMessageRequest) error } -type BeeperAIStreamHandlingNetworkAPI interface { - NetworkAPI - HandleMatrixBeeperAIStream(ctx context.Context, msg *MatrixBeeperAIStream) error -} - type ResolveIdentifierResponse struct { // Ghost is the ghost of the user that the identifier resolves to. // This field should be set whenever possible. However, it is not required, @@ -1115,11 +1110,6 @@ type RemoteEvent interface { GetSender() EventSender } -type RemoteEventWithContextMutation interface { - RemoteEvent - MutateContext(ctx context.Context) context.Context -} - type RemoteEventWithUncertainPortalReceiver interface { RemoteEvent PortalReceiverIsUncertain() bool @@ -1449,7 +1439,6 @@ type MatrixViewingChat struct { type MatrixDeleteChat = MatrixEventBase[*event.BeeperChatDeleteEventContent] type MatrixAcceptMessageRequest = MatrixEventBase[*event.BeeperAcceptMessageRequestEventContent] -type MatrixBeeperAIStream = MatrixEventBase[*event.BeeperAIStreamEventContent] type MatrixMarkedUnread = MatrixRoomMeta[*event.MarkedUnreadEventContent] type MatrixMute = MatrixRoomMeta[*event.BeeperMuteEventContent] type MatrixRoomTag = MatrixRoomMeta[*event.TagEventContent] diff --git a/bridgev2/portal.go b/bridgev2/portal.go index 5ba29507..e9feb448 100644 --- a/bridgev2/portal.go +++ b/bridgev2/portal.go @@ -169,9 +169,7 @@ func (br *Bridge) loadPortal(ctx context.Context, dbPortal *database.Portal, que } func (portal *Portal) updateLogger() { - logWith := portal.Bridge.Log.With(). - Str("portal_id", string(portal.ID)). - Str("portal_receiver", string(portal.Receiver)) + logWith := portal.Bridge.Log.With().Str("portal_id", string(portal.ID)) if portal.MXID != "" { logWith = logWith.Stringer("portal_mxid", portal.MXID) } @@ -448,23 +446,6 @@ func (portal *Portal) handleSingleEventWithDelayLogging(idx int, rawEvt any) (ou return } -type contextKey int - -const ( - contextKeyRemoteEvent contextKey = iota - contextKeyMatrixEvent -) - -func GetMatrixEventFromContext(ctx context.Context) (evt *event.Event) { - evt, _ = ctx.Value(contextKeyMatrixEvent).(*event.Event) - return -} - -func GetRemoteEventFromContext(ctx context.Context) (evt RemoteEvent) { - evt, _ = ctx.Value(contextKeyRemoteEvent).(RemoteEvent) - return -} - func (portal *Portal) getEventCtxWithLog(rawEvt any, idx int) context.Context { var logWith zerolog.Context switch evt := rawEvt.(type) { @@ -478,10 +459,6 @@ func (portal *Portal) getEventCtxWithLog(rawEvt any, idx int) context.Context { Stringer("event_id", evt.evt.ID). Stringer("sender", evt.sender.MXID) } - ctx := portal.Bridge.BackgroundCtx - ctx = context.WithValue(ctx, contextKeyMatrixEvent, evt.evt) - ctx = logWith.Logger().WithContext(ctx) - return ctx case *portalRemoteEvent: evt.evtType = evt.evt.GetType() logWith = portal.Log.With().Int("event_loop_index", idx). @@ -507,23 +484,10 @@ func (portal *Portal) getEventCtxWithLog(rawEvt any, idx int) context.Context { logWith = logWith.Int64("remote_stream_order", remoteStreamOrder) } } - if remoteMsg, ok := evt.evt.(RemoteEventWithTimestamp); ok { - if remoteTimestamp := remoteMsg.GetTimestamp(); !remoteTimestamp.IsZero() { - logWith = logWith.Time("remote_timestamp", remoteTimestamp) - } - } - ctx := portal.Bridge.BackgroundCtx - ctx = context.WithValue(ctx, contextKeyRemoteEvent, evt.evt) - ctx = logWith.Logger().WithContext(ctx) - if ctxMut, ok := evt.evt.(RemoteEventWithContextMutation); ok { - ctx = ctxMut.MutateContext(ctx) - } - return ctx case *portalCreateEvent: return evt.ctx - default: - panic(fmt.Errorf("invalid type %T in getEventCtxWithLog", evt)) } + return logWith.Logger().WithContext(portal.Bridge.BackgroundCtx) } func (portal *Portal) handleSingleEvent(ctx context.Context, rawEvt any, doneCallback func(res EventHandlingResult)) { @@ -728,8 +692,6 @@ func (portal *Portal) handleMatrixEvent(ctx context.Context, sender *User, evt * return portal.handleMatrixReceipts(ctx, evt) case event.EphemeralEventTyping: return portal.handleMatrixTyping(ctx, evt) - case event.BeeperEphemeralEventAIStream: - return portal.handleMatrixAIStream(ctx, sender, evt) default: return EventHandlingResultIgnored } @@ -974,50 +936,6 @@ func (portal *Portal) handleMatrixTyping(ctx context.Context, evt *event.Event) return EventHandlingResultSuccess } -func (portal *Portal) handleMatrixAIStream(ctx context.Context, sender *User, evt *event.Event) EventHandlingResult { - log := zerolog.Ctx(ctx) - if sender == nil { - log.Error().Msg("Missing sender for Matrix AI stream event") - return EventHandlingResultIgnored - } - login, _, err := portal.FindPreferredLogin(ctx, sender, true) - if err != nil { - log.Err(err).Msg("Failed to get user login to handle Matrix AI stream event") - return EventHandlingResultFailed.WithMSSError(err) - } - var origSender *OrigSender - if login == nil { - if portal.Relay == nil { - return EventHandlingResultIgnored - } - login = portal.Relay - origSender = &OrigSender{ - User: sender, - UserID: sender.MXID, - } - } - content, ok := evt.Content.Parsed.(*event.BeeperAIStreamEventContent) - if !ok { - log.Error().Type("content_type", evt.Content.Parsed).Msg("Unexpected parsed content type") - return EventHandlingResultFailed.WithMSSError(fmt.Errorf("%w: %T", ErrUnexpectedParsedContentType, evt.Content.Parsed)) - } - api, ok := login.Client.(BeeperAIStreamHandlingNetworkAPI) - if !ok { - return EventHandlingResultIgnored.WithMSSError(ErrBeeperAIStreamNotSupported) - } - err = api.HandleMatrixBeeperAIStream(ctx, &MatrixBeeperAIStream{ - Event: evt, - Content: content, - Portal: portal, - OrigSender: origSender, - }) - if err != nil { - log.Err(err).Msg("Failed to handle Matrix AI stream event") - return EventHandlingResultFailed.WithMSSError(err) - } - return EventHandlingResultSuccess.WithMSS() -} - func (portal *Portal) sendTypings(ctx context.Context, userIDs []id.UserID, typing bool) { for _, userID := range userIDs { login, ok := portal.currentlyTypingLogins[userID] @@ -1305,12 +1223,6 @@ func (portal *Portal) handleMatrixMessage(ctx context.Context, sender *UserLogin } } - err = portal.autoAcceptMessageRequest(ctx, evt, sender, origSender, caps) - if err != nil { - log.Warn().Err(err).Msg("Failed to auto-accept message request on message") - // TODO stop processing? - } - var resp *MatrixMessageResponse if msgContent != nil { resp, err = sender.Client.HandleMatrixMessage(ctx, wrappedMsgEvt) @@ -1590,12 +1502,6 @@ func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *UserLogi log.Warn().Msg("Reaction target message not found in database") return EventHandlingResultFailed.WithMSSError(fmt.Errorf("reaction %w", ErrTargetMessageNotFound)) } - caps := sender.Client.GetCapabilities(ctx, portal) - err = portal.autoAcceptMessageRequest(ctx, evt, sender, nil, caps) - if err != nil { - log.Warn().Err(err).Msg("Failed to auto-accept message request on reaction") - // TODO stop processing? - } log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c.Str("reaction_target_remote_id", string(reactionTarget.ID)) }) @@ -1618,12 +1524,6 @@ func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *UserLogi if portal.Bridge.Config.OutgoingMessageReID { deterministicID = portal.Bridge.Matrix.GenerateReactionEventID(portal.MXID, reactionTarget, preResp.SenderID, preResp.EmojiID) } - defer func() { - // Do this in a defer so that it happens after any potential defer calls to removeOutdatedReaction - if handleRes.Success { - portal.sendSuccessStatus(ctx, evt, 0, deterministicID) - } - }() removeOutdatedReaction := func(oldReact *database.Reaction, deleteDB bool) { if !handleRes.Success { return @@ -1669,10 +1569,6 @@ func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *UserLogi // Keep n-1 previous reactions and remove the rest react.ExistingReactionsToKeep = allReactions[:preResp.MaxReactions-1] for _, oldReaction := range allReactions[preResp.MaxReactions-1:] { - if existing != nil && oldReaction.EmojiID == existing.EmojiID { - // Don't double-delete on networks that only allow one emoji - continue - } // Intentionally defer in a loop, there won't be that many items, // and we want all of them to be done after this function completes successfully //goland:noinspection GoDeferInLoop @@ -1721,6 +1617,7 @@ func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *UserLogi if err != nil { log.Err(err).Msg("Failed to save reaction to database") } + portal.sendSuccessStatus(ctx, evt, 0, deterministicID) return EventHandlingResultSuccess.WithEventID(deterministicID) } @@ -1904,38 +1801,6 @@ func (portal *Portal) handleMatrixAcceptMessageRequest( return EventHandlingResultSuccess.WithMSS() } -func (portal *Portal) autoAcceptMessageRequest( - ctx context.Context, evt *event.Event, sender *UserLogin, origSender *OrigSender, caps *event.RoomFeatures, -) error { - if !portal.MessageRequest || caps.MessageRequest == nil || caps.MessageRequest.AcceptWithMessage == event.CapLevelFullySupported { - return nil - } - mran, ok := sender.Client.(MessageRequestAcceptingNetworkAPI) - if !ok { - return nil - } - err := mran.HandleMatrixAcceptMessageRequest(ctx, &MatrixAcceptMessageRequest{ - Event: evt, - Content: &event.BeeperAcceptMessageRequestEventContent{ - IsImplicit: true, - }, - Portal: portal, - OrigSender: origSender, - }) - if err != nil { - return err - } - if portal.MessageRequest { - portal.MessageRequest = false - portal.UpdateBridgeInfo(ctx) - err = portal.Save(ctx) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to save portal after accepting message request") - } - } - return nil -} - func (portal *Portal) handleMatrixDeleteChat( ctx context.Context, sender *UserLogin, @@ -2792,7 +2657,7 @@ func (portal *Portal) getRelationMeta( log.Err(err).Msg("Failed to get last thread message from database") } if prevThreadEvent == nil { - prevThreadEvent = ptr.Clone(threadRoot) + prevThreadEvent = threadRoot } } return @@ -3730,7 +3595,7 @@ func (portal *Portal) handleRemoteMarkUnread(ctx context.Context, source *UserLo } func (portal *Portal) handleRemoteDeliveryReceipt(ctx context.Context, source *UserLogin, evt RemoteDeliveryReceipt) EventHandlingResult { - if portal.RoomType != database.RoomTypeDM || (evt.GetSender().Sender != portal.OtherUserID && portal.OtherUserID != "") { + if portal.RoomType != database.RoomTypeDM || evt.GetSender().Sender != portal.OtherUserID { return EventHandlingResultIgnored } intent, ok := portal.GetIntentFor(ctx, evt.GetSender(), source, RemoteEventDeliveryReceipt) @@ -5403,9 +5268,6 @@ func (portal *Portal) removeInPortalCache(ctx context.Context) { } func (portal *Portal) unlockedDelete(ctx context.Context) error { - if portal.deleted.IsSet() { - return nil - } err := portal.safeDBDelete(ctx) if err != nil { return err diff --git a/bridgev2/portalreid.go b/bridgev2/portalreid.go index c976d97c..6a5091fc 100644 --- a/bridgev2/portalreid.go +++ b/bridgev2/portalreid.go @@ -38,20 +38,17 @@ func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.Porta Stringer("target_portal_key", target). Logger() ctx = log.WithContext(ctx) + if !br.cacheLock.TryLock() { + log.Debug().Msg("Waiting for cache lock") + br.cacheLock.Lock() + log.Debug().Msg("Acquired cache lock after waiting") + } defer func() { + br.cacheLock.Unlock() log.Debug().Msg("Finished handling portal re-ID") }() - acquireCacheLock := func() { - if !br.cacheLock.TryLock() { - log.Debug().Msg("Waiting for global cache lock") - br.cacheLock.Lock() - log.Debug().Msg("Acquired global cache lock after waiting") - } else { - log.Trace().Msg("Acquired global cache lock without waiting") - } - } log.Debug().Msg("Re-ID'ing portal") - sourcePortal, err := br.GetExistingPortalByKey(ctx, source) + sourcePortal, err := br.UnlockedGetPortalByKey(ctx, source, true) if err != nil { return ReIDResultError, nil, fmt.Errorf("failed to get source portal: %w", err) } else if sourcePortal == nil { @@ -78,24 +75,18 @@ func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.Porta log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c.Stringer("source_portal_mxid", sourcePortal.MXID) }) - - acquireCacheLock() targetPortal, err := br.UnlockedGetPortalByKey(ctx, target, true) if err != nil { - br.cacheLock.Unlock() return ReIDResultError, nil, fmt.Errorf("failed to get target portal: %w", err) } if targetPortal == nil { log.Info().Msg("Target portal doesn't exist, re-ID'ing source portal") err = sourcePortal.unlockedReID(ctx, target) - br.cacheLock.Unlock() if err != nil { return ReIDResultError, nil, fmt.Errorf("failed to re-ID source portal: %w", err) } return ReIDResultSourceReIDd, sourcePortal, nil } - br.cacheLock.Unlock() - if !targetPortal.roomCreateLock.TryLock() { if cancelCreate := targetPortal.cancelRoomCreate.Swap(nil); cancelCreate != nil { (*cancelCreate)() @@ -107,8 +98,6 @@ func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.Porta defer targetPortal.roomCreateLock.Unlock() if targetPortal.MXID == "" { log.Info().Msg("Target portal row exists, but doesn't have a Matrix room. Deleting target portal row and re-ID'ing source portal") - acquireCacheLock() - defer br.cacheLock.Unlock() err = targetPortal.unlockedDelete(ctx) if err != nil { return ReIDResultError, nil, fmt.Errorf("failed to delete target portal: %w", err) @@ -123,9 +112,6 @@ func (br *Bridge) ReIDPortal(ctx context.Context, source, target networkid.Porta return c.Stringer("target_portal_mxid", targetPortal.MXID) }) log.Info().Msg("Both target and source portals have Matrix rooms, tombstoning source portal") - sourcePortal.removeInPortalCache(ctx) - acquireCacheLock() - defer br.cacheLock.Unlock() err = sourcePortal.unlockedDelete(ctx) if err != nil { return ReIDResultError, nil, fmt.Errorf("failed to delete source portal row: %w", err) diff --git a/bridgev2/provisionutil/creategroup.go b/bridgev2/provisionutil/creategroup.go index 72bacaff..fbe0a513 100644 --- a/bridgev2/provisionutil/creategroup.go +++ b/bridgev2/provisionutil/creategroup.go @@ -32,9 +32,6 @@ func CreateGroup(ctx context.Context, login *bridgev2.UserLogin, params *bridgev if !ok { return nil, bridgev2.RespError(mautrix.MUnrecognized.WithMessage("This bridge does not support creating groups")) } - zerolog.Ctx(ctx).Debug(). - Any("create_params", params). - Msg("Creating group chat on remote network") caps := login.Bridge.Network.GetCapabilities() typeSpec, validType := caps.Provisioning.GroupCreation[params.Type] if !validType { @@ -101,9 +98,6 @@ func CreateGroup(ctx context.Context, login *bridgev2.UserLogin, params *bridgev if resp.PortalKey.IsEmpty() { return nil, ErrNoPortalKey } - zerolog.Ctx(ctx).Debug(). - Object("portal_key", resp.PortalKey). - Msg("Successfully created group on remote network") if resp.Portal == nil { resp.Portal, err = login.Bridge.GetPortalByKey(ctx, resp.PortalKey) if err != nil { diff --git a/bridgev2/simplevent/meta.go b/bridgev2/simplevent/meta.go index 96c8a9c5..449a8773 100644 --- a/bridgev2/simplevent/meta.go +++ b/bridgev2/simplevent/meta.go @@ -27,9 +27,8 @@ type EventMeta struct { Timestamp time.Time StreamOrder int64 - PreHandleFunc func(context.Context, *bridgev2.Portal) - PostHandleFunc func(context.Context, *bridgev2.Portal) - MutateContextFunc func(context.Context) context.Context + PreHandleFunc func(context.Context, *bridgev2.Portal) + PostHandleFunc func(context.Context, *bridgev2.Portal) } var ( @@ -40,7 +39,6 @@ var ( _ bridgev2.RemoteEventWithStreamOrder = (*EventMeta)(nil) _ bridgev2.RemotePreHandler = (*EventMeta)(nil) _ bridgev2.RemotePostHandler = (*EventMeta)(nil) - _ bridgev2.RemoteEventWithContextMutation = (*EventMeta)(nil) ) func (evt *EventMeta) AddLogContext(c zerolog.Context) zerolog.Context { @@ -93,13 +91,6 @@ func (evt *EventMeta) PostHandle(ctx context.Context, portal *bridgev2.Portal) { } } -func (evt *EventMeta) MutateContext(ctx context.Context) context.Context { - if evt.MutateContextFunc == nil { - return ctx - } - return evt.MutateContextFunc(ctx) -} - func (evt EventMeta) WithType(t bridgev2.RemoteEventType) EventMeta { evt.Type = t return evt diff --git a/bridgev2/status/bridgestate.go b/bridgev2/status/bridgestate.go index 5925dd4f..430d4c7c 100644 --- a/bridgev2/status/bridgestate.go +++ b/bridgev2/status/bridgestate.go @@ -19,6 +19,7 @@ import ( "github.com/tidwall/sjson" "go.mau.fi/util/jsontime" + "go.mau.fi/util/ptr" "maunium.net/go/mautrix" "maunium.net/go/mautrix/bridgev2/networkid" @@ -111,7 +112,7 @@ func (rp *RemoteProfile) Merge(other RemoteProfile) RemoteProfile { return other } -func (rp *RemoteProfile) IsZero() bool { +func (rp *RemoteProfile) IsEmpty() bool { return rp == nil || (rp.Phone == "" && rp.Email == "" && rp.Username == "" && rp.Name == "" && rp.Avatar == "" && rp.AvatarFile == nil) } @@ -129,7 +130,7 @@ type BridgeState struct { UserID id.UserID `json:"user_id,omitempty"` RemoteID networkid.UserLoginID `json:"remote_id,omitempty"` RemoteName string `json:"remote_name,omitempty"` - RemoteProfile RemoteProfile `json:"remote_profile,omitzero"` + RemoteProfile *RemoteProfile `json:"remote_profile,omitempty"` Reason string `json:"reason,omitempty"` Info map[string]interface{} `json:"info,omitempty"` @@ -209,7 +210,7 @@ func (pong *BridgeState) ShouldDeduplicate(newPong *BridgeState) bool { pong.StateEvent == newPong.StateEvent && pong.RemoteName == newPong.RemoteName && pong.UserAction == newPong.UserAction && - pong.RemoteProfile == newPong.RemoteProfile && + ptr.Val(pong.RemoteProfile) == ptr.Val(newPong.RemoteProfile) && pong.Error == newPong.Error && maps.EqualFunc(pong.Info, newPong.Info, reflect.DeepEqual) && pong.Timestamp.Add(time.Duration(pong.TTL)*time.Second).After(time.Now()) diff --git a/bridgev2/userlogin.go b/bridgev2/userlogin.go index d56dc4cc..c9102248 100644 --- a/bridgev2/userlogin.go +++ b/bridgev2/userlogin.go @@ -51,8 +51,6 @@ func (br *Bridge) loadUserLogin(ctx context.Context, user *User, dbUserLogin *da if err != nil { return nil, fmt.Errorf("failed to get user: %w", err) } - // TODO if loading the user caused the provided userlogin to be loaded, cancel here? - // Currently this will double-load it } userLogin := &UserLogin{ UserLogin: dbUserLogin, @@ -512,7 +510,7 @@ func (ul *UserLogin) FillBridgeState(state status.BridgeState) status.BridgeStat state.UserID = ul.UserMXID state.RemoteID = ul.ID state.RemoteName = ul.RemoteName - state.RemoteProfile = ul.RemoteProfile + state.RemoteProfile = &ul.RemoteProfile filler, ok := ul.Client.(status.BridgeStateFiller) if ok { return filler.FillBridgeState(state) diff --git a/client.go b/client.go index 045d7b8e..87b6d87e 100644 --- a/client.go +++ b/client.go @@ -386,14 +386,7 @@ func (cli *Client) LogRequestDone(req *http.Request, resp *http.Response, err er } } if body := req.Context().Value(LogBodyContextKey); body != nil { - switch typedLogBody := body.(type) { - case json.RawMessage: - evt.RawJSON("req_body", typedLogBody) - case string: - evt.Str("req_body", typedLogBody) - default: - panic(fmt.Errorf("invalid type for LogBodyContextKey: %T", body)) - } + evt.Interface("req_body", body) } if errors.Is(err, context.Canceled) { evt.Msg("Request canceled") @@ -457,10 +450,8 @@ func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, e } if params.SensitiveContent && !logSensitiveContent { logBody = "" - } else if len(jsonStr) > 32768 { - logBody = fmt.Sprintf("", len(jsonStr)) } else { - logBody = json.RawMessage(jsonStr) + logBody = params.RequestJSON } reqBody = bytes.NewReader(jsonStr) reqLen = int64(len(jsonStr)) @@ -485,7 +476,7 @@ func (params *FullRequest) compileRequest(ctx context.Context) (*http.Request, e } } else if params.Method != http.MethodGet && params.Method != http.MethodHead { params.RequestJSON = struct{}{} - logBody = json.RawMessage("{}") + logBody = params.RequestJSON reqBody = bytes.NewReader([]byte("{}")) reqLen = 2 } @@ -918,7 +909,7 @@ func (cli *Client) RegisterAvailable(ctx context.Context, username string) (resp return } -func (cli *Client) register(ctx context.Context, url string, req *ReqRegister[any]) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { +func (cli *Client) register(ctx context.Context, url string, req *ReqRegister) (resp *RespRegister, uiaResp *RespUserInteractive, err error) { var bodyBytes []byte bodyBytes, err = cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, @@ -942,7 +933,7 @@ func (cli *Client) register(ctx context.Context, url string, req *ReqRegister[an // Register makes an HTTP request according to https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register // // Registers with kind=user. For kind=guest, see RegisterGuest. -func (cli *Client) Register(ctx context.Context, req *ReqRegister[any]) (*RespRegister, *RespUserInteractive, error) { +func (cli *Client) Register(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { u := cli.BuildClientURL("v3", "register") return cli.register(ctx, u, req) } @@ -951,7 +942,7 @@ func (cli *Client) Register(ctx context.Context, req *ReqRegister[any]) (*RespRe // with kind=guest. // // For kind=user, see Register. -func (cli *Client) RegisterGuest(ctx context.Context, req *ReqRegister[any]) (*RespRegister, *RespUserInteractive, error) { +func (cli *Client) RegisterGuest(ctx context.Context, req *ReqRegister) (*RespRegister, *RespUserInteractive, error) { query := map[string]string{ "kind": "guest", } @@ -974,7 +965,7 @@ func (cli *Client) RegisterGuest(ctx context.Context, req *ReqRegister[any]) (*R // panic(err) // } // token := res.AccessToken -func (cli *Client) RegisterDummy(ctx context.Context, req *ReqRegister[any]) (*RespRegister, error) { +func (cli *Client) RegisterDummy(ctx context.Context, req *ReqRegister) (*RespRegister, error) { _, uia, err := cli.Register(ctx, req) if err != nil && uia == nil { return nil, err @@ -1158,9 +1149,7 @@ func (cli *Client) SearchUserDirectory(ctx context.Context, query string, limit } func (cli *Client) GetMutualRooms(ctx context.Context, otherUserID id.UserID, extras ...ReqMutualRooms) (resp *RespMutualRooms, err error) { - supportsStable := cli.SpecVersions.Supports(FeatureStableMutualRooms) - supportsUnstable := cli.SpecVersions.Supports(FeatureUnstableMutualRooms) - if cli.SpecVersions != nil && !supportsUnstable && !supportsStable { + if cli.SpecVersions != nil && !cli.SpecVersions.Supports(FeatureMutualRooms) { err = fmt.Errorf("server does not support fetching mutual rooms") return } @@ -1170,10 +1159,7 @@ func (cli *Client) GetMutualRooms(ctx context.Context, otherUserID id.UserID, ex if len(extras) > 0 { query["from"] = extras[0].From } - urlPath := cli.BuildURLWithQuery(ClientURLPath{"v1", "mutual_rooms"}, query) - if !supportsStable && supportsUnstable { - urlPath = cli.BuildURLWithQuery(ClientURLPath{"unstable", "uk.half-shot.msc2666", "user", "mutual_rooms"}, query) - } + urlPath := cli.BuildURLWithQuery(ClientURLPath{"unstable", "uk.half-shot.msc2666", "user", "mutual_rooms"}, query) _, err = cli.MakeRequest(ctx, http.MethodGet, urlPath, nil, &resp) return } @@ -1338,9 +1324,6 @@ func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, event if req.UnstableDelay > 0 { queryParams["org.matrix.msc4140.delay"] = strconv.FormatInt(req.UnstableDelay.Milliseconds(), 10) } - if req.UnstableStickyDuration > 0 { - queryParams["org.matrix.msc4354.sticky_duration_ms"] = strconv.FormatInt(req.UnstableStickyDuration.Milliseconds(), 10) - } if !req.DontEncrypt && cli != nil && cli.Crypto != nil && eventType != event.EventReaction && eventType != event.EventEncrypted { var isEncrypted bool @@ -1364,48 +1347,6 @@ func (cli *Client) SendMessageEvent(ctx context.Context, roomID id.RoomID, event return } -// BeeperSendEphemeralEvent sends an ephemeral event into a room using Beeper's unstable endpoint. -// contentJSON should be a value that can be encoded as JSON using json.Marshal. -func (cli *Client) BeeperSendEphemeralEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, contentJSON any, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { - var req ReqSendEvent - if len(extra) > 0 { - req = extra[0] - } - - var txnID string - if len(req.TransactionID) > 0 { - txnID = req.TransactionID - } else { - txnID = cli.TxnID() - } - - queryParams := map[string]string{} - if req.Timestamp > 0 { - queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10) - } - - if !req.DontEncrypt && cli != nil && cli.Crypto != nil && eventType != event.EventEncrypted { - var isEncrypted bool - isEncrypted, err = cli.StateStore.IsEncrypted(ctx, roomID) - if err != nil { - err = fmt.Errorf("failed to check if room is encrypted: %w", err) - return - } - if isEncrypted { - if contentJSON, err = cli.Crypto.Encrypt(ctx, roomID, eventType, contentJSON); err != nil { - err = fmt.Errorf("failed to encrypt event: %w", err) - return - } - eventType = event.EventEncrypted - } - } - - urlData := ClientURLPath{"unstable", "com.beeper.ephemeral", "rooms", roomID, "ephemeral", eventType.String(), txnID} - urlPath := cli.BuildURLWithQuery(urlData, queryParams) - _, err = cli.MakeRequest(ctx, http.MethodPut, urlPath, contentJSON, &resp) - return -} - // SendStateEvent sends a state event into a room. See https://spec.matrix.org/v1.16/client-server-api/#put_matrixclientv3roomsroomidstateeventtypestatekey // contentJSON should be a pointer to something that can be encoded as JSON using json.Marshal. func (cli *Client) SendStateEvent(ctx context.Context, roomID id.RoomID, eventType event.Type, stateKey string, contentJSON any, extra ...ReqSendEvent) (resp *RespSendEvent, err error) { @@ -1424,9 +1365,6 @@ func (cli *Client) SendStateEvent(ctx context.Context, roomID id.RoomID, eventTy if req.UnstableDelay > 0 { queryParams["org.matrix.msc4140.delay"] = strconv.FormatInt(req.UnstableDelay.Milliseconds(), 10) } - if req.UnstableStickyDuration > 0 { - queryParams["org.matrix.msc4354.sticky_duration_ms"] = strconv.FormatInt(req.UnstableStickyDuration.Milliseconds(), 10) - } if req.Timestamp > 0 { queryParams["ts"] = strconv.FormatInt(req.Timestamp, 10) } @@ -1989,15 +1927,10 @@ func (cli *Client) UploadAsync(ctx context.Context, req ReqUploadMedia) (*RespCr } req.MXC = resp.ContentURI req.UnstableUploadURL = resp.UnstableUploadURL - if req.AsyncContext == nil { - req.AsyncContext = cli.cliOrContextLog(ctx).WithContext(context.Background()) - } go func() { - _, err = cli.UploadMedia(req.AsyncContext, req) + _, err = cli.UploadMedia(ctx, req) if err != nil { - zerolog.Ctx(req.AsyncContext).Err(err). - Stringer("mxc", req.MXC). - Msg("Async upload of media failed") + cli.Log.Error().Stringer("mxc", req.MXC).Err(err).Msg("Async upload of media failed") } }() return resp, nil @@ -2033,7 +1966,6 @@ type ReqUploadMedia struct { ContentType string FileName string - AsyncContext context.Context DoneCallback func() // MXC specifies an existing MXC URI which doesn't have content yet to upload into. @@ -2046,10 +1978,7 @@ type ReqUploadMedia struct { } func (cli *Client) tryUploadMediaToURL(ctx context.Context, url, contentType string, content io.Reader, contentLength int64) (*http.Response, error) { - cli.Log.Debug(). - Str("url", url). - Int64("content_length", contentLength). - Msg("Uploading media to external URL") + cli.Log.Debug().Str("url", url).Msg("Uploading media to external URL") req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, content) if err != nil { return nil, err @@ -2687,13 +2616,13 @@ func (cli *Client) SetDeviceInfo(ctx context.Context, deviceID id.DeviceID, req return err } -func (cli *Client) DeleteDevice(ctx context.Context, deviceID id.DeviceID, req *ReqDeleteDevice[any]) error { +func (cli *Client) DeleteDevice(ctx context.Context, deviceID id.DeviceID, req *ReqDeleteDevice) error { urlPath := cli.BuildClientURL("v3", "devices", deviceID) _, err := cli.MakeRequest(ctx, http.MethodDelete, urlPath, req, nil) return err } -func (cli *Client) DeleteDevices(ctx context.Context, req *ReqDeleteDevices[any]) error { +func (cli *Client) DeleteDevices(ctx context.Context, req *ReqDeleteDevices) error { urlPath := cli.BuildClientURL("v3", "delete_devices") _, err := cli.MakeRequest(ctx, http.MethodPost, urlPath, req, nil) return err @@ -2704,7 +2633,7 @@ type UIACallback = func(*RespUserInteractive) interface{} // UploadCrossSigningKeys uploads the given cross-signing keys to the server. // Because the endpoint requires user-interactive authentication a callback must be provided that, // given the UI auth parameters, produces the required result (or nil to end the flow). -func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCrossSigningKeysReq[any], uiaCallback UIACallback) error { +func (cli *Client) UploadCrossSigningKeys(ctx context.Context, keys *UploadCrossSigningKeysReq, uiaCallback UIACallback) error { content, err := cli.MakeFullRequest(ctx, FullRequest{ Method: http.MethodPost, URL: cli.BuildClientURL("v3", "keys", "device_signing", "upload"), diff --git a/client_ephemeral_test.go b/client_ephemeral_test.go deleted file mode 100644 index c2846427..00000000 --- a/client_ephemeral_test.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright (c) 2026 Tulir Asokan -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package mautrix_test - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "net/http/httptest" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" -) - -func TestClient_SendEphemeralEvent_UsesUnstablePathTxnAndTS(t *testing.T) { - roomID := id.RoomID("!room:example.com") - evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType} - txnID := "txn-123" - - var gotPath string - var gotQueryTS string - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath = r.URL.Path - gotQueryTS = r.URL.Query().Get("ts") - assert.Equal(t, http.MethodPut, r.Method) - _, _ = w.Write([]byte(`{"event_id":"$evt"}`)) - })) - defer ts.Close() - - cli, err := mautrix.NewClient(ts.URL, "", "") - require.NoError(t, err) - - _, err = cli.BeeperSendEphemeralEvent( - context.Background(), - roomID, - evtType, - map[string]any{"foo": "bar"}, - mautrix.ReqSendEvent{TransactionID: txnID, Timestamp: 1234}, - ) - require.NoError(t, err) - - assert.True(t, strings.Contains(gotPath, "/_matrix/client/unstable/com.beeper.ephemeral/rooms/")) - assert.True(t, strings.HasSuffix(gotPath, "/ephemeral/com.example.ephemeral/"+txnID)) - assert.Equal(t, "1234", gotQueryTS) -} - -func TestClient_SendEphemeralEvent_UnsupportedReturnsMUnrecognized(t *testing.T) { - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(http.StatusNotFound) - _, _ = w.Write([]byte(`{"errcode":"M_UNRECOGNIZED","error":"Unrecognized endpoint"}`)) - })) - defer ts.Close() - - cli, err := mautrix.NewClient(ts.URL, "", "") - require.NoError(t, err) - - _, err = cli.BeeperSendEphemeralEvent( - context.Background(), - id.RoomID("!room:example.com"), - event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType}, - map[string]any{"foo": "bar"}, - ) - require.Error(t, err) - assert.True(t, errors.Is(err, mautrix.MUnrecognized)) -} - -func TestClient_SendEphemeralEvent_EncryptsInEncryptedRooms(t *testing.T) { - roomID := id.RoomID("!room:example.com") - evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType} - txnID := "txn-encrypted" - - stateStore := mautrix.NewMemoryStateStore() - err := stateStore.SetEncryptionEvent(context.Background(), roomID, &event.EncryptionEventContent{ - Algorithm: id.AlgorithmMegolmV1, - }) - require.NoError(t, err) - - fakeCrypto := &fakeCryptoHelper{ - encryptedContent: &event.EncryptedEventContent{ - Algorithm: id.AlgorithmMegolmV1, - MegolmCiphertext: []byte("ciphertext"), - }, - } - - var gotPath string - var gotBody map[string]any - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - gotPath = r.URL.Path - assert.Equal(t, http.MethodPut, r.Method) - err := json.NewDecoder(r.Body).Decode(&gotBody) - require.NoError(t, err) - _, _ = w.Write([]byte(`{"event_id":"$evt"}`)) - })) - defer ts.Close() - - cli, err := mautrix.NewClient(ts.URL, "", "") - require.NoError(t, err) - cli.StateStore = stateStore - cli.Crypto = fakeCrypto - - _, err = cli.BeeperSendEphemeralEvent( - context.Background(), - roomID, - evtType, - map[string]any{"foo": "bar"}, - mautrix.ReqSendEvent{TransactionID: txnID}, - ) - require.NoError(t, err) - - assert.True(t, strings.HasSuffix(gotPath, "/ephemeral/m.room.encrypted/"+txnID)) - assert.Equal(t, string(id.AlgorithmMegolmV1), gotBody["algorithm"]) - assert.Equal(t, 1, fakeCrypto.encryptCalls) - assert.Equal(t, roomID, fakeCrypto.lastRoomID) - assert.Equal(t, evtType, fakeCrypto.lastEventType) -} - -type fakeCryptoHelper struct { - encryptCalls int - lastRoomID id.RoomID - lastEventType event.Type - lastEncryptInput any - encryptedContent *event.EncryptedEventContent -} - -func (f *fakeCryptoHelper) Encrypt(_ context.Context, roomID id.RoomID, eventType event.Type, content any) (*event.EncryptedEventContent, error) { - f.encryptCalls++ - f.lastRoomID = roomID - f.lastEventType = eventType - f.lastEncryptInput = content - return f.encryptedContent, nil -} - -func (f *fakeCryptoHelper) Decrypt(context.Context, *event.Event) (*event.Event, error) { - return nil, nil -} - -func (f *fakeCryptoHelper) WaitForSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, time.Duration) bool { - return false -} - -func (f *fakeCryptoHelper) RequestSession(context.Context, id.RoomID, id.SenderKey, id.SessionID, id.UserID, id.DeviceID) { -} - -func (f *fakeCryptoHelper) Init(context.Context) error { - return nil -} diff --git a/crypto/cross_sign_key.go b/crypto/cross_sign_key.go index 5d9bf5b3..4094f695 100644 --- a/crypto/cross_sign_key.go +++ b/crypto/cross_sign_key.go @@ -135,7 +135,7 @@ func (mach *OlmMachine) PublishCrossSigningKeys(ctx context.Context, keys *Cross } userKey.Signatures = signatures.NewSingleSignature(userID, id.KeyAlgorithmEd25519, keys.MasterKey.PublicKey().String(), userSig) - err = mach.Client.UploadCrossSigningKeys(ctx, &mautrix.UploadCrossSigningKeysReq[any]{ + err = mach.Client.UploadCrossSigningKeys(ctx, &mautrix.UploadCrossSigningKeysReq{ Master: masterKey, SelfSigning: selfKey, UserSigning: userKey, diff --git a/crypto/cross_sign_ssss.go b/crypto/cross_sign_ssss.go index fd42880d..50b58ea0 100644 --- a/crypto/cross_sign_ssss.go +++ b/crypto/cross_sign_ssss.go @@ -8,7 +8,6 @@ package crypto import ( "context" - "errors" "fmt" "maunium.net/go/mautrix" @@ -78,11 +77,7 @@ func (mach *OlmMachine) VerifyWithRecoveryKey(ctx context.Context, recoveryKey s return fmt.Errorf("failed to get default SSSS key data: %w", err) } key, err := keyData.VerifyRecoveryKey(keyID, recoveryKey) - if errors.Is(err, ssss.ErrUnverifiableKey) { - mach.machOrContextLog(ctx).Warn(). - Str("key_id", keyID). - Msg("SSSS key is unverifiable, trying to use without verifying") - } else if err != nil { + if err != nil { return err } err = mach.FetchCrossSigningKeysFromSSSS(ctx, key) diff --git a/crypto/decryptmegolm.go b/crypto/decryptmegolm.go index 457d5a0c..d8b419ab 100644 --- a/crypto/decryptmegolm.go +++ b/crypto/decryptmegolm.go @@ -29,8 +29,8 @@ var ( ErrDuplicateMessageIndex = errors.New("duplicate megolm message index") ErrWrongRoom = errors.New("encrypted megolm event is not intended for this room") ErrDeviceKeyMismatch = errors.New("device keys in event and verified device info do not match") + ErrSenderKeyMismatch = errors.New("sender keys in content and megolm session do not match") ErrRatchetError = errors.New("failed to ratchet session after use") - ErrCorruptedMegolmPayload = errors.New("corrupted megolm payload") ) // Deprecated: use variables prefixed with Err @@ -40,6 +40,7 @@ var ( DuplicateMessageIndex = ErrDuplicateMessageIndex WrongRoom = ErrWrongRoom DeviceKeyMismatch = ErrDeviceKeyMismatch + SenderKeyMismatch = ErrSenderKeyMismatch RatchetError = ErrRatchetError ) @@ -55,17 +56,6 @@ var ( relatesToTopLevelPath = exgjson.Path("content", "m.relates_to") ) -const sessionIDLength = 43 - -func validateCiphertextCharacters(ciphertext []byte) bool { - for _, b := range ciphertext { - if (b < 'a' || b > 'z') && (b < 'A' || b > 'Z') && (b < '0' || b > '9') && b != '+' && b != '/' { - return false - } - } - return true -} - // DecryptMegolmEvent decrypts an m.room.encrypted event where the algorithm is m.megolm.v1.aes-sha2 func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event) (*event.Event, error) { content, ok := evt.Content.Parsed.(*event.EncryptedEventContent) @@ -73,12 +63,6 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event return nil, ErrIncorrectEncryptedContentType } else if content.Algorithm != id.AlgorithmMegolmV1 { return nil, ErrUnsupportedAlgorithm - } else if len(content.MegolmCiphertext) < 74 { - return nil, fmt.Errorf("%w: ciphertext too short (%d bytes)", ErrCorruptedMegolmPayload, len(content.MegolmCiphertext)) - } else if len(content.SessionID) != sessionIDLength { - return nil, fmt.Errorf("%w: invalid session ID length %d", ErrCorruptedMegolmPayload, len(content.SessionID)) - } else if !validateCiphertextCharacters(content.MegolmCiphertext) { - return nil, fmt.Errorf("%w: invalid characters in ciphertext", ErrCorruptedMegolmPayload) } log := mach.machOrContextLog(ctx).With(). Str("action", "decrypt megolm event"). @@ -124,13 +108,7 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event Msg("Couldn't resolve trust level of session: sent by unknown device") trustLevel = id.TrustStateUnknownDevice } else if device.SigningKey != sess.SigningKey || device.IdentityKey != sess.SenderKey { - log.Debug(). - Stringer("session_sender_key", sess.SenderKey). - Stringer("device_sender_key", device.IdentityKey). - Stringer("session_signing_key", sess.SigningKey). - Stringer("device_signing_key", device.SigningKey). - Msg("Device keys don't match keys in session, marking as untrusted") - trustLevel = id.TrustStateDeviceKeyMismatch + return nil, ErrDeviceKeyMismatch } else { trustLevel, err = mach.ResolveTrustContext(ctx, device) if err != nil { @@ -213,7 +191,6 @@ func (mach *OlmMachine) DecryptMegolmEvent(ctx context.Context, evt *event.Event TrustSource: device, ForwardedKeys: forwardedKeys, WasEncrypted: true, - EventSource: evt.Mautrix.EventSource | event.SourceDecrypted, ReceivedAt: evt.Mautrix.ReceivedAt, }, }, nil @@ -259,6 +236,8 @@ func (mach *OlmMachine) actuallyDecryptMegolmEvent(ctx context.Context, evt *eve return nil, nil, 0, fmt.Errorf("failed to get group session: %w", err) } else if sess == nil { return nil, nil, 0, fmt.Errorf("%w (ID %s)", ErrNoSessionFound, content.SessionID) + } else if content.SenderKey != "" && content.SenderKey != sess.SenderKey { + return sess, nil, 0, ErrSenderKeyMismatch } plaintext, messageIndex, err := sess.Internal.Decrypt(content.MegolmCiphertext) if err != nil { diff --git a/crypto/decryptolm.go b/crypto/decryptolm.go index aea5e6dc..cd02726d 100644 --- a/crypto/decryptolm.go +++ b/crypto/decryptolm.go @@ -134,9 +134,6 @@ func (mach *OlmMachine) decryptAndParseOlmCiphertext(ctx context.Context, evt *e } func olmMessageHash(ciphertext string) ([32]byte, error) { - if ciphertext == "" { - return [32]byte{}, fmt.Errorf("empty ciphertext") - } ciphertextBytes, err := base64.RawStdEncoding.DecodeString(ciphertext) return sha256.Sum256(ciphertextBytes), err } diff --git a/crypto/encryptmegolm.go b/crypto/encryptmegolm.go index 88f9c8d4..8ce70ca0 100644 --- a/crypto/encryptmegolm.go +++ b/crypto/encryptmegolm.go @@ -91,16 +91,11 @@ func IsShareError(err error) bool { } func ParseMegolmMessageIndex(ciphertext []byte) (uint, error) { - if len(ciphertext) == 0 { - return 0, fmt.Errorf("empty ciphertext") - } decoded := make([]byte, base64.RawStdEncoding.DecodedLen(len(ciphertext))) var err error _, err = base64.RawStdEncoding.Decode(decoded, ciphertext) if err != nil { return 0, err - } else if len(decoded) < 2+binary.MaxVarintLen64 { - return 0, fmt.Errorf("decoded ciphertext too short: %d bytes", len(decoded)) } else if decoded[0] != 3 || decoded[1] != 8 { return 0, fmt.Errorf("unexpected initial bytes %d and %d", decoded[0], decoded[1]) } @@ -370,19 +365,26 @@ func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session log.Trace().Msg("Encrypting group session for all found devices") deviceCount := 0 toDevice := &mautrix.ReqSendToDevice{Messages: make(map[id.UserID]map[id.DeviceID]*event.Content)} - logUsers := zerolog.Dict() for userID, sessions := range olmSessions { if len(sessions) == 0 { continue } - logDevices := zerolog.Dict() output := make(map[id.DeviceID]*event.Content) toDevice.Messages[userID] = output for deviceID, device := range sessions { + log.Trace(). + Stringer("target_user_id", userID). + Stringer("target_device_id", deviceID). + Stringer("target_identity_key", device.identity.IdentityKey). + Msg("Encrypting group session for device") content := mach.encryptOlmEvent(ctx, device.session, device.identity, event.ToDeviceRoomKey, session.ShareContent()) output[deviceID] = &event.Content{Parsed: content} - logDevices.Str(string(deviceID), string(device.identity.IdentityKey)) deviceCount++ + log.Debug(). + Stringer("target_user_id", userID). + Stringer("target_device_id", deviceID). + Stringer("target_identity_key", device.identity.IdentityKey). + Msg("Encrypted group session for device") if !mach.DisableSharedGroupSessionTracking { err := mach.CryptoStore.MarkOutboundGroupSessionShared(ctx, userID, device.identity.IdentityKey, session.id) if err != nil { @@ -396,13 +398,11 @@ func (mach *OlmMachine) encryptAndSendGroupSession(ctx context.Context, session } } } - logUsers.Dict(string(userID), logDevices) } log.Debug(). Int("device_count", deviceCount). Int("user_count", len(toDevice.Messages)). - Dict("destination_map", logUsers). Msg("Sending to-device messages to share group session") _, err := mach.Client.SendToDevice(ctx, event.ToDeviceEncrypted, toDevice) return err diff --git a/crypto/encryptolm.go b/crypto/encryptolm.go index 765307af..80b76dc5 100644 --- a/crypto/encryptolm.go +++ b/crypto/encryptolm.go @@ -96,19 +96,15 @@ func (mach *OlmMachine) encryptOlmEvent(ctx context.Context, session *OlmSession panic(err) } log := mach.machOrContextLog(ctx) + log.Debug(). + Str("recipient_identity_key", recipient.IdentityKey.String()). + Str("olm_session_id", session.ID().String()). + Str("olm_session_description", session.Describe()). + Msg("Encrypting olm message") msgType, ciphertext, err := session.Encrypt(plaintext) if err != nil { panic(err) } - ciphertextStr := string(ciphertext) - ciphertextHash, _ := olmMessageHash(ciphertextStr) - log.Debug(). - Stringer("event_type", evtType). - Str("recipient_identity_key", recipient.IdentityKey.String()). - Str("olm_session_id", session.ID().String()). - Str("olm_session_description", session.Describe()). - Hex("ciphertext_hash", ciphertextHash[:]). - Msg("Encrypted olm message") err = mach.CryptoStore.UpdateSession(ctx, recipient.IdentityKey, session) if err != nil { log.Error().Err(err).Msg("Failed to update olm session in crypto store after encrypting") @@ -119,7 +115,7 @@ func (mach *OlmMachine) encryptOlmEvent(ctx context.Context, session *OlmSession OlmCiphertext: event.OlmCiphertexts{ recipient.IdentityKey: { Type: msgType, - Body: ciphertextStr, + Body: string(ciphertext), }, }, } diff --git a/crypto/goolm/crypto/curve25519.go b/crypto/goolm/crypto/curve25519.go index 6e42d886..e9759501 100644 --- a/crypto/goolm/crypto/curve25519.go +++ b/crypto/goolm/crypto/curve25519.go @@ -53,7 +53,6 @@ func (c Curve25519KeyPair) B64Encoded() id.Curve25519 { // SharedSecret returns the shared secret between the key pair and the given public key. func (c Curve25519KeyPair) SharedSecret(pubKey Curve25519PublicKey) ([]byte, error) { - // Note: the standard library checks that the output is non-zero return c.PrivateKey.SharedSecret(pubKey) } diff --git a/crypto/goolm/crypto/curve25519_test.go b/crypto/goolm/crypto/curve25519_test.go index 2550f15e..9039c126 100644 --- a/crypto/goolm/crypto/curve25519_test.go +++ b/crypto/goolm/crypto/curve25519_test.go @@ -25,8 +25,6 @@ func TestCurve25519(t *testing.T) { fromPrivate, err := crypto.Curve25519GenerateFromPrivate(firstKeypair.PrivateKey) assert.NoError(t, err) assert.Equal(t, fromPrivate, firstKeypair) - _, err = secondKeypair.SharedSecret(make([]byte, crypto.Curve25519PublicKeyLength)) - assert.Error(t, err) } func TestCurve25519Case1(t *testing.T) { diff --git a/crypto/keybackup.go b/crypto/keybackup.go index 7b3c30db..ceec1d58 100644 --- a/crypto/keybackup.go +++ b/crypto/keybackup.go @@ -200,14 +200,13 @@ func (mach *OlmMachine) ImportRoomKeyFromBackupWithoutSaving( SigningKey: keyBackupData.SenderClaimedKeys.Ed25519, SenderKey: keyBackupData.SenderKey, RoomID: roomID, - ForwardingChains: keyBackupData.ForwardingKeyChain, + ForwardingChains: append(keyBackupData.ForwardingKeyChain, keyBackupData.SenderKey.String()), id: sessionID, ReceivedAt: time.Now().UTC(), MaxAge: maxAge.Milliseconds(), MaxMessages: maxMessages, KeyBackupVersion: version, - KeySource: id.KeySourceBackup, }, nil } diff --git a/crypto/keyexport_test.go b/crypto/keyexport_test.go index fd6f105d..47616a20 100644 --- a/crypto/keyexport_test.go +++ b/crypto/keyexport_test.go @@ -31,5 +31,5 @@ func TestExportKeys(t *testing.T) { )) data, err := crypto.ExportKeys("meow", []*crypto.InboundGroupSession{sess}) assert.NoError(t, err) - assert.Len(t, data, 893) + assert.Len(t, data, 836) } diff --git a/crypto/keyimport.go b/crypto/keyimport.go index 3ffc74a5..36ad6b9c 100644 --- a/crypto/keyimport.go +++ b/crypto/keyimport.go @@ -108,20 +108,19 @@ func (mach *OlmMachine) importExportedRoomKey(ctx context.Context, session Expor return false, ErrMismatchingExportedSessionID } igs := &InboundGroupSession{ - Internal: igsInternal, - SigningKey: session.SenderClaimedKeys.Ed25519, - SenderKey: session.SenderKey, - RoomID: session.RoomID, + Internal: igsInternal, + SigningKey: session.SenderClaimedKeys.Ed25519, + SenderKey: session.SenderKey, + RoomID: session.RoomID, + // TODO should we add something here to mark the signing key as unverified like key requests do? ForwardingChains: session.ForwardingChains, - KeySource: id.KeySourceImport, - ReceivedAt: time.Now().UTC(), + + ReceivedAt: time.Now().UTC(), } existingIGS, _ := mach.CryptoStore.GetGroupSession(ctx, igs.RoomID, igs.ID()) firstKnownIndex := igs.Internal.FirstKnownIndex() if existingIGS != nil && existingIGS.Internal.FirstKnownIndex() <= firstKnownIndex { - // We already have an equivalent or better session in the store, so don't override it, - // but do notify the session received callback just in case. - mach.MarkSessionReceived(ctx, session.RoomID, igs.ID(), existingIGS.Internal.FirstKnownIndex()) + // We already have an equivalent or better session in the store, so don't override it. return false, nil } err = mach.CryptoStore.PutGroupSession(ctx, igs) diff --git a/crypto/keysharing.go b/crypto/keysharing.go index 19a68c87..f1d427af 100644 --- a/crypto/keysharing.go +++ b/crypto/keysharing.go @@ -189,7 +189,6 @@ func (mach *OlmMachine) importForwardedRoomKey(ctx context.Context, evt *Decrypt MaxAge: maxAge.Milliseconds(), MaxMessages: maxMessages, IsScheduled: content.IsScheduled, - KeySource: id.KeySourceForward, } existingIGS, _ := mach.CryptoStore.GetGroupSession(ctx, igs.RoomID, igs.ID()) if existingIGS != nil && existingIGS.Internal.FirstKnownIndex() <= igs.Internal.FirstKnownIndex() { @@ -215,7 +214,6 @@ func (mach *OlmMachine) rejectKeyRequest(ctx context.Context, rejection KeyShare RoomID: request.RoomID, Algorithm: request.Algorithm, SessionID: request.SessionID, - //lint:ignore SA1019 This is just echoing back the deprecated field SenderKey: request.SenderKey, Code: rejection.Code, Reason: rejection.Reason, @@ -265,14 +263,9 @@ func (mach *OlmMachine) defaultAllowKeyShare(ctx context.Context, device *id.Dev log.Err(err).Msg("Rejecting key request due to internal error when checking session sharing") return &KeyShareRejectNoResponse } else if !isShared { - igs, _ := mach.CryptoStore.GetGroupSession(ctx, evt.RoomID, evt.SessionID) - if igs != nil && igs.SenderKey == mach.OwnIdentity().IdentityKey { - log.Debug().Msg("Rejecting key request for unshared session") - return &KeyShareRejectNotRecipient - } - // Note: this case will also happen for redacted sessions and database errors - log.Debug().Msg("Rejecting key request for session created by another device") - return &KeyShareRejectNoResponse + // TODO differentiate session not shared with requester vs session not created by this device? + log.Debug().Msg("Rejecting key request for unshared session") + return &KeyShareRejectNotRecipient } log.Debug().Msg("Accepting key request for shared session") return nil @@ -330,9 +323,7 @@ func (mach *OlmMachine) HandleRoomKeyRequest(ctx context.Context, sender id.User if err != nil { if errors.Is(err, ErrGroupSessionWithheld) { log.Debug().Err(err).Msg("Requested group session not available") - if sender != mach.Client.UserID { - mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) - } + mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) } else { log.Error().Err(err).Msg("Failed to get group session to forward") mach.rejectKeyRequest(ctx, KeyShareRejectInternalError, device, content.Body) @@ -340,9 +331,7 @@ func (mach *OlmMachine) HandleRoomKeyRequest(ctx context.Context, sender id.User return } else if igs == nil { log.Error().Msg("Didn't find group session to forward") - if sender != mach.Client.UserID { - mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) - } + mach.rejectKeyRequest(ctx, KeyShareRejectUnavailable, device, content.Body) return } if internalID := igs.ID(); internalID != content.Body.SessionID { @@ -367,7 +356,7 @@ func (mach *OlmMachine) HandleRoomKeyRequest(ctx context.Context, sender id.User SessionID: igs.ID(), SessionKey: string(exportedKey), }, - SenderKey: igs.SenderKey, + SenderKey: content.Body.SenderKey, ForwardingKeyChain: igs.ForwardingChains, SenderClaimedKey: igs.SigningKey, }, diff --git a/crypto/sessions.go b/crypto/sessions.go index ccc7b784..6b90c998 100644 --- a/crypto/sessions.go +++ b/crypto/sessions.go @@ -117,7 +117,6 @@ type InboundGroupSession struct { MaxMessages int IsScheduled bool KeyBackupVersion id.KeyBackupVersion - KeySource id.KeySource id id.SessionID } @@ -137,7 +136,6 @@ func NewInboundGroupSession(senderKey id.SenderKey, signingKey id.Ed25519, roomI MaxAge: maxAge.Milliseconds(), MaxMessages: maxMessages, IsScheduled: isScheduled, - KeySource: id.KeySourceDirect, }, nil } @@ -171,7 +169,7 @@ func (igs *InboundGroupSession) export() (*ExportedSession, error) { ForwardingChains: igs.ForwardingChains, RoomID: igs.RoomID, SenderKey: igs.SenderKey, - SenderClaimedKeys: SenderClaimedKeys{Ed25519: igs.SigningKey}, + SenderClaimedKeys: SenderClaimedKeys{}, SessionID: igs.ID(), SessionKey: string(key), }, nil diff --git a/crypto/sql_store.go b/crypto/sql_store.go index 138cc557..ca75b3f6 100644 --- a/crypto/sql_store.go +++ b/crypto/sql_store.go @@ -346,23 +346,22 @@ func (store *SQLCryptoStore) PutGroupSession(ctx context.Context, session *Inbou Int("max_messages", session.MaxMessages). Bool("is_scheduled", session.IsScheduled). Stringer("key_backup_version", session.KeyBackupVersion). - Stringer("key_source", session.KeySource). Msg("Upserting megolm inbound group session") _, err = store.DB.Exec(ctx, ` INSERT INTO crypto_megolm_inbound_session ( session_id, sender_key, signing_key, room_id, session, forwarding_chains, - ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, key_source, account_id - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) + ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, account_id + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) ON CONFLICT (session_id, account_id) DO UPDATE SET withheld_code=NULL, withheld_reason=NULL, sender_key=excluded.sender_key, signing_key=excluded.signing_key, room_id=excluded.room_id, session=excluded.session, forwarding_chains=excluded.forwarding_chains, ratchet_safety=excluded.ratchet_safety, received_at=excluded.received_at, max_age=excluded.max_age, max_messages=excluded.max_messages, is_scheduled=excluded.is_scheduled, - key_backup_version=excluded.key_backup_version, key_source=excluded.key_source + key_backup_version=excluded.key_backup_version `, session.ID(), session.SenderKey, session.SigningKey, session.RoomID, sessionBytes, forwardingChains, ratchetSafety, datePtr(session.ReceivedAt), dbutil.NumPtr(session.MaxAge), dbutil.NumPtr(session.MaxMessages), - session.IsScheduled, session.KeyBackupVersion, session.KeySource, store.AccountID, + session.IsScheduled, session.KeyBackupVersion, store.AccountID, ) return err } @@ -375,13 +374,12 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room var maxAge, maxMessages sql.NullInt64 var isScheduled bool var version id.KeyBackupVersion - var keySource id.KeySource err := store.DB.QueryRow(ctx, ` - SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, key_source + SELECT sender_key, signing_key, session, forwarding_chains, withheld_code, withheld_reason, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE room_id=$1 AND session_id=$2 AND account_id=$3`, roomID, sessionID, store.AccountID, - ).Scan(&senderKey, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version, &keySource) + ).Scan(&senderKey, &signingKey, &sessionBytes, &forwardingChains, &withheldCode, &withheldReason, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version) if errors.Is(err, sql.ErrNoRows) { return nil, nil } else if err != nil { @@ -412,7 +410,6 @@ func (store *SQLCryptoStore) GetGroupSession(ctx context.Context, roomID id.Room MaxMessages: int(maxMessages.Int64), IsScheduled: isScheduled, KeyBackupVersion: version, - KeySource: keySource, }, nil } @@ -537,8 +534,7 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In var maxAge, maxMessages sql.NullInt64 var isScheduled bool var version id.KeyBackupVersion - var keySource id.KeySource - err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version, &keySource) + err := rows.Scan(&roomID, &senderKey, &signingKey, &sessionBytes, &forwardingChains, &ratchetSafetyBytes, &receivedAt, &maxAge, &maxMessages, &isScheduled, &version) if err != nil { return nil, err } @@ -558,13 +554,12 @@ func (store *SQLCryptoStore) scanInboundGroupSession(rows dbutil.Scannable) (*In MaxMessages: int(maxMessages.Int64), IsScheduled: isScheduled, KeyBackupVersion: version, - KeySource: keySource, }, nil } func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID id.RoomID) dbutil.RowIter[*InboundGroupSession] { rows, err := store.DB.Query(ctx, ` - SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, key_source + SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE room_id=$1 AND account_id=$2 AND session IS NOT NULL`, roomID, store.AccountID, ) @@ -573,7 +568,7 @@ func (store *SQLCryptoStore) GetGroupSessionsForRoom(ctx context.Context, roomID func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) dbutil.RowIter[*InboundGroupSession] { rows, err := store.DB.Query(ctx, ` - SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, key_source + SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL`, store.AccountID, ) @@ -582,7 +577,7 @@ func (store *SQLCryptoStore) GetAllGroupSessions(ctx context.Context) dbutil.Row func (store *SQLCryptoStore) GetGroupSessionsWithoutKeyBackupVersion(ctx context.Context, version id.KeyBackupVersion) dbutil.RowIter[*InboundGroupSession] { rows, err := store.DB.Query(ctx, ` - SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version, key_source + SELECT room_id, sender_key, signing_key, session, forwarding_chains, ratchet_safety, received_at, max_age, max_messages, is_scheduled, key_backup_version FROM crypto_megolm_inbound_session WHERE account_id=$1 AND session IS NOT NULL AND key_backup_version != $2`, store.AccountID, version, ) diff --git a/crypto/sql_store_upgrade/00-latest-revision.sql b/crypto/sql_store_upgrade/00-latest-revision.sql index 3709f1e5..af8ab5cc 100644 --- a/crypto/sql_store_upgrade/00-latest-revision.sql +++ b/crypto/sql_store_upgrade/00-latest-revision.sql @@ -1,4 +1,4 @@ --- v0 -> v19 (compatible with v15+): Latest revision +-- v0 -> v18 (compatible with v15+): Latest revision CREATE TABLE IF NOT EXISTS crypto_account ( account_id TEXT PRIMARY KEY, device_id TEXT NOT NULL, @@ -71,7 +71,6 @@ CREATE TABLE IF NOT EXISTS crypto_megolm_inbound_session ( max_messages INTEGER, is_scheduled BOOLEAN NOT NULL DEFAULT false, key_backup_version TEXT NOT NULL DEFAULT '', - key_source TEXT NOT NULL DEFAULT '', PRIMARY KEY (account_id, session_id) ); -- Useful index to find keys that need backing up diff --git a/crypto/sql_store_upgrade/19-megolm-session-source.sql b/crypto/sql_store_upgrade/19-megolm-session-source.sql deleted file mode 100644 index f624222f..00000000 --- a/crypto/sql_store_upgrade/19-megolm-session-source.sql +++ /dev/null @@ -1,2 +0,0 @@ --- v19 (compatible with v15+): Store megolm session source -ALTER TABLE crypto_megolm_inbound_session ADD COLUMN key_source TEXT NOT NULL DEFAULT ''; diff --git a/crypto/ssss/key.go b/crypto/ssss/key.go index 78ebd8f3..cd8e3fce 100644 --- a/crypto/ssss/key.go +++ b/crypto/ssss/key.go @@ -59,12 +59,12 @@ func NewKey(passphrase string) (*Key, error) { // We store a certain hash in the key metadata so that clients can check if the user entered the correct key. ivBytes := random.Bytes(utils.AESCTRIVLength) keyData.IV = base64.RawStdEncoding.EncodeToString(ivBytes) - macBytes, err := keyData.calculateHash(ssssKey) + var err error + keyData.MAC, err = keyData.calculateHash(ssssKey) if err != nil { // This should never happen because we just generated the IV and key. return nil, fmt.Errorf("failed to calculate hash: %w", err) } - keyData.MAC = base64.RawStdEncoding.EncodeToString(macBytes) return &Key{ Key: ssssKey, diff --git a/crypto/ssss/meta.go b/crypto/ssss/meta.go index 34775fa7..474c85d8 100644 --- a/crypto/ssss/meta.go +++ b/crypto/ssss/meta.go @@ -7,10 +7,7 @@ package ssss import ( - "crypto/hmac" - "crypto/sha256" "encoding/base64" - "errors" "fmt" "strings" @@ -36,9 +33,7 @@ func (kd *KeyMetadata) VerifyPassphrase(keyID, passphrase string) (*Key, error) ssssKey, err := kd.Passphrase.GetKey(passphrase) if err != nil { return nil, err - } - err = kd.verifyKey(ssssKey) - if err != nil && !errors.Is(err, ErrUnverifiableKey) { + } else if err = kd.verifyKey(ssssKey); err != nil { return nil, err } @@ -54,9 +49,7 @@ func (kd *KeyMetadata) VerifyRecoveryKey(keyID, recoveryKey string) (*Key, error ssssKey := utils.DecodeBase58RecoveryKey(recoveryKey) if ssssKey == nil { return nil, ErrInvalidRecoveryKey - } - err := kd.verifyKey(ssssKey) - if err != nil && !errors.Is(err, ErrUnverifiableKey) { + } else if err := kd.verifyKey(ssssKey); err != nil { return nil, err } @@ -64,28 +57,20 @@ func (kd *KeyMetadata) VerifyRecoveryKey(keyID, recoveryKey string) (*Key, error ID: keyID, Key: ssssKey, Metadata: kd, - }, err + }, nil } func (kd *KeyMetadata) verifyKey(key []byte) error { - if kd.MAC == "" || kd.IV == "" { - return ErrUnverifiableKey - } unpaddedMAC := strings.TrimRight(kd.MAC, "=") expectedMACLength := base64.RawStdEncoding.EncodedLen(utils.SHAHashLength) if len(unpaddedMAC) != expectedMACLength { return fmt.Errorf("%w: invalid mac length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedMAC), expectedMACLength) } - expectedMAC, err := base64.RawStdEncoding.DecodeString(unpaddedMAC) - if err != nil { - return fmt.Errorf("%w: failed to decode mac: %w", ErrCorruptedKeyMetadata, err) - } - calculatedMAC, err := kd.calculateHash(key) + hash, err := kd.calculateHash(key) if err != nil { return err } - // This doesn't really need to be constant time since it's fully local, but might as well be. - if !hmac.Equal(expectedMAC, calculatedMAC) { + if unpaddedMAC != hash { return ErrIncorrectSSSSKey } return nil @@ -98,26 +83,23 @@ func (kd *KeyMetadata) VerifyKey(key []byte) bool { // calculateHash calculates the hash used for checking if the key is entered correctly as described // in the spec: https://matrix.org/docs/spec/client_server/unstable#m-secret-storage-v1-aes-hmac-sha2 -func (kd *KeyMetadata) calculateHash(key []byte) ([]byte, error) { +func (kd *KeyMetadata) calculateHash(key []byte) (string, error) { aesKey, hmacKey := utils.DeriveKeysSHA256(key, "") unpaddedIV := strings.TrimRight(kd.IV, "=") expectedIVLength := base64.RawStdEncoding.EncodedLen(utils.AESCTRIVLength) - if len(unpaddedIV) < expectedIVLength || len(unpaddedIV) > expectedIVLength*3 { - return nil, fmt.Errorf("%w: invalid iv length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedIV), expectedIVLength) + if len(unpaddedIV) != expectedIVLength { + return "", fmt.Errorf("%w: invalid iv length %d (expected %d)", ErrCorruptedKeyMetadata, len(unpaddedIV), expectedIVLength) } - rawIVBytes, err := base64.RawStdEncoding.DecodeString(unpaddedIV) - if err != nil { - return nil, fmt.Errorf("%w: failed to decode iv: %w", ErrCorruptedKeyMetadata, err) - } - // TODO log a warning for non-16 byte IVs? - // Certain broken clients like nheko generated 32-byte IVs where only the first 16 bytes were used. - ivBytes := *(*[utils.AESCTRIVLength]byte)(rawIVBytes[:utils.AESCTRIVLength]) - zeroes := make([]byte, utils.AESCTRKeyLength) - encryptedZeroes := utils.XorA256CTR(zeroes, aesKey, ivBytes) - h := hmac.New(sha256.New, hmacKey[:]) - h.Write(encryptedZeroes) - return h.Sum(nil), nil + var ivBytes [utils.AESCTRIVLength]byte + _, err := base64.RawStdEncoding.Decode(ivBytes[:], []byte(unpaddedIV)) + if err != nil { + return "", fmt.Errorf("%w: failed to decode iv: %w", ErrCorruptedKeyMetadata, err) + } + + cipher := utils.XorA256CTR(make([]byte, utils.AESCTRKeyLength), aesKey, ivBytes) + + return utils.HMACSHA256B64(cipher, hmacKey), nil } // PassphraseMetadata represents server-side metadata about a SSSS key passphrase. diff --git a/crypto/ssss/meta_test.go b/crypto/ssss/meta_test.go index d59809c7..7a5ef8b9 100644 --- a/crypto/ssss/meta_test.go +++ b/crypto/ssss/meta_test.go @@ -8,6 +8,7 @@ package ssss_test import ( "encoding/json" + "errors" "testing" "github.com/stretchr/testify/assert" @@ -41,24 +42,10 @@ const key2Meta = ` } ` -const key2MetaUnverified = ` -{ - "algorithm": "m.secret_storage.v1.aes-hmac-sha2" -} -` - -const key2MetaLongIV = ` -{ - "algorithm": "m.secret_storage.v1.aes-hmac-sha2", - "iv": "O0BOvTqiIAYjC+RMcyHfW2f/gdxjceTxoYtNlpPduJ8=", - "mac": "7k6OruQlWg0UmQjxGZ0ad4Q6DdwkgnoI7G6X3IjBYtI=" -} -` - const key2MetaBrokenIV = ` { "algorithm": "m.secret_storage.v1.aes-hmac-sha2", - "iv": "MeowMeowMeow", + "iv": "O0BOvTqiIAYjC+RMcyHfWwMeowMeowMeow", "mac": "7k6OruQlWg0UmQjxGZ0ad4Q6DdwkgnoI7G6X3IjBYtI=" } ` @@ -107,33 +94,17 @@ func TestKeyMetadata_VerifyRecoveryKey_Correct2(t *testing.T) { assert.Equal(t, key2RecoveryKey, key.RecoveryKey()) } -func TestKeyMetadata_VerifyRecoveryKey_NonCompliant_LongIV(t *testing.T) { - km := getKeyMeta(key2MetaLongIV) - key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey) - assert.NoError(t, err) - assert.NotNil(t, key) - assert.Equal(t, key2RecoveryKey, key.RecoveryKey()) -} - -func TestKeyMetadata_VerifyRecoveryKey_Unverified(t *testing.T) { - km := getKeyMeta(key2MetaUnverified) - key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey) - assert.ErrorIs(t, err, ssss.ErrUnverifiableKey) - assert.NotNil(t, key) - assert.Equal(t, key2RecoveryKey, key.RecoveryKey()) -} - func TestKeyMetadata_VerifyRecoveryKey_Invalid(t *testing.T) { km := getKeyMeta(key1Meta) key, err := km.VerifyRecoveryKey(key1ID, "foo") - assert.ErrorIs(t, err, ssss.ErrInvalidRecoveryKey) + assert.True(t, errors.Is(err, ssss.ErrInvalidRecoveryKey), "unexpected error: %v", err) assert.Nil(t, key) } func TestKeyMetadata_VerifyRecoveryKey_Incorrect(t *testing.T) { km := getKeyMeta(key1Meta) key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey) - assert.ErrorIs(t, err, ssss.ErrIncorrectSSSSKey) + assert.True(t, errors.Is(err, ssss.ErrIncorrectSSSSKey), "unexpected error: %v", err) assert.Nil(t, key) } @@ -148,27 +119,27 @@ func TestKeyMetadata_VerifyPassphrase_Correct(t *testing.T) { func TestKeyMetadata_VerifyPassphrase_Incorrect(t *testing.T) { km := getKeyMeta(key1Meta) key, err := km.VerifyPassphrase(key1ID, "incorrect horse battery staple") - assert.ErrorIs(t, err, ssss.ErrIncorrectSSSSKey) + assert.True(t, errors.Is(err, ssss.ErrIncorrectSSSSKey), "unexpected error %v", err) assert.Nil(t, key) } func TestKeyMetadata_VerifyPassphrase_NotSet(t *testing.T) { km := getKeyMeta(key2Meta) key, err := km.VerifyPassphrase(key2ID, "hmm") - assert.ErrorIs(t, err, ssss.ErrNoPassphrase) + assert.True(t, errors.Is(err, ssss.ErrNoPassphrase), "unexpected error %v", err) assert.Nil(t, key) } func TestKeyMetadata_VerifyRecoveryKey_CorruptedIV(t *testing.T) { km := getKeyMeta(key2MetaBrokenIV) key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey) - assert.ErrorIs(t, err, ssss.ErrCorruptedKeyMetadata) + assert.True(t, errors.Is(err, ssss.ErrCorruptedKeyMetadata), "unexpected error %v", err) assert.Nil(t, key) } func TestKeyMetadata_VerifyRecoveryKey_CorruptedMAC(t *testing.T) { km := getKeyMeta(key2MetaBrokenMAC) key, err := km.VerifyRecoveryKey(key2ID, key2RecoveryKey) - assert.ErrorIs(t, err, ssss.ErrCorruptedKeyMetadata) + assert.True(t, errors.Is(err, ssss.ErrCorruptedKeyMetadata), "unexpected error %v", err) assert.Nil(t, key) } diff --git a/crypto/ssss/types.go b/crypto/ssss/types.go index b7465d3e..c08f107c 100644 --- a/crypto/ssss/types.go +++ b/crypto/ssss/types.go @@ -26,8 +26,7 @@ var ( ErrUnsupportedPassphraseAlgorithm = errors.New("unsupported passphrase KDF algorithm") ErrIncorrectSSSSKey = errors.New("incorrect SSSS key") ErrInvalidRecoveryKey = errors.New("invalid recovery key") - ErrCorruptedKeyMetadata = errors.New("corrupted recovery key metadata") - ErrUnverifiableKey = errors.New("cannot verify recovery key: missing MAC or IV in metadata") + ErrCorruptedKeyMetadata = errors.New("corrupted key metadata") ) // Algorithm is the identifier for an SSSS encryption algorithm. diff --git a/error.go b/error.go index 4711b3dc..5ff671e0 100644 --- a/error.go +++ b/error.go @@ -140,10 +140,7 @@ type RespError struct { Err string ExtraData map[string]any - StatusCode int - ExtraHeader map[string]string - - CanRetry bool + StatusCode int } func (e *RespError) UnmarshalJSON(data []byte) error { @@ -153,7 +150,6 @@ func (e *RespError) UnmarshalJSON(data []byte) error { } e.ErrCode, _ = e.ExtraData["errcode"].(string) e.Err, _ = e.ExtraData["error"].(string) - e.CanRetry, _ = e.ExtraData["com.beeper.can_retry"].(bool) return nil } @@ -161,9 +157,6 @@ func (e *RespError) MarshalJSON() ([]byte, error) { data := exmaps.NonNilClone(e.ExtraData) data["errcode"] = e.ErrCode data["error"] = e.Err - if e.CanRetry { - data["com.beeper.can_retry"] = e.CanRetry - } return json.Marshal(data) } @@ -175,9 +168,6 @@ func (e RespError) Write(w http.ResponseWriter) { if statusCode == 0 { statusCode = http.StatusInternalServerError } - for key, value := range e.ExtraHeader { - w.Header().Set(key, value) - } exhttp.WriteJSONResponse(w, statusCode, &e) } @@ -194,29 +184,12 @@ func (e RespError) WithStatus(status int) RespError { return e } -func (e RespError) WithCanRetry(canRetry bool) RespError { - e.CanRetry = canRetry - return e -} - func (e RespError) WithExtraData(extraData map[string]any) RespError { e.ExtraData = exmaps.NonNilClone(e.ExtraData) maps.Copy(e.ExtraData, extraData) return e } -func (e RespError) WithExtraHeader(key, value string) RespError { - e.ExtraHeader = exmaps.NonNilClone(e.ExtraHeader) - e.ExtraHeader[key] = value - return e -} - -func (e RespError) WithExtraHeaders(headers map[string]string) RespError { - e.ExtraHeader = exmaps.NonNilClone(e.ExtraHeader) - maps.Copy(e.ExtraHeader, headers) - return e -} - // Error returns the errcode and error message. func (e RespError) Error() string { return e.ErrCode + ": " + e.Err diff --git a/event/beeper.go b/event/beeper.go index a1a60b35..b46106ab 100644 --- a/event/beeper.go +++ b/event/beeper.go @@ -94,8 +94,6 @@ type BeeperChatDeleteEventContent struct { } type BeeperAcceptMessageRequestEventContent struct { - // Whether this was triggered by a message rather than an explicit event - IsImplicit bool `json:"-"` } type BeeperSendStateEventContent struct { @@ -146,7 +144,7 @@ type BeeperLinkPreview struct { MatchedURL string `json:"matched_url,omitempty"` ImageEncryption *EncryptedFileInfo `json:"beeper:image:encryption,omitempty"` - ImageBlurhash string `json:"matrix:image:blurhash,omitempty"` + ImageBlurhash string `json:"beeper:image:blurhash,omitempty"` } type BeeperProfileExtra struct { @@ -166,24 +164,6 @@ type BeeperPerMessageProfile struct { HasFallback bool `json:"has_fallback,omitempty"` } -type BeeperActionMessageType string - -const ( - BeeperActionMessageCall BeeperActionMessageType = "call" -) - -type BeeperActionMessageCallType string - -const ( - BeeperActionMessageCallTypeVoice BeeperActionMessageCallType = "voice" - BeeperActionMessageCallTypeVideo BeeperActionMessageCallType = "video" -) - -type BeeperActionMessage struct { - Type BeeperActionMessageType `json:"type"` - CallType BeeperActionMessageCallType `json:"call_type,omitempty"` -} - func (content *MessageEventContent) AddPerMessageProfileFallback() { if content.BeeperPerMessageProfile == nil || content.BeeperPerMessageProfile.HasFallback || content.BeeperPerMessageProfile.Displayname == "" { return @@ -214,15 +194,6 @@ func (content *MessageEventContent) RemovePerMessageProfileFallback() { } } -type BeeperAIStreamEventContent struct { - TurnID string `json:"turn_id"` - Seq int `json:"seq"` - Part map[string]any `json:"part"` - TargetEvent id.EventID `json:"target_event,omitempty"` - AgentID string `json:"agent_id,omitempty"` - RelatesTo *RelatesTo `json:"m.relates_to,omitempty"` -} - type BeeperEncodedOrder struct { order int64 suborder int16 diff --git a/event/content.go b/event/content.go index 814aeec4..d1ced268 100644 --- a/event/content.go +++ b/event/content.go @@ -40,9 +40,6 @@ var TypeMap = map[Type]reflect.Type{ StateSpaceParent: reflect.TypeOf(SpaceParentEventContent{}), StateSpaceChild: reflect.TypeOf(SpaceChildEventContent{}), - StateRoomPolicy: reflect.TypeOf(RoomPolicyEventContent{}), - StateUnstableRoomPolicy: reflect.TypeOf(RoomPolicyEventContent{}), - StateLegacyPolicyRoom: reflect.TypeOf(ModPolicyContent{}), StateLegacyPolicyServer: reflect.TypeOf(ModPolicyContent{}), StateLegacyPolicyUser: reflect.TypeOf(ModPolicyContent{}), @@ -76,11 +73,9 @@ var TypeMap = map[Type]reflect.Type{ AccountDataMarkedUnread: reflect.TypeOf(MarkedUnreadEventContent{}), AccountDataBeeperMute: reflect.TypeOf(BeeperMuteEventContent{}), - EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}), - EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}), - EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}), - EphemeralEventEncrypted: reflect.TypeOf(EncryptedEventContent{}), - BeeperEphemeralEventAIStream: reflect.TypeOf(BeeperAIStreamEventContent{}), + EphemeralEventTyping: reflect.TypeOf(TypingEventContent{}), + EphemeralEventReceipt: reflect.TypeOf(ReceiptEventContent{}), + EphemeralEventPresence: reflect.TypeOf(PresenceEventContent{}), InRoomVerificationReady: reflect.TypeOf(VerificationReadyEventContent{}), InRoomVerificationStart: reflect.TypeOf(VerificationStartEventContent{}), diff --git a/event/encryption.go b/event/encryption.go index c60cb91a..e07944af 100644 --- a/event/encryption.go +++ b/event/encryption.go @@ -63,7 +63,7 @@ func (content *EncryptedEventContent) UnmarshalJSON(data []byte) error { return json.Unmarshal(content.Ciphertext, &content.OlmCiphertext) case id.AlgorithmMegolmV1: if len(content.Ciphertext) == 0 || content.Ciphertext[0] != '"' || content.Ciphertext[len(content.Ciphertext)-1] != '"' { - return fmt.Errorf("ciphertext %w", id.ErrInputNotJSONString) + return id.ErrInputNotJSONString } content.MegolmCiphertext = content.Ciphertext[1 : len(content.Ciphertext)-1] } @@ -132,9 +132,8 @@ type RoomKeyRequestEventContent struct { type RequestedKeyInfo struct { Algorithm id.Algorithm `json:"algorithm"` RoomID id.RoomID `json:"room_id"` - SessionID id.SessionID `json:"session_id"` - // Deprecated: Matrix v1.3 SenderKey id.SenderKey `json:"sender_key"` + SessionID id.SessionID `json:"session_id"` } type RoomKeyWithheldCode string diff --git a/event/message.go b/event/message.go index 3fb3dc82..5e80d2ef 100644 --- a/event/message.go +++ b/event/message.go @@ -135,7 +135,6 @@ type MessageEventContent struct { BeeperGalleryCaption string `json:"com.beeper.gallery.caption,omitempty"` BeeperGalleryCaptionHTML string `json:"com.beeper.gallery.caption_html,omitempty"` BeeperPerMessageProfile *BeeperPerMessageProfile `json:"com.beeper.per_message_profile,omitempty"` - BeeperActionMessage *BeeperActionMessage `json:"com.beeper.action_message,omitempty"` BeeperLinkPreviews []*BeeperLinkPreview `json:"com.beeper.linkpreviews,omitempty"` diff --git a/event/powerlevels.go b/event/powerlevels.go index 668eb6d3..708721f9 100644 --- a/event/powerlevels.go +++ b/event/powerlevels.go @@ -28,9 +28,6 @@ type PowerLevelsEventContent struct { Events map[string]int `json:"events,omitempty"` EventsDefault int `json:"events_default,omitempty"` - beeperEphemeralLock sync.RWMutex - BeeperEphemeral map[string]int `json:"com.beeper.ephemeral,omitempty"` - Notifications *NotificationPowerLevels `json:"notifications,omitempty"` StateDefaultPtr *int `json:"state_default,omitempty"` @@ -40,8 +37,6 @@ type PowerLevelsEventContent struct { BanPtr *int `json:"ban,omitempty"` RedactPtr *int `json:"redact,omitempty"` - BeeperEphemeralDefaultPtr *int `json:"com.beeper.ephemeral_default,omitempty"` - // This is not a part of power levels, it's added by mautrix-go internally in certain places // in order to detect creator power accurately. CreateEvent *Event `json:"-"` @@ -56,7 +51,6 @@ func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent { UsersDefault: pl.UsersDefault, Events: maps.Clone(pl.Events), EventsDefault: pl.EventsDefault, - BeeperEphemeral: maps.Clone(pl.BeeperEphemeral), StateDefaultPtr: ptr.Clone(pl.StateDefaultPtr), Notifications: pl.Notifications.Clone(), @@ -66,8 +60,6 @@ func (pl *PowerLevelsEventContent) Clone() *PowerLevelsEventContent { BanPtr: ptr.Clone(pl.BanPtr), RedactPtr: ptr.Clone(pl.RedactPtr), - BeeperEphemeralDefaultPtr: ptr.Clone(pl.BeeperEphemeralDefaultPtr), - CreateEvent: pl.CreateEvent, } } @@ -127,13 +119,6 @@ func (pl *PowerLevelsEventContent) StateDefault() int { return 50 } -func (pl *PowerLevelsEventContent) BeeperEphemeralDefault() int { - if pl.BeeperEphemeralDefaultPtr != nil { - return *pl.BeeperEphemeralDefaultPtr - } - return pl.EventsDefault -} - func (pl *PowerLevelsEventContent) GetUserLevel(userID id.UserID) int { if pl.isCreator(userID) { return math.MaxInt @@ -217,29 +202,6 @@ func (pl *PowerLevelsEventContent) GetEventLevel(eventType Type) int { return level } -func (pl *PowerLevelsEventContent) GetBeeperEphemeralLevel(eventType Type) int { - pl.beeperEphemeralLock.RLock() - defer pl.beeperEphemeralLock.RUnlock() - level, ok := pl.BeeperEphemeral[eventType.String()] - if !ok { - return pl.BeeperEphemeralDefault() - } - return level -} - -func (pl *PowerLevelsEventContent) SetBeeperEphemeralLevel(eventType Type, level int) { - pl.beeperEphemeralLock.Lock() - defer pl.beeperEphemeralLock.Unlock() - if level == pl.BeeperEphemeralDefault() { - delete(pl.BeeperEphemeral, eventType.String()) - } else { - if pl.BeeperEphemeral == nil { - pl.BeeperEphemeral = make(map[string]int) - } - pl.BeeperEphemeral[eventType.String()] = level - } -} - func (pl *PowerLevelsEventContent) SetEventLevel(eventType Type, level int) { pl.eventsLock.Lock() defer pl.eventsLock.Unlock() diff --git a/event/powerlevels_ephemeral_test.go b/event/powerlevels_ephemeral_test.go deleted file mode 100644 index f5861583..00000000 --- a/event/powerlevels_ephemeral_test.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2026 Tulir Asokan -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -package event_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "maunium.net/go/mautrix/event" -) - -func TestPowerLevelsEventContent_BeeperEphemeralDefaultFallsBackToEventsDefault(t *testing.T) { - pl := &event.PowerLevelsEventContent{ - EventsDefault: 45, - } - - assert.Equal(t, 45, pl.BeeperEphemeralDefault()) - - override := 60 - pl.BeeperEphemeralDefaultPtr = &override - assert.Equal(t, 60, pl.BeeperEphemeralDefault()) -} - -func TestPowerLevelsEventContent_GetSetBeeperEphemeralLevel(t *testing.T) { - pl := &event.PowerLevelsEventContent{ - EventsDefault: 25, - } - evtType := event.Type{Type: "com.example.ephemeral", Class: event.EphemeralEventType} - - assert.Equal(t, 25, pl.GetBeeperEphemeralLevel(evtType)) - - pl.SetBeeperEphemeralLevel(evtType, 50) - assert.Equal(t, 50, pl.GetBeeperEphemeralLevel(evtType)) - require.NotNil(t, pl.BeeperEphemeral) - assert.Equal(t, 50, pl.BeeperEphemeral[evtType.String()]) - - pl.SetBeeperEphemeralLevel(evtType, 25) - _, exists := pl.BeeperEphemeral[evtType.String()] - assert.False(t, exists) -} - -func TestPowerLevelsEventContent_CloneCopiesBeeperEphemeralFields(t *testing.T) { - override := 70 - pl := &event.PowerLevelsEventContent{ - EventsDefault: 35, - BeeperEphemeral: map[string]int{"com.example.ephemeral": 90}, - BeeperEphemeralDefaultPtr: &override, - } - - cloned := pl.Clone() - require.NotNil(t, cloned) - require.NotNil(t, cloned.BeeperEphemeralDefaultPtr) - assert.Equal(t, 70, *cloned.BeeperEphemeralDefaultPtr) - assert.Equal(t, 90, cloned.BeeperEphemeral["com.example.ephemeral"]) - - cloned.BeeperEphemeral["com.example.ephemeral"] = 99 - *cloned.BeeperEphemeralDefaultPtr = 71 - - assert.Equal(t, 90, pl.BeeperEphemeral["com.example.ephemeral"]) - assert.Equal(t, 70, *pl.BeeperEphemeralDefaultPtr) -} diff --git a/event/state.go b/event/state.go index ace170a5..6d027e04 100644 --- a/event/state.go +++ b/event/state.go @@ -343,15 +343,3 @@ func (efmc *ElementFunctionalMembersContent) Add(mxid id.UserID) bool { efmc.ServiceMembers = append(efmc.ServiceMembers, mxid) return true } - -type PolicyServerPublicKeys struct { - Ed25519 id.Ed25519 `json:"ed25519,omitempty"` -} - -type RoomPolicyEventContent struct { - Via string `json:"via,omitempty"` - PublicKeys *PolicyServerPublicKeys `json:"public_keys,omitempty"` - - // Deprecated, only for legacy use - PublicKey id.Ed25519 `json:"public_key,omitempty"` -} diff --git a/event/type.go b/event/type.go index 80b86728..b193dc59 100644 --- a/event/type.go +++ b/event/type.go @@ -113,9 +113,9 @@ func (et *Type) GuessClass() TypeClass { StatePinnedEvents.Type, StateTombstone.Type, StateEncryption.Type, StateBridge.Type, StateHalfShotBridge.Type, StateSpaceParent.Type, StateSpaceChild.Type, StatePolicyRoom.Type, StatePolicyServer.Type, StatePolicyUser.Type, StateElementFunctionalMembers.Type, StateBeeperRoomFeatures.Type, StateBeeperDisappearingTimer.Type, - StateMSC4391BotCommand.Type, StateRoomPolicy.Type, StateUnstableRoomPolicy.Type: + StateMSC4391BotCommand.Type: return StateEventType - case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type, BeeperEphemeralEventAIStream.Type: + case EphemeralEventReceipt.Type, EphemeralEventTyping.Type, EphemeralEventPresence.Type: return EphemeralEventType case AccountDataDirectChats.Type, AccountDataPushRules.Type, AccountDataRoomTags.Type, AccountDataFullyRead.Type, AccountDataIgnoredUserList.Type, AccountDataMarkedUnread.Type, @@ -195,9 +195,6 @@ var ( StateSpaceChild = Type{"m.space.child", StateEventType} StateSpaceParent = Type{"m.space.parent", StateEventType} - StateRoomPolicy = Type{"m.room.policy", StateEventType} - StateUnstableRoomPolicy = Type{"org.matrix.msc4284.policy", StateEventType} - StateLegacyPolicyRoom = Type{"m.room.rule.room", StateEventType} StateLegacyPolicyServer = Type{"m.room.rule.server", StateEventType} StateLegacyPolicyUser = Type{"m.room.rule.user", StateEventType} @@ -250,11 +247,9 @@ var ( // Ephemeral events var ( - EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType} - EphemeralEventTyping = Type{"m.typing", EphemeralEventType} - EphemeralEventPresence = Type{"m.presence", EphemeralEventType} - EphemeralEventEncrypted = Type{"m.room.encrypted", EphemeralEventType} - BeeperEphemeralEventAIStream = Type{"com.beeper.ai.stream_event", EphemeralEventType} + EphemeralEventReceipt = Type{"m.receipt", EphemeralEventType} + EphemeralEventTyping = Type{"m.typing", EphemeralEventType} + EphemeralEventPresence = Type{"m.presence", EphemeralEventType} ) // Account data events diff --git a/federation/eventauth/eventauth.go b/federation/eventauth/eventauth.go index c72933c2..eac110a3 100644 --- a/federation/eventauth/eventauth.go +++ b/federation/eventauth/eventauth.go @@ -505,7 +505,7 @@ func authorizeMember(roomVersion id.RoomVersion, evt, createEvt *pdu.PDU, authEv // 5.5.5. Otherwise, reject. return ErrInsufficientPermissionForKick case event.MembershipBan: - if senderMembership != event.MembershipJoin { + if senderMembership != event.MembershipLeave { // 5.6.1. If the sender’s current membership state is not join, reject. return ErrCantBanWithoutBeingInRoom } diff --git a/federation/eventauth/eventauth_internal_test.go b/federation/eventauth/eventauth_internal_test.go deleted file mode 100644 index d316f3c8..00000000 --- a/federation/eventauth/eventauth_internal_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) 2026 Tulir Asokan -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this -// file, You can obtain one at http://mozilla.org/MPL/2.0/. - -//go:build goexperiment.jsonv2 - -package eventauth - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tidwall/gjson" -) - -type pythonIntTest struct { - Name string - Input string - Expected int64 -} - -var pythonIntTests = []pythonIntTest{ - {"True", `true`, 1}, - {"False", `false`, 0}, - {"SmallFloat", `3.1415`, 3}, - {"SmallFloatRoundDown", `10.999999999999999`, 10}, - {"SmallFloatRoundUp", `10.9999999999999999`, 11}, - {"BigFloatRoundDown", `1000000.9999999999`, 1000000}, - {"BigFloatRoundUp", `1000000.99999999999`, 1000001}, - {"BigFloatPrecisionError", `9007199254740993.0`, 9007199254740992}, - {"BigFloatPrecisionError2", `9007199254740993.123`, 9007199254740994}, - {"Int64", `9223372036854775807`, 9223372036854775807}, - {"Int64String", `"9223372036854775807"`, 9223372036854775807}, - {"String", `"123"`, 123}, - {"InvalidFloatInString", `"123.456"`, 0}, - {"StringWithPlusSign", `"+123"`, 123}, - {"StringWithMinusSign", `"-123"`, -123}, - {"StringWithSpaces", `" 123 "`, 123}, - {"StringWithSpacesAndSign", `" -123 "`, -123}, - //{"StringWithUnderscores", `"123_456"`, 123456}, - //{"StringWithUnderscores", `"123_456"`, 123456}, - {"InvalidStringWithTrailingUnderscore", `"123_456_"`, 0}, - {"InvalidStringWithMultipleUnderscores", `"123__456"`, 0}, - {"InvalidStringWithLeadingUnderscore", `"_123_456"`, 0}, - {"InvalidStringWithUnderscoreAfterSign", `"+_123_456"`, 0}, - {"InvalidStringWithUnderscoreAfterSpace", `" _123_456"`, 0}, - //{"StringWithUnderscoresAndSpaces", `" +1_2_3_4_5_6 "`, 123456}, -} - -func TestParsePythonInt(t *testing.T) { - for _, test := range pythonIntTests { - t.Run(test.Name, func(t *testing.T) { - output := parsePythonInt(gjson.Parse(test.Input)) - if strings.HasPrefix(test.Name, "Invalid") { - assert.Nil(t, output) - } else { - require.NotNil(t, output) - assert.Equal(t, int(test.Expected), *output) - } - }) - } -} diff --git a/federation/pdu/pdu.go b/federation/pdu/pdu.go index 17db6995..cecee5b9 100644 --- a/federation/pdu/pdu.go +++ b/federation/pdu/pdu.go @@ -123,19 +123,6 @@ func (pdu *PDU) ToClientEvent(roomVersion id.RoomVersion) (*event.Event, error) return evt, nil } -func (pdu *PDU) AddSignature(serverName string, keyID id.KeyID, signature string) { - if signature == "" { - return - } - if pdu.Signatures == nil { - pdu.Signatures = make(map[string]map[id.KeyID]string) - } - if _, ok := pdu.Signatures[serverName]; !ok { - pdu.Signatures[serverName] = make(map[id.KeyID]string) - } - pdu.Signatures[serverName][keyID] = signature -} - func marshalCanonical(data any) (jsontext.Value, error) { marshaledBytes, err := json.Marshal(data) if err != nil { diff --git a/federation/pdu/signature.go b/federation/pdu/signature.go index 04e7c5ef..a7685cc6 100644 --- a/federation/pdu/signature.go +++ b/federation/pdu/signature.go @@ -28,7 +28,13 @@ func (pdu *PDU) Sign(roomVersion id.RoomVersion, serverName string, keyID id.Key return fmt.Errorf("failed to marshal redacted PDU to sign: %w", err) } signature := ed25519.Sign(privateKey, rawJSON) - pdu.AddSignature(serverName, keyID, base64.RawStdEncoding.EncodeToString(signature)) + if pdu.Signatures == nil { + pdu.Signatures = make(map[string]map[id.KeyID]string) + } + if _, ok := pdu.Signatures[serverName]; !ok { + pdu.Signatures[serverName] = make(map[id.KeyID]string) + } + pdu.Signatures[serverName][keyID] = base64.RawStdEncoding.EncodeToString(signature) return nil } diff --git a/format/htmlparser.go b/format/htmlparser.go index e0507d93..e5f92896 100644 --- a/format/htmlparser.go +++ b/format/htmlparser.go @@ -93,30 +93,6 @@ func DefaultPillConverter(displayname, mxid, eventID string, ctx Context) string } } -func onlyBacktickCount(line string) (count int) { - for i := 0; i < len(line); i++ { - if line[i] != '`' { - return -1 - } - count++ - } - return -} - -func DefaultMonospaceBlockConverter(code, language string, ctx Context) string { - if len(code) == 0 || code[len(code)-1] != '\n' { - code += "\n" - } - fence := "```" - for line := range strings.SplitSeq(code, "\n") { - count := onlyBacktickCount(strings.TrimSpace(line)) - if count >= len(fence) { - fence = strings.Repeat("`", count+1) - } - } - return fmt.Sprintf("%s%s\n%s%s", fence, language, code, fence) -} - // HTMLParser is a somewhat customizable Matrix HTML parser. type HTMLParser struct { PillConverter PillConverter @@ -372,7 +348,10 @@ func (parser *HTMLParser) tagToString(node *html.Node, ctx Context) string { if parser.MonospaceBlockConverter != nil { return parser.MonospaceBlockConverter(preStr, language, ctx) } - return DefaultMonospaceBlockConverter(preStr, language, ctx) + if len(preStr) == 0 || preStr[len(preStr)-1] != '\n' { + preStr += "\n" + } + return fmt.Sprintf("```%s\n%s```", language, preStr) default: return parser.nodeToTagAwareString(node.FirstChild, ctx) } diff --git a/go.mod b/go.mod index 49a1d4e4..27acd21b 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ module maunium.net/go/mautrix -go 1.25.0 +go 1.24.0 -toolchain go1.26.0 +toolchain go1.25.6 require ( - filippo.io/edwards25519 v1.2.0 + filippo.io/edwards25519 v1.1.0 github.com/chzyer/readline v1.5.1 github.com/coder/websocket v1.8.14 - github.com/lib/pq v1.11.2 - github.com/mattn/go-sqlite3 v1.14.34 + github.com/lib/pq v1.10.9 + github.com/mattn/go-sqlite3 v1.14.33 github.com/rs/xid v1.6.0 github.com/rs/zerolog v1.34.0 github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e @@ -17,11 +17,11 @@ require ( github.com/tidwall/gjson v1.18.0 github.com/tidwall/sjson v1.2.5 github.com/yuin/goldmark v1.7.16 - go.mau.fi/util v0.9.6 + go.mau.fi/util v0.9.5 go.mau.fi/zeroconfig v0.2.0 - golang.org/x/crypto v0.48.0 - golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa - golang.org/x/net v0.50.0 + golang.org/x/crypto v0.47.0 + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 + golang.org/x/net v0.49.0 golang.org/x/sync v0.19.0 gopkg.in/yaml.v3 v3.0.1 maunium.net/go/mauflag v1.0.0 @@ -36,7 +36,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - golang.org/x/sys v0.41.0 // indirect - golang.org/x/text v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect ) diff --git a/go.sum b/go.sum index 871a5156..9702337a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= @@ -16,8 +16,8 @@ github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= -github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= @@ -25,8 +25,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= -github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= +github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -52,26 +52,26 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts= -go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI= +go.mau.fi/util v0.9.5 h1:7AoWPCIZJGv4jvtFEuCe3GhAbI7uF9ckIooaXvwlIR4= +go.mau.fi/util v0.9.5/go.mod h1:g1uvZ03VQhtTt2BgaRGVytS/Zj67NV0YNIECch0sQCQ= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= -golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= -golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa h1:Zt3DZoOFFYkKhDT3v7Lm9FDMEV06GpzjG2jrqW+QTE0= -golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= -golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= -golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= -golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= -golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/id/contenturi.go b/id/contenturi.go index 67127b6c..be45eb2b 100644 --- a/id/contenturi.go +++ b/id/contenturi.go @@ -92,7 +92,7 @@ func (uri *ContentURI) UnmarshalJSON(raw []byte) (err error) { *uri = ContentURI{} return nil } else if len(raw) < 2 || raw[0] != '"' || raw[len(raw)-1] != '"' { - return fmt.Errorf("ContentURI: %w", ErrInputNotJSONString) + return ErrInputNotJSONString } parsed, err := ParseContentURIBytes(raw[1 : len(raw)-1]) if err != nil { diff --git a/id/crypto.go b/id/crypto.go index ee857f78..355a84a8 100644 --- a/id/crypto.go +++ b/id/crypto.go @@ -53,34 +53,6 @@ const ( KeyBackupAlgorithmMegolmBackupV1 KeyBackupAlgorithm = "m.megolm_backup.v1.curve25519-aes-sha2" ) -type KeySource string - -func (source KeySource) String() string { - return string(source) -} - -func (source KeySource) Int() int { - switch source { - case KeySourceDirect: - return 100 - case KeySourceBackup: - return 90 - case KeySourceImport: - return 80 - case KeySourceForward: - return 50 - default: - return 0 - } -} - -const ( - KeySourceDirect KeySource = "direct" - KeySourceBackup KeySource = "backup" - KeySourceImport KeySource = "import" - KeySourceForward KeySource = "forward" -) - // BackupVersion is an arbitrary string that identifies a server side key backup. type KeyBackupVersion string diff --git a/id/trust.go b/id/trust.go index 6255093e..04f6e36b 100644 --- a/id/trust.go +++ b/id/trust.go @@ -16,7 +16,6 @@ type TrustState int const ( TrustStateBlacklisted TrustState = -100 - TrustStateDeviceKeyMismatch TrustState = -5 TrustStateUnset TrustState = 0 TrustStateUnknownDevice TrustState = 10 TrustStateForwarded TrustState = 20 @@ -24,7 +23,7 @@ const ( TrustStateCrossSignedTOFU TrustState = 100 TrustStateCrossSignedVerified TrustState = 200 TrustStateVerified TrustState = 300 - TrustStateInvalid TrustState = -2147483647 + TrustStateInvalid TrustState = (1 << 31) - 1 ) func (ts *TrustState) UnmarshalText(data []byte) error { @@ -45,8 +44,6 @@ func ParseTrustState(val string) TrustState { switch strings.ToLower(val) { case "blacklisted": return TrustStateBlacklisted - case "device-key-mismatch": - return TrustStateDeviceKeyMismatch case "unverified": return TrustStateUnset case "cross-signed-untrusted": @@ -70,8 +67,6 @@ func (ts TrustState) String() string { switch ts { case TrustStateBlacklisted: return "blacklisted" - case TrustStateDeviceKeyMismatch: - return "device-key-mismatch" case TrustStateUnset: return "unverified" case TrustStateCrossSignedUntrusted: diff --git a/mediaproxy/mediaproxy.go b/mediaproxy/mediaproxy.go index 4d2bc7cf..2063675a 100644 --- a/mediaproxy/mediaproxy.go +++ b/mediaproxy/mediaproxy.go @@ -143,7 +143,6 @@ func New(serverName string, serverKey string, getMedia GetMediaFunc) (*MediaProx } mp.FederationRouter = http.NewServeMux() mp.FederationRouter.HandleFunc("GET /v1/media/download/{mediaID}", mp.DownloadMediaFederation) - mp.FederationRouter.HandleFunc("GET /v1/media/thumbnail/{mediaID}", mp.DownloadMediaFederation) mp.FederationRouter.HandleFunc("GET /v1/version", mp.KeyServer.GetServerVersion) mp.ClientMediaRouter = http.NewServeMux() mp.ClientMediaRouter.HandleFunc("GET /download/{serverName}/{mediaID}", mp.DownloadMedia) diff --git a/mockserver/mockserver.go b/mockserver/mockserver.go index 507c24a5..e52c387a 100644 --- a/mockserver/mockserver.go +++ b/mockserver/mockserver.go @@ -231,7 +231,7 @@ func (ms *MockServer) postKeysUpload(w http.ResponseWriter, r *http.Request) { } func (ms *MockServer) postDeviceSigningUpload(w http.ResponseWriter, r *http.Request) { - var req mautrix.UploadCrossSigningKeysReq[any] + var req mautrix.UploadCrossSigningKeysReq mustDecode(r, &req) userID := ms.getUserID(r).UserID diff --git a/requests.go b/requests.go index cc8b7266..f0287b3c 100644 --- a/requests.go +++ b/requests.go @@ -66,14 +66,14 @@ const ( ) // ReqRegister is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3register -type ReqRegister[UIAType any] struct { +type ReqRegister struct { Username string `json:"username,omitempty"` Password string `json:"password,omitempty"` DeviceID id.DeviceID `json:"device_id,omitempty"` InitialDeviceDisplayName string `json:"initial_device_display_name,omitempty"` InhibitLogin bool `json:"inhibit_login,omitempty"` RefreshToken bool `json:"refresh_token,omitempty"` - Auth UIAType `json:"auth,omitempty"` + Auth interface{} `json:"auth,omitempty"` // Type for registration, only used for appservice user registrations // https://spec.matrix.org/v1.2/application-service-api/#server-admin-style-permissions @@ -320,11 +320,11 @@ func (csk *CrossSigningKeys) FirstKey() id.Ed25519 { return "" } -type UploadCrossSigningKeysReq[UIAType any] struct { +type UploadCrossSigningKeysReq struct { Master CrossSigningKeys `json:"master_key"` SelfSigning CrossSigningKeys `json:"self_signing_key"` UserSigning CrossSigningKeys `json:"user_signing_key"` - Auth UIAType `json:"auth,omitempty"` + Auth interface{} `json:"auth,omitempty"` } type KeyMap map[id.DeviceKeyID]string @@ -367,12 +367,13 @@ type ReqSendToDevice struct { } type ReqSendEvent struct { - Timestamp int64 - TransactionID string - UnstableDelay time.Duration - UnstableStickyDuration time.Duration - DontEncrypt bool - MeowEventID id.EventID + Timestamp int64 + TransactionID string + UnstableDelay time.Duration + + DontEncrypt bool + + MeowEventID id.EventID } type ReqDelayedEvents struct { @@ -392,14 +393,14 @@ type ReqDeviceInfo struct { } // ReqDeleteDevice is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#delete_matrixclientv3devicesdeviceid -type ReqDeleteDevice[UIAType any] struct { - Auth UIAType `json:"auth,omitempty"` +type ReqDeleteDevice struct { + Auth interface{} `json:"auth,omitempty"` } // ReqDeleteDevices is the JSON request for https://spec.matrix.org/v1.2/client-server-api/#post_matrixclientv3delete_devices -type ReqDeleteDevices[UIAType any] struct { +type ReqDeleteDevices struct { Devices []id.DeviceID `json:"devices"` - Auth UIAType `json:"auth,omitempty"` + Auth interface{} `json:"auth,omitempty"` } type ReqPutPushRule struct { diff --git a/responses.go b/responses.go index 4fbe1fbc..d822c84b 100644 --- a/responses.go +++ b/responses.go @@ -258,7 +258,6 @@ func (r *UserDirectoryEntry) MarshalJSON() ([]byte, error) { type RespMutualRooms struct { Joined []id.RoomID `json:"joined"` NextBatch string `json:"next_batch,omitempty"` - Count int `json:"count,omitempty"` } type RespRoomSummary struct { @@ -342,13 +341,6 @@ type LazyLoadSummary struct { InvitedMemberCount *int `json:"m.invited_member_count,omitempty"` } -func (lls *LazyLoadSummary) MemberCount() int { - if lls == nil { - return 0 - } - return ptr.Val(lls.JoinedMemberCount) + ptr.Val(lls.InvitedMemberCount) -} - func (lls *LazyLoadSummary) Equal(other *LazyLoadSummary) bool { if lls == other { return true diff --git a/version.go b/version.go index f00bbf39..b0e31c7e 100644 --- a/version.go +++ b/version.go @@ -8,7 +8,7 @@ import ( "strings" ) -const Version = "v0.26.3" +const Version = "v0.26.2" var GoModVersion = "" var Commit = "" diff --git a/versions.go b/versions.go index 61b2e4ea..8ae82a06 100644 --- a/versions.go +++ b/versions.go @@ -63,8 +63,7 @@ var ( FeatureAsyncUploads = UnstableFeature{UnstableFlag: "fi.mau.msc2246.stable", SpecVersion: SpecV17} FeatureAppservicePing = UnstableFeature{UnstableFlag: "fi.mau.msc2659.stable", SpecVersion: SpecV17} FeatureAuthenticatedMedia = UnstableFeature{UnstableFlag: "org.matrix.msc3916.stable", SpecVersion: SpecV111} - FeatureUnstableMutualRooms = UnstableFeature{UnstableFlag: "uk.half-shot.msc2666.query_mutual_rooms"} - FeatureStableMutualRooms = UnstableFeature{UnstableFlag: "uk.half-shot.msc2666.query_mutual_rooms.stable" /*, SpecVersion: SpecV118*/} + FeatureMutualRooms = UnstableFeature{UnstableFlag: "uk.half-shot.msc2666.query_mutual_rooms"} FeatureUserRedaction = UnstableFeature{UnstableFlag: "org.matrix.msc4194"} FeatureViewRedactedContent = UnstableFeature{UnstableFlag: "fi.mau.msc2815"} FeatureUnstableAccountModeration = UnstableFeature{UnstableFlag: "uk.timedout.msc4323"} @@ -81,7 +80,6 @@ var ( BeeperFeatureAccountDataMute = UnstableFeature{UnstableFlag: "com.beeper.account_data_mute"} BeeperFeatureInboxState = UnstableFeature{UnstableFlag: "com.beeper.inbox_state"} BeeperFeatureArbitraryMemberChange = UnstableFeature{UnstableFlag: "com.beeper.arbitrary_member_change"} - BeeperFeatureEphemeralEvents = UnstableFeature{UnstableFlag: "com.beeper.ephemeral"} ) func (versions *RespVersions) Supports(feature UnstableFeature) bool {