1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-14 21:26:54 -04:00

Compare commits

...

3 commits

Author SHA1 Message Date
Tulir Asokan
7ffa38469f groupinfo: add exclude from timeline flag for group resyncs 2025-10-01 15:30:21 +03:00
Tulir Asokan
606d904cac dependencies: update mautrix-go 2025-10-01 15:30:18 +03:00
Tulir Asokan
c795cea7a1 signalmeow/groups: update to v2 api 2025-10-01 15:30:15 +03:00
4 changed files with 55 additions and 43 deletions

4
go.mod
View file

@ -12,13 +12,13 @@ require (
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.18.0 github.com/tidwall/gjson v1.18.0
go.mau.fi/util v0.9.1 go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.42.0
golang.org/x/exp v0.0.0-20250911091902-df9299821621 golang.org/x/exp v0.0.0-20250911091902-df9299821621
golang.org/x/net v0.44.0 golang.org/x/net v0.44.0
google.golang.org/protobuf v1.36.9 google.golang.org/protobuf v1.36.9
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.25.2-0.20250924172949-cf29b07f32ce maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf
) )
require ( require (

8
go.sum
View file

@ -65,8 +65,8 @@ 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/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA= github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.9.1 h1:A+XKHRsjKkFi2qOm4RriR1HqY2hoOXNS3WFHaC89r2Y= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10 h1:EvX/di02gOriKN0xGDJuQ5mgiNdAF4LJc8moffI7Svo=
go.mau.fi/util v0.9.1/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ= go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
@ -95,5 +95,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.25.2-0.20250924172949-cf29b07f32ce h1:sRBScG2xa66ERjgrX+7D//HorAhmNHwxB2Kzltg+aUg= maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf h1:prmIYgiziW4A8H2v/TliQ7fis8uTWblabxyPIeLFlNg=
maunium.net/go/mautrix v0.25.2-0.20250924172949-cf29b07f32ce/go.mod h1:iSueLJ/2fBaNrsTObGqi1j0cl/loxrtAjmjay1scYD8= maunium.net/go/mautrix v0.25.2-0.20251001115535-dd778ae0cdaf/go.mod h1:eWXuX2UAGye4AU7i/8Fv2L2Nh7L9kZtuv3R0O0n1KaM=

View file

@ -114,6 +114,7 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
event.StatePowerLevels: moderatorPL, event.StatePowerLevels: moderatorPL,
}, },
}, },
ExcludeChangesFromTimeline: true,
} }
applyAnnouncementsOnly(members.PowerLevels, groupInfo.AnnouncementsOnly) applyAnnouncementsOnly(members.PowerLevels, groupInfo.AnnouncementsOnly)
joinRule := event.JoinRuleInvite joinRule := event.JoinRuleInvite
@ -185,6 +186,8 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
JoinRule: &event.JoinRulesEventContent{JoinRule: joinRule}, JoinRule: &event.JoinRulesEventContent{JoinRule: joinRule},
ExtraUpdates: bridgev2.MergeExtraUpdaters(makeRevisionUpdater(groupInfo.Revision), updatePortalSyncMeta), ExtraUpdates: bridgev2.MergeExtraUpdaters(makeRevisionUpdater(groupInfo.Revision), updatePortalSyncMeta),
CanBackfill: backupChat != nil, CanBackfill: backupChat != nil,
ExcludeChangesFromTimeline: true,
}, nil }, nil
} }

View file

@ -192,7 +192,7 @@ type GroupChange struct {
ModifyInviteLinkPassword *types.SerializedInviteLinkPassword ModifyInviteLinkPassword *types.SerializedInviteLinkPassword
} }
func (groupChange *GroupChange) isEmptpy() bool { func (groupChange *GroupChange) isEmpty() bool {
return len(groupChange.AddMembers) == 0 && return len(groupChange.AddMembers) == 0 &&
len(groupChange.DeleteMembers) == 0 && len(groupChange.DeleteMembers) == 0 &&
len(groupChange.ModifyMemberRoles) == 0 && len(groupChange.ModifyMemberRoles) == 0 &&
@ -638,6 +638,7 @@ func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier
} }
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey) return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
} }
func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey types.SerializedGroupMasterKey) (*Group, error) { func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey types.SerializedGroupMasterKey) (*Group, error) {
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
@ -650,24 +651,28 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
ContentType: web.ContentTypeProtobuf, ContentType: web.ContentTypeProtobuf,
Host: web.StorageHostname, Host: web.StorageHostname,
} }
response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v1/groups", opts) response, err := web.SendHTTPRequest(ctx, http.MethodGet, "/v2/groups", opts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if response.StatusCode != 200 { if response.StatusCode != 200 {
return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode) return nil, fmt.Errorf("fetchGroupByID SendHTTPRequest bad status: %d", response.StatusCode)
} }
var encryptedGroup signalpb.Group return cli.parseGroupResponse(ctx, response, groupMasterKey)
}
func (cli *Client) parseGroupResponse(ctx context.Context, response *http.Response, masterKey types.SerializedGroupMasterKey) (*Group, error) {
var groupResponse signalpb.GroupResponse
groupBytes, err := io.ReadAll(response.Body) groupBytes, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = proto.Unmarshal(groupBytes, &encryptedGroup) err = proto.Unmarshal(groupBytes, &groupResponse)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal group: %w", err) return nil, fmt.Errorf("failed to unmarshal group: %w", err)
} }
group, err := decryptGroup(ctx, &encryptedGroup, groupMasterKey) group, err := decryptGroup(ctx, groupResponse.Group, masterKey)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to decrypt group: %w", err) return nil, fmt.Errorf("failed to decrypt group: %w", err)
} }
@ -1196,7 +1201,7 @@ func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.Req
}, nil }, nil
} }
func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroupChange *GroupChange, gid types.GroupIdentifier) (*signalpb.GroupChange, error) { func (cli *Client) EncryptAndSignGroupChange(ctx context.Context, decryptedGroupChange *GroupChange) (*signalpb.GroupChangeResponse, error) {
log := zerolog.Ctx(ctx).With().Str("action", "EncryptGroupChange").Logger() log := zerolog.Ctx(ctx).With().Str("action", "EncryptGroupChange").Logger()
groupMasterKey := decryptedGroupChange.GroupMasterKey groupMasterKey := decryptedGroupChange.GroupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
@ -1479,7 +1484,7 @@ func (e RespError) Error() string {
return e.Err return e.Err
} }
func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupChange_Actions, groupMasterKey types.SerializedGroupMasterKey, groupLinkPassword []byte) (*signalpb.GroupChange, error) { func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupChange_Actions, groupMasterKey types.SerializedGroupMasterKey, groupLinkPassword []byte) (*signalpb.GroupChangeResponse, error) {
log := zerolog.Ctx(ctx).With().Str("action", "patchGroup").Logger() log := zerolog.Ctx(ctx).With().Str("action", "patchGroup").Logger()
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey)) groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil { if err != nil {
@ -1488,9 +1493,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
} }
var path string var path string
if groupLinkPassword == nil { if groupLinkPassword == nil {
path = "/v1/groups/" path = "/v2/groups/"
} else { } else {
path = fmt.Sprintf("/v1/groups/?inviteLinkPassword=%s", base64.StdEncoding.EncodeToString(groupLinkPassword)) path = fmt.Sprintf("/v2/groups/?inviteLinkPassword=%s", base64.StdEncoding.EncodeToString(groupLinkPassword))
} }
requestBody, err := proto.Marshal(groupChange) requestBody, err := proto.Marshal(groupChange)
if err != nil { if err != nil {
@ -1535,70 +1540,78 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to read storage manifest response: %w", err) return nil, fmt.Errorf("failed to read storage manifest response: %w", err)
} }
signedGroupChange := signalpb.GroupChange{} var changeResp signalpb.GroupChangeResponse
err = proto.Unmarshal(body, &signedGroupChange) err = proto.Unmarshal(body, &changeResp)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to unmarshal signed groupChange: %w", err) return nil, fmt.Errorf("failed to unmarshal signed groupChange: %w", err)
} }
return &signedGroupChange, nil return &changeResp, nil
} }
func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) { func (cli *Client) UpdateGroup(ctx context.Context, groupChange *GroupChange, gid types.GroupIdentifier) (uint32, error) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger() log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid) groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil { if err != nil {
log.Err(err).Msg("Could not get master key from group id") return 0, fmt.Errorf("failed to get master key for group: %w", err)
return 0, err
} }
groupChange.GroupMasterKey = groupMasterKey groupChange.GroupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey) masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChange var signedGroupChange *signalpb.GroupChangeResponse
group, err := cli.RetrieveGroupByID(ctx, gid, 0) group, err := cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to retrieve Group") return 0, fmt.Errorf("failed to fetch group info to update: %w", err)
} }
if group.InviteLinkPassword == nil && groupChange.ModifyAddFromInviteLinkAccess != nil && groupChange.ModifyInviteLinkPassword != nil { if group.InviteLinkPassword == nil && groupChange.ModifyAddFromInviteLinkAccess != nil && groupChange.ModifyInviteLinkPassword != nil {
groupChange.ModifyInviteLinkPassword = ptr.Ptr(GenerateInviteLinkPassword()) groupChange.ModifyInviteLinkPassword = ptr.Ptr(GenerateInviteLinkPassword())
} }
groupChange.Revision = group.Revision + 1 groupChange.Revision = group.Revision + 1
for attempt := 0; attempt < 5; attempt++ { for attempt := 0; ; attempt++ {
signedGroupChange, err = cli.EncryptAndSignGroupChange(ctx, groupChange, gid) signedGroupChange, err = cli.EncryptAndSignGroupChange(ctx, groupChange)
if errors.Is(err, GroupPatchNotAcceptedError) { if err == nil {
log.Warn().Str("Error applying GroupChange, retrying...", err.Error()) break
} else if attempt >= 5 {
return 0, fmt.Errorf("failed to encrypt and sign group change after multiple retries: %w", err)
} else if errors.Is(err, GroupPatchNotAcceptedError) {
log.Err(err).Msg("Failed to apply group change, retrying...")
if len(groupChange.AddMembers) > 0 && !refetchedAddMemberCredentials { if len(groupChange.AddMembers) > 0 && !refetchedAddMemberCredentials {
refetchedAddMemberCredentials = true refetchedAddMemberCredentials = true
// change = refetchAddMemberCredentials(change); TODO // change = refetchAddMemberCredentials(change); TODO
} else { } else {
return 0, fmt.Errorf("Group Change Failed: %w", err) return 0, fmt.Errorf("failed to update group: %w", err)
} }
} else if errors.Is(err, ConflictError) { } else if errors.Is(err, ConflictError) {
delete(cli.GroupCache.groups, gid) delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid) delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid) delete(cli.GroupCache.activeCalls, gid)
group, err = cli.RetrieveGroupByID(ctx, gid, 0) group, err = cli.RetrieveGroupByID(ctx, gid, 0)
if err != nil {
return 0, fmt.Errorf("failed to fetch group after conflict: %w", err)
}
groupChange.resolveConflict(group) groupChange.resolveConflict(group)
if groupChange.isEmptpy() { if groupChange.isEmpty() {
log.Debug().Msg("Change is empty after conflict resolution") log.Debug().Msg("Change is empty after conflict resolution")
} }
groupChange.Revision = group.Revision + 1 groupChange.Revision = group.Revision + 1
} else { } else {
break return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err)
} }
} }
delete(cli.GroupCache.groups, gid) delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid) delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid) delete(cli.GroupCache.activeCalls, gid)
if err != nil { if signedGroupChange == nil {
log.Err(err).Msg("couldn't patch group on server") return 0, fmt.Errorf("no signed group change returned: %w", err)
return 0, err
} }
groupChangeBytes, err := proto.Marshal(signedGroupChange) groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
if err != nil { if err != nil {
log.Err(err).Msg("Error marshalling signed GroupChange") return 0, fmt.Errorf("failed to marshal signed group change: %w", err)
return 0, err }
groupContext := &signalpb.GroupContextV2{
Revision: &groupChange.Revision,
GroupChange: groupChangeBytes,
MasterKey: masterKeyBytes[:],
} }
groupContext := &signalpb.GroupContextV2{Revision: &groupChange.Revision, GroupChange: groupChangeBytes, MasterKey: masterKeyBytes[:]}
_, err = cli.SendGroupUpdate(ctx, group, groupContext, groupChange) _, err = cli.SendGroupUpdate(ctx, group, groupContext, groupChange)
if err != nil { if err != nil {
log.Err(err).Msg("Error sending GroupChange to group members") log.Err(err).Msg("Error sending GroupChange to group members")
@ -1718,7 +1731,7 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
log.Err(err).Msg("Failed to get Authorization for today") log.Err(err).Msg("Failed to get Authorization for today")
return nil, err return nil, err
} }
path := "/v1/groups/" path := "/v2/groups/"
requestBody, err := proto.Marshal(encryptedGroup) requestBody, err := proto.Marshal(encryptedGroup)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to marshal request") log.Err(err).Msg("Failed to marshal request")
@ -1751,11 +1764,7 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
case http.StatusBadRequest: case http.StatusBadRequest:
return nil, fmt.Errorf("failed to put new group: bad request") return nil, fmt.Errorf("failed to put new group: bad request")
} }
group, err := cli.fetchGroupWithMasterKey(ctx, decryptedGroup.GroupMasterKey) return cli.parseGroupResponse(ctx, resp, decryptedGroup.GroupMasterKey)
if err != nil {
return nil, fmt.Errorf("failed to get new group: %w", err)
}
return group, nil
} }
func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword { func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
@ -1801,7 +1810,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
Host: web.StorageHostname, Host: web.StorageHostname,
} }
// highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false // highest known epoch seems to always be 5, but that may change in the future. includeLastState is always false
path := fmt.Sprintf("/v1/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState) path := fmt.Sprintf("/v2/groups/logs/%d?maxSupportedChangeEpoch=%d&includeFirstState=%t&includeLastState=false", fromRevision, 5, includeFirstState)
response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts) response, err := web.SendHTTPRequest(ctx, http.MethodGet, path, opts)
if err != nil { if err != nil {
return nil, err return nil, err