diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c8e541..5059f2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All user-visible bugs and enhancements should be recorded here. ## Unreleased +### Fixed + +- `xurl chat add-members` no longer fails with "Invalid action signature" on a group whose message timer (`message_ttl_ms`) or screen-capture blocking setting (`screen_capture_blocking_enabled`) has been set. The signed group state left both out; it now carries the values from the conversation. + ## v1.3.2 - 2026-09-23 ### Fixed diff --git a/cli/chat.go b/cli/chat.go index d92f7d3..790964c 100644 --- a/cli/chat.go +++ b/cli/chat.go @@ -1636,13 +1636,15 @@ func (s *chatSession) addGroupMembers(conversationID string, newMembers []string return err } prepared, err := s.chat.PrepareGroupMembersChange(chatxdk.GroupMembersChangeParams{ - PublicKeys: inputs, - ConversationID: api.ChatConversationEventID(conversationID), - NewMemberIDs: newIDs, - CurrentMemberIDs: meta.MemberIDs, - CurrentAdminIDs: meta.AdminIDs, - CurrentTitle: meta.GroupName, - CurrentAvatarURL: meta.GroupAvatarURL, + PublicKeys: inputs, + ConversationID: api.ChatConversationEventID(conversationID), + NewMemberIDs: newIDs, + CurrentMemberIDs: meta.MemberIDs, + CurrentAdminIDs: meta.AdminIDs, + CurrentTitle: meta.GroupName, + CurrentAvatarURL: meta.GroupAvatarURL, + CurrentTTLMsec: meta.MessageTTLMs, + CurrentScreenCaptureBlockingEnabled: meta.ScreenCaptureBlockingEnabled, }) if err != nil { return fmt.Errorf("failed to prepare member change: %w", err) diff --git a/cli/chat_test.go b/cli/chat_test.go index 315c63b..42181ad 100644 --- a/cli/chat_test.go +++ b/cli/chat_test.go @@ -152,6 +152,75 @@ func TestPreparedChangeToRequestShape(t *testing.T) { assert.False(t, has) } +// addMembersStubClient serves the requests addGroupMembers makes and records +// the body it posts. +type addMembersStubClient struct { + api.Client + conversation string + publicKey string + posted string +} + +func (c *addMembersStubClient) SendRequest(opts api.RequestOptions) (json.RawMessage, error) { + switch { + case opts.Method == "GET" && opts.Endpoint == "/2/chat/conversations/g100": + return json.RawMessage(c.conversation), nil + case opts.Method == "GET" && strings.HasSuffix(opts.Endpoint, "/public_keys"): + return json.RawMessage(fmt.Sprintf(`{"data":[{"public_key_version":"1","public_key":%q}]}`, c.publicKey)), nil + case opts.Method == "POST" && opts.Endpoint == "/2/chat/conversations/g100/members": + c.posted = opts.Data + return json.RawMessage(`{"data":{}}`), nil + } + return nil, fmt.Errorf("unexpected request %s %s", opts.Method, opts.Endpoint) +} + +func TestAddGroupMembersSignsGroupSettings(t *testing.T) { + // The member-add signature covers the group's current settings. Its signed + // payload ends with {message_ttl_ms|null},{conversation key version}, + // {screen_capture_blocking_enabled|null}, and must match the conversation. + tests := []struct { + name string + settings string + want string + }{ + {"settings never set", ``, "null,%s,null"}, + {"timer on, screen capture off", `,"message_ttl_ms":86400000,"screen_capture_blocking_enabled":false`, "86400000,%s,false"}, + {"screen capture on", `,"screen_capture_blocking_enabled":true`, "null,%s,true"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + chat := chatxdk.New() + defer chat.Close() + keys, err := chat.GenerateKeypairs() + require.NoError(t, err) + require.NoError(t, chat.SetIdentity("42", "1")) + + client := &addMembersStubClient{ + conversation: `{"data":{"id":"g100","type":"group","member_ids":["42","7"],"admin_ids":["42"]` + tt.settings + `}}`, + publicKey: keys.PublicKey.PublicKey, + } + s := &chatSession{chat: chat, client: client, userID: "42", usernames: map[string]string{}} + require.NoError(t, s.addGroupMembers("g100", []string{"300"}, true)) + + var body struct { + ConversationKeyVersion string `json:"conversation_key_version"` + ActionSignatures []struct { + SignaturePayload string `json:"signature_payload"` + } `json:"action_signatures"` + } + require.NoError(t, json.Unmarshal([]byte(client.posted), &body)) + var memberAdd string + for _, sig := range body.ActionSignatures { + if strings.HasPrefix(sig.SignaturePayload, "GroupChangeEvent.GroupMemberAddChange,") { + memberAdd = sig.SignaturePayload + } + } + require.NotEmpty(t, memberAdd, "no member-add signature in the request") + assert.True(t, strings.HasSuffix(memberAdd, ","+fmt.Sprintf(tt.want, body.ConversationKeyVersion)), memberAdd) + }) + } +} + func TestChatConversationPathHelpers(t *testing.T) { assert.Equal(t, "1-2", api.ChatConversationPathID("1:2")) assert.Equal(t, "1:2", api.ChatConversationEventID("1-2"))