1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-15 05:36:53 -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/stretchr/testify v1.11.1
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/exp v0.0.0-20250911091902-df9299821621
golang.org/x/net v0.44.0
google.golang.org/protobuf v1.36.9
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 (

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/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
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.1/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
go.mau.fi/util v0.9.2-0.20251001114608-d99877b9cc10 h1:EvX/di02gOriKN0xGDJuQ5mgiNdAF4LJc8moffI7Svo=
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/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
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=
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/mautrix v0.25.2-0.20250924172949-cf29b07f32ce h1:sRBScG2xa66ERjgrX+7D//HorAhmNHwxB2Kzltg+aUg=
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 h1:prmIYgiziW4A8H2v/TliQ7fis8uTWblabxyPIeLFlNg=
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,
},
},
ExcludeChangesFromTimeline: true,
}
applyAnnouncementsOnly(members.PowerLevels, groupInfo.AnnouncementsOnly)
joinRule := event.JoinRuleInvite
@ -185,6 +186,8 @@ func (s *SignalClient) wrapGroupInfo(ctx context.Context, groupInfo *signalmeow.
JoinRule: &event.JoinRulesEventContent{JoinRule: joinRule},
ExtraUpdates: bridgev2.MergeExtraUpdaters(makeRevisionUpdater(groupInfo.Revision), updatePortalSyncMeta),
CanBackfill: backupChat != nil,
ExcludeChangesFromTimeline: true,
}, nil
}

View file

@ -192,7 +192,7 @@ type GroupChange struct {
ModifyInviteLinkPassword *types.SerializedInviteLinkPassword
}
func (groupChange *GroupChange) isEmptpy() bool {
func (groupChange *GroupChange) isEmpty() bool {
return len(groupChange.AddMembers) == 0 &&
len(groupChange.DeleteMembers) == 0 &&
len(groupChange.ModifyMemberRoles) == 0 &&
@ -638,6 +638,7 @@ func (cli *Client) fetchGroupByID(ctx context.Context, gid types.GroupIdentifier
}
return cli.fetchGroupWithMasterKey(ctx, groupMasterKey)
}
func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey types.SerializedGroupMasterKey) (*Group, error) {
masterKeyBytes := masterKeyToBytes(groupMasterKey)
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyBytes)
@ -650,24 +651,28 @@ func (cli *Client) fetchGroupWithMasterKey(ctx context.Context, groupMasterKey t
ContentType: web.ContentTypeProtobuf,
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 {
return nil, err
}
if response.StatusCode != 200 {
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)
if err != nil {
return nil, err
}
err = proto.Unmarshal(groupBytes, &encryptedGroup)
err = proto.Unmarshal(groupBytes, &groupResponse)
if err != nil {
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 {
return nil, fmt.Errorf("failed to decrypt group: %w", err)
}
@ -1196,7 +1201,7 @@ func decryptRequestingMember(ctx context.Context, requestingMember *signalpb.Req
}, 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()
groupMasterKey := decryptedGroupChange.GroupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey)
@ -1479,7 +1484,7 @@ func (e RespError) Error() string {
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()
groupAuth, err := cli.GetAuthorizationForToday(ctx, masterKeyToBytes(groupMasterKey))
if err != nil {
@ -1488,9 +1493,9 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
}
var path string
if groupLinkPassword == nil {
path = "/v1/groups/"
path = "/v2/groups/"
} 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)
if err != nil {
@ -1535,70 +1540,78 @@ func (cli *Client) patchGroup(ctx context.Context, groupChange *signalpb.GroupCh
if err != nil {
return nil, fmt.Errorf("failed to read storage manifest response: %w", err)
}
signedGroupChange := signalpb.GroupChange{}
err = proto.Unmarshal(body, &signedGroupChange)
var changeResp signalpb.GroupChangeResponse
err = proto.Unmarshal(body, &changeResp)
if err != nil {
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) {
log := zerolog.Ctx(ctx).With().Str("action", "UpdateGroup").Logger()
groupMasterKey, err := cli.Store.GroupStore.MasterKeyFromGroupIdentifier(ctx, gid)
if err != nil {
log.Err(err).Msg("Could not get master key from group id")
return 0, err
return 0, fmt.Errorf("failed to get master key for group: %w", err)
}
groupChange.GroupMasterKey = groupMasterKey
masterKeyBytes := masterKeyToBytes(groupMasterKey)
var refetchedAddMemberCredentials bool
var signedGroupChange *signalpb.GroupChange
var signedGroupChange *signalpb.GroupChangeResponse
group, err := cli.RetrieveGroupByID(ctx, gid, 0)
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 {
groupChange.ModifyInviteLinkPassword = ptr.Ptr(GenerateInviteLinkPassword())
}
groupChange.Revision = group.Revision + 1
for attempt := 0; attempt < 5; attempt++ {
signedGroupChange, err = cli.EncryptAndSignGroupChange(ctx, groupChange, gid)
if errors.Is(err, GroupPatchNotAcceptedError) {
log.Warn().Str("Error applying GroupChange, retrying...", err.Error())
for attempt := 0; ; attempt++ {
signedGroupChange, err = cli.EncryptAndSignGroupChange(ctx, groupChange)
if err == nil {
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 {
refetchedAddMemberCredentials = true
// change = refetchAddMemberCredentials(change); TODO
} 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) {
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
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)
if groupChange.isEmptpy() {
if groupChange.isEmpty() {
log.Debug().Msg("Change is empty after conflict resolution")
}
groupChange.Revision = group.Revision + 1
} else {
break
return 0, fmt.Errorf("unknown error encrypting and signing group change: %w", err)
}
}
delete(cli.GroupCache.groups, gid)
delete(cli.GroupCache.lastFetched, gid)
delete(cli.GroupCache.activeCalls, gid)
if err != nil {
log.Err(err).Msg("couldn't patch group on server")
return 0, err
if signedGroupChange == nil {
return 0, fmt.Errorf("no signed group change returned: %w", err)
}
groupChangeBytes, err := proto.Marshal(signedGroupChange)
groupChangeBytes, err := proto.Marshal(signedGroupChange.GroupChange)
if err != nil {
log.Err(err).Msg("Error marshalling signed GroupChange")
return 0, err
return 0, fmt.Errorf("failed to marshal signed group change: %w", 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)
if err != nil {
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")
return nil, err
}
path := "/v1/groups/"
path := "/v2/groups/"
requestBody, err := proto.Marshal(encryptedGroup)
if err != nil {
log.Err(err).Msg("Failed to marshal request")
@ -1751,11 +1764,7 @@ func (cli *Client) createGroupOnServer(ctx context.Context, decryptedGroup *Grou
case http.StatusBadRequest:
return nil, fmt.Errorf("failed to put new group: bad request")
}
group, err := cli.fetchGroupWithMasterKey(ctx, decryptedGroup.GroupMasterKey)
if err != nil {
return nil, fmt.Errorf("failed to get new group: %w", err)
}
return group, nil
return cli.parseGroupResponse(ctx, resp, decryptedGroup.GroupMasterKey)
}
func GenerateInviteLinkPassword() types.SerializedInviteLinkPassword {
@ -1801,7 +1810,7 @@ func (cli *Client) GetGroupHistoryPage(ctx context.Context, gid types.GroupIdent
Host: web.StorageHostname,
}
// 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)
if err != nil {
return nil, err