Merge fc9a90cb7704e3729a966730ebc6eb018c9d3b37 into 1fface6074e41a13dcbd65ae376f0f3c055f5086

This commit is contained in:
Joakim Berglund 2026-09-10 21:52:03 +02:00 committed by GitHub
commit cf0c11aa22
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 285 additions and 44 deletions

View File

@ -115,6 +115,16 @@ e.g:
`curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291'` `curl -X GET -H "Content-Type: application/json" 'http://127.0.0.1:8080/v1/groups/+431212131491291'`
- Update your group member label
Updates the member label for the registered number in the given group. The group id can be obtained via the "List groups" REST call.
`curl -X PUT -H "Content-Type: application/json" -d '{"member_label": {"name": "<label>", "emoji": "<emoji>"}}' 'http://127.0.0.1:8080/v1/groups/<number>/<group id>'`
e.g:
`curl -X PUT -H "Content-Type: application/json" -d '{"member_label": {"name": "Dad", "emoji": "👨‍👧"}}' 'http://127.0.0.1:8080/v1/groups/+431212131491291/<group id>'`
- Delete a group - Delete a group
Delete the group with the given group id. The group id can be obtained via the "List groups" REST call. Delete the group with the given group id. The group id can be obtained via the "List groups" REST call.

View File

@ -47,10 +47,16 @@ type CreateGroupRequest struct {
ExpirationTime *int `json:"expiration_time,omitempty"` ExpirationTime *int `json:"expiration_time,omitempty"`
} }
type MemberLabel struct {
Name *string `json:"name,omitempty"`
Emoji *string `json:"emoji,omitempty"`
}
type UpdateGroupRequest struct { type UpdateGroupRequest struct {
Base64Avatar *string `json:"base64_avatar,omitempty"` Base64Avatar *string `json:"base64_avatar,omitempty"`
Description *string `json:"description,omitempty"` Description *string `json:"description,omitempty"`
Name *string `json:"name,omitempty"` Name *string `json:"name,omitempty"`
MemberLabel *MemberLabel `json:"member_label,omitempty"`
ExpirationTime *int `json:"expiration_time,omitempty"` ExpirationTime *int `json:"expiration_time,omitempty"`
GroupLinkState *string `json:"group_link,omitempty" enums:"disabled,enabled,enabled-with-approval"` GroupLinkState *string `json:"group_link,omitempty" enums:"disabled,enabled,enabled-with-approval"`
Permissions *ds.GroupPermissions `json:"permissions,omitempty"` Permissions *ds.GroupPermissions `json:"permissions,omitempty"`
@ -1854,8 +1860,15 @@ func (a *Api) UpdateGroup(c *gin.Context) {
} }
} }
var memberLabel *string
var memberLabelEmoji *string
if req.MemberLabel != nil {
memberLabel = req.MemberLabel.Name
memberLabelEmoji = req.MemberLabel.Emoji
}
err = a.signalClient.UpdateGroup(number, internalGroupId, req.Base64Avatar, req.Description, req.Name, req.ExpirationTime, groupLinkState, err = a.signalClient.UpdateGroup(number, internalGroupId, req.Base64Avatar, req.Description, req.Name, req.ExpirationTime, groupLinkState,
editGroupPermission, addMembersPermission, sendMessagesPermission) editGroupPermission, addMembersPermission, sendMessagesPermission, memberLabel, memberLabelEmoji)
if err != nil { if err != nil {
c.JSON(400, Error{Msg: err.Error()}) c.JSON(400, Error{Msg: err.Error()})
return return

27
src/api/api_test.go Normal file
View File

@ -0,0 +1,27 @@
package api
import (
"encoding/json"
"testing"
)
func TestUpdateGroupRequestMemberLabel(t *testing.T) {
var req UpdateGroupRequest
rawRequest := []byte(`{"member_label":{"name":"Dad","emoji":"\ud83d\udc4b"}}`)
if err := json.Unmarshal(rawRequest, &req); err != nil {
t.Fatalf("json.Unmarshal() error: %v", err)
}
if req.MemberLabel == nil {
t.Fatal("member_label got nil, want value")
}
if req.MemberLabel.Name == nil || *req.MemberLabel.Name != "Dad" {
t.Fatalf("member_label.name got %v, want Dad", req.MemberLabel.Name)
}
if req.MemberLabel.Emoji == nil || *req.MemberLabel.Emoji != "\U0001F44B" {
t.Fatalf("member_label.emoji got %v, want \\U0001F44B", req.MemberLabel.Emoji)
}
}

View File

@ -113,23 +113,27 @@ func (g GroupLinkState) FromString(input string) GroupLinkState {
} }
type GroupEntry struct { type GroupEntry struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Id string `json:"id"` Id string `json:"id"`
InternalId string `json:"internal_id"` InternalId string `json:"internal_id"`
Members []string `json:"members"` MemberLabel string `json:"member_label"`
Blocked bool `json:"blocked"` MemberLabelEmoji string `json:"member_label_emoji"`
Member bool `json:"member"` Members []string `json:"members"`
PendingInvites []string `json:"pending_invites"` Blocked bool `json:"blocked"`
PendingRequests []string `json:"pending_requests"` Member bool `json:"member"`
InviteLink string `json:"invite_link"` PendingInvites []string `json:"pending_invites"`
Admins []string `json:"admins"` PendingRequests []string `json:"pending_requests"`
Permissions ds.GroupPermissions `json:"permissions"` InviteLink string `json:"invite_link"`
Admins []string `json:"admins"`
Permissions ds.GroupPermissions `json:"permissions"`
} }
type GroupMember struct { type GroupMember struct {
Number string `json:"number"` Number string `json:"number"`
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
LabelEmoji *string `json:"label_emoji,omitempty"`
Label *string `json:"label,omitempty"`
} }
type GroupAdmin struct { type GroupAdmin struct {
@ -137,19 +141,28 @@ type GroupAdmin struct {
Uuid string `json:"uuid"` Uuid string `json:"uuid"`
} }
type SignalCliGroupMember struct {
Number string `json:"number"`
Uuid string `json:"uuid"`
LabelEmoji *string `json:"labelEmoji"`
Label *string `json:"label"`
}
type ExpandedGroupEntry struct { type ExpandedGroupEntry struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Id string `json:"id"` Id string `json:"id"`
InternalId string `json:"internal_id"` InternalId string `json:"internal_id"`
Members []GroupMember `json:"members"` MemberLabel string `json:"member_label"`
Blocked bool `json:"blocked"` MemberLabelEmoji string `json:"member_label_emoji"`
Member bool `json:"member"` Members []GroupMember `json:"members"`
PendingInvites []GroupMember `json:"pending_invites"` Blocked bool `json:"blocked"`
PendingRequests []GroupMember `json:"pending_requests"` Member bool `json:"member"`
InviteLink string `json:"invite_link"` PendingInvites []GroupMember `json:"pending_invites"`
Admins []GroupAdmin `json:"admins"` PendingRequests []GroupMember `json:"pending_requests"`
Permissions ds.GroupPermissions `json:"permissions"` InviteLink string `json:"invite_link"`
Admins []GroupAdmin `json:"admins"`
Permissions ds.GroupPermissions `json:"permissions"`
} }
type IdentityEntry struct { type IdentityEntry struct {
@ -162,20 +175,22 @@ type IdentityEntry struct {
} }
type SignalCliGroupEntry struct { type SignalCliGroupEntry struct {
Name string `json:"name"` Name string `json:"name"`
Description string `json:"description"` Description string `json:"description"`
Id string `json:"id"` Id string `json:"id"`
IsMember bool `json:"isMember"` IsMember bool `json:"isMember"`
IsBlocked bool `json:"isBlocked"` IsBlocked bool `json:"isBlocked"`
Members []GroupMember `json:"members"` MemberLabel *string `json:"memberLabel"`
PendingMembers []GroupMember `json:"pendingMembers"` MemberLabelEmoji *string `json:"memberLabelEmoji"`
RequestingMembers []GroupMember `json:"requestingMembers"` Members []SignalCliGroupMember `json:"members"`
GroupInviteLink string `json:"groupInviteLink"` PendingMembers []GroupMember `json:"pendingMembers"`
Admins []GroupAdmin `json:"admins"` RequestingMembers []GroupMember `json:"requestingMembers"`
Uuid string `json:"uuid"` GroupInviteLink string `json:"groupInviteLink"`
PermissionEditDetails string `json:"permissionEditDetails"` Admins []GroupAdmin `json:"admins"`
PermissionAddMember string `json:"permissionAddMember"` Uuid string `json:"uuid"`
PermissionSendMessage string `json:"permissionSendMessage"` PermissionEditDetails string `json:"permissionEditDetails"`
PermissionAddMember string `json:"permissionAddMember"`
PermissionSendMessage string `json:"permissionSendMessage"`
} }
type SignalCliIdentityEntry struct { type SignalCliIdentityEntry struct {
@ -1222,6 +1237,35 @@ func prefixUsernameMembers(members []string) []string {
return res return res
} }
func signalCliGroupMembersToGroupMembers(signalCliGroupMembers []SignalCliGroupMember) []GroupMember {
groupMembers := []GroupMember{}
for _, signalCliGroupMember := range signalCliGroupMembers {
groupMembers = append(groupMembers, GroupMember{
Number: signalCliGroupMember.Number,
Uuid: signalCliGroupMember.Uuid,
LabelEmoji: signalCliGroupMember.LabelEmoji,
Label: signalCliGroupMember.Label,
})
}
return groupMembers
}
func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}
func findOwnGroupMemberLabel(members []SignalCliGroupMember, number string) (string, string) {
for _, member := range members {
if member.Number == number {
return stringValue(member.Label), stringValue(member.LabelEmoji)
}
}
return "", ""
}
func (s *SignalClient) updateGroupMembers(number string, groupId string, members []string, add bool) error { func (s *SignalClient) updateGroupMembers(number string, groupId string, members []string, add bool) error {
if len(members) == 0 { if len(members) == 0 {
@ -1355,10 +1399,12 @@ func signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry SignalCliGroupE
groupEntry.Blocked = signalCliGroupEntry.IsBlocked groupEntry.Blocked = signalCliGroupEntry.IsBlocked
groupEntry.Member = signalCliGroupEntry.IsMember groupEntry.Member = signalCliGroupEntry.IsMember
groupEntry.Description = signalCliGroupEntry.Description groupEntry.Description = signalCliGroupEntry.Description
groupEntry.MemberLabel = stringValue(signalCliGroupEntry.MemberLabel)
groupEntry.MemberLabelEmoji = stringValue(signalCliGroupEntry.MemberLabelEmoji)
groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage) groupEntry.Permissions.SendMessages = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage) groupEntry.Permissions.EditGroup = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionSendMessage)
groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember) groupEntry.Permissions.AddMembers = signalCliGroupPermissionToRestApiGroupPermission(signalCliGroupEntry.PermissionAddMember)
groupEntry.Members = signalCliGroupEntry.Members groupEntry.Members = signalCliGroupMembersToGroupMembers(signalCliGroupEntry.Members)
groupEntry.PendingInvites = signalCliGroupEntry.PendingMembers groupEntry.PendingInvites = signalCliGroupEntry.PendingMembers
groupEntry.PendingRequests = signalCliGroupEntry.RequestingMembers groupEntry.PendingRequests = signalCliGroupEntry.RequestingMembers
groupEntry.Admins = signalCliGroupEntry.Admins groupEntry.Admins = signalCliGroupEntry.Admins
@ -1396,7 +1442,11 @@ func (s *SignalClient) GetGroupsExpanded(number string) ([]ExpandedGroupEntry, e
} }
for _, signalCliGroupEntry := range signalCliGroupEntries { for _, signalCliGroupEntry := range signalCliGroupEntries {
groupEntries = append(groupEntries, signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry)) groupEntry := signalCliGroupEntryToExpandedGroupEntry(signalCliGroupEntry)
if groupEntry.MemberLabel == "" && groupEntry.MemberLabelEmoji == "" {
groupEntry.MemberLabel, groupEntry.MemberLabelEmoji = findOwnGroupMemberLabel(signalCliGroupEntry.Members, number)
}
groupEntries = append(groupEntries, groupEntry)
} }
return groupEntries, nil return groupEntries, nil
@ -1412,6 +1462,7 @@ func (s *SignalClient) GetGroups(number string) ([]GroupEntry, error) {
for _, expandedGroupEntry := range expandedGroupEntries { for _, expandedGroupEntry := range expandedGroupEntries {
groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name, groupEntry := GroupEntry{InternalId: expandedGroupEntry.InternalId, Name: expandedGroupEntry.Name,
Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Member: expandedGroupEntry.Member, Description: expandedGroupEntry.Description, Id: expandedGroupEntry.Id, Blocked: expandedGroupEntry.Blocked, Member: expandedGroupEntry.Member, Description: expandedGroupEntry.Description,
MemberLabel: expandedGroupEntry.MemberLabel, MemberLabelEmoji: expandedGroupEntry.MemberLabelEmoji,
Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink} Permissions: expandedGroupEntry.Permissions, InviteLink: expandedGroupEntry.InviteLink}
members := []string{} members := []string{}
@ -2074,7 +2125,8 @@ func (s *SignalClient) QuitGroup(number string, groupId string) error {
} }
func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *string, groupDescription *string, groupName *string, expirationTime *int, func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *string, groupDescription *string, groupName *string, expirationTime *int,
groupLinkState *GroupLinkState, editGroupPermission GroupPermission, addMembersPermission GroupPermission, sendMessagesPermission GroupPermission) error { groupLinkState *GroupLinkState, editGroupPermission GroupPermission, addMembersPermission GroupPermission, sendMessagesPermission GroupPermission,
memberLabel *string, memberLabelEmoji *string) error {
var err error var err error
var avatarTmpPath string = "" var avatarTmpPath string = ""
if base64Avatar != nil { if base64Avatar != nil {
@ -2123,6 +2175,8 @@ func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *
EditGroupPermissions string `json:"setPermissionEditDetails,omitempty"` EditGroupPermissions string `json:"setPermissionEditDetails,omitempty"`
AddMembersPermissions string `json:"setPermissionAddMember,omitempty"` AddMembersPermissions string `json:"setPermissionAddMember,omitempty"`
SendMessagesPermissions string `json:"setPermissionSendMessages,omitempty"` SendMessagesPermissions string `json:"setPermissionSendMessages,omitempty"`
MemberLabel *string `json:"memberLabel,omitempty"`
MemberLabelEmoji *string `json:"memberLabelEmoji,omitempty"`
} }
request := Request{GroupId: groupId} request := Request{GroupId: groupId}
@ -2153,6 +2207,9 @@ func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *
request.SendMessagesPermissions = sendMessagesPermission.String() request.SendMessagesPermissions = sendMessagesPermission.String()
} }
request.MemberLabel = memberLabel
request.MemberLabelEmoji = memberLabelEmoji
jsonRpc2Client, err := s.getJsonRpc2Client() jsonRpc2Client, err := s.getJsonRpc2Client()
if err != nil { if err != nil {
return err return err
@ -2192,6 +2249,14 @@ func (s *SignalClient) UpdateGroup(number string, groupId string, base64Avatar *
cmd = append(cmd, []string{"--set-permission-send-messages", sendMessagesPermission.String()}...) cmd = append(cmd, []string{"--set-permission-send-messages", sendMessagesPermission.String()}...)
} }
if memberLabelEmoji != nil {
cmd = append(cmd, []string{"--member-label-emoji", *memberLabelEmoji}...)
}
if memberLabel != nil {
cmd = append(cmd, []string{"--member-label", *memberLabel}...)
}
_, err = s.cliClient.Execute(true, cmd, "") _, err = s.cliClient.Execute(true, cmd, "")
} }

82
src/client/group_test.go Normal file
View File

@ -0,0 +1,82 @@
package client
import (
"encoding/json"
"testing"
)
func TestSignalCliGroupEntryMemberLabelFields(t *testing.T) {
var signalCliGroupEntry SignalCliGroupEntry
rawGroup := []byte(`{"id":"abc","members":[{"number":"+1234","uuid":"uuid-1","labelEmoji":"\ud83d\udc4b","label":"Dad"}]}`)
if err := json.Unmarshal(rawGroup, &signalCliGroupEntry); err != nil {
t.Fatalf("json.Unmarshal() error: %v", err)
}
memberLabel, memberLabelEmoji := findOwnGroupMemberLabel(signalCliGroupEntry.Members, "+1234")
if memberLabel != "Dad" {
t.Fatalf("memberLabel got %v, want Dad", memberLabel)
}
if memberLabelEmoji != "\U0001F44B" {
t.Fatalf("memberLabelEmoji got %v, want \\U0001F44B", memberLabelEmoji)
}
groupEntry := GroupEntry{
MemberLabel: memberLabel,
MemberLabelEmoji: memberLabelEmoji,
}
encodedGroup, err := json.Marshal(groupEntry)
if err != nil {
t.Fatalf("json.Marshal() error: %v", err)
}
var response map[string]any
if err := json.Unmarshal(encodedGroup, &response); err != nil {
t.Fatalf("json.Unmarshal() response error: %v", err)
}
if response["member_label"] != "Dad" {
t.Fatalf("member_label got %v, want Dad", response["member_label"])
}
if response["member_label_emoji"] != "\U0001F44B" {
t.Fatalf("member_label_emoji got %v, want \\U0001F44B", response["member_label_emoji"])
}
}
func TestSignalCliGroupEntryMissingMemberLabelFields(t *testing.T) {
var signalCliGroupEntry SignalCliGroupEntry
rawGroup := []byte(`{"id":"abc","members":[{"number":"+1234","uuid":"uuid-1"}]}`)
if err := json.Unmarshal(rawGroup, &signalCliGroupEntry); err != nil {
t.Fatalf("json.Unmarshal() error: %v", err)
}
memberLabel, memberLabelEmoji := findOwnGroupMemberLabel(signalCliGroupEntry.Members, "+1234")
groupEntry := GroupEntry{
MemberLabel: memberLabel,
MemberLabelEmoji: memberLabelEmoji,
}
encodedGroup, err := json.Marshal(groupEntry)
if err != nil {
t.Fatalf("json.Marshal() error: %v", err)
}
var response map[string]any
if err := json.Unmarshal(encodedGroup, &response); err != nil {
t.Fatalf("json.Unmarshal() response error: %v", err)
}
if response["member_label"] != "" {
t.Fatalf("member_label got %v, want empty string", response["member_label"])
}
if response["member_label_emoji"] != "" {
t.Fatalf("member_label_emoji got %v, want empty string", response["member_label_emoji"])
}
}

View File

@ -219,6 +219,17 @@ const docTemplate = `{
], ],
"type": "object" "type": "object"
}, },
"api.MemberLabel": {
"properties": {
"emoji": {
"type": "string"
},
"name": {
"type": "string"
}
},
"type": "object"
},
"api.PinMessageInGroupRequest": { "api.PinMessageInGroupRequest": {
"properties": { "properties": {
"duration": { "duration": {
@ -643,6 +654,9 @@ const docTemplate = `{
], ],
"type": "string" "type": "string"
}, },
"member_label": {
"$ref": "#/definitions/api.MemberLabel"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -798,6 +812,12 @@ const docTemplate = `{
"member": { "member": {
"type": "boolean" "type": "boolean"
}, },
"member_label": {
"type": "string"
},
"member_label_emoji": {
"type": "string"
},
"members": { "members": {
"items": { "items": {
"type": "string" "type": "string"
@ -831,6 +851,8 @@ const docTemplate = `{
"internal_id", "internal_id",
"invite_link", "invite_link",
"member", "member",
"member_label",
"member_label_emoji",
"members", "members",
"name", "name",
"pending_invites", "pending_invites",

View File

@ -214,6 +214,17 @@
], ],
"type": "object" "type": "object"
}, },
"api.MemberLabel": {
"properties": {
"emoji": {
"type": "string"
},
"name": {
"type": "string"
}
},
"type": "object"
},
"api.PinMessageInGroupRequest": { "api.PinMessageInGroupRequest": {
"properties": { "properties": {
"duration": { "duration": {
@ -638,6 +649,9 @@
], ],
"type": "string" "type": "string"
}, },
"member_label": {
"$ref": "#/definitions/api.MemberLabel"
},
"name": { "name": {
"type": "string" "type": "string"
}, },
@ -793,6 +807,12 @@
"member": { "member": {
"type": "boolean" "type": "boolean"
}, },
"member_label": {
"type": "string"
},
"member_label_emoji": {
"type": "string"
},
"members": { "members": {
"items": { "items": {
"type": "string" "type": "string"
@ -826,6 +846,8 @@
"internal_id", "internal_id",
"invite_link", "invite_link",
"member", "member",
"member_label",
"member_label_emoji",
"members", "members",
"name", "name",
"pending_invites", "pending_invites",