Skip to content

Commit

Permalink
v2: add group change handling
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Jun 27, 2024
1 parent fdb9e7d commit 6a92ccc
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 50 deletions.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ require (
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8
golang.org/x/net v0.26.0
google.golang.org/protobuf v1.34.2
maunium.net/go/mautrix v0.19.0-beta.1.0.20240627083250-e25578d435a2
maunium.net/go/mautrix v0.19.0-beta.1.0.20240627155110-35f8d837b5f5
nhooyr.io/websocket v1.8.11
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ 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.19.0-beta.1.0.20240627083250-e25578d435a2 h1:HnP1dmvCVO/V22BdJre56Vu1IaEtMRUt86AD/1rmyME=
maunium.net/go/mautrix v0.19.0-beta.1.0.20240627083250-e25578d435a2/go.mod h1:eu/C1dTewrW7yiFNiCKGm4zuWJANyt7zPjaY5g3f3r4=
maunium.net/go/mautrix v0.19.0-beta.1.0.20240627155110-35f8d837b5f5 h1:dJBtUNpc9G0IKq1uRU6kZetBmgMtcmh1F/sMiyDpbDc=
maunium.net/go/mautrix v0.19.0-beta.1.0.20240627155110-35f8d837b5f5/go.mod h1:eu/C1dTewrW7yiFNiCKGm4zuWJANyt7zPjaY5g3f3r4=
nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0=
nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c=
25 changes: 1 addition & 24 deletions pkg/connector/chatinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,30 +53,7 @@ func (s *SignalClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal)
return nil, err
}
if groupID != "" {
groupInfo, err := s.Client.RetrieveGroupByID(ctx, groupID, 0)
if err != nil {
return nil, err
}
isDM := false
isSpace := false
members := make([]networkid.UserID, len(groupInfo.Members))
for i, member := range groupInfo.Members {
members[i] = makeUserID(member.ACI)
}
return &bridgev2.PortalInfo{
Name: &groupInfo.Title,
Topic: &groupInfo.Description,
Avatar: &bridgev2.Avatar{
ID: makeAvatarPathID(groupInfo.AvatarPath),
Get: func(ctx context.Context) ([]byte, error) {
return s.Client.DownloadGroupAvatar(ctx, groupInfo)
},
Remove: groupInfo.AvatarPath == "",
},
Members: members,
IsDirectChat: &isDM,
IsSpace: &isSpace,
}, nil
return s.getGroupInfo(ctx, groupID, 0)
} else {
aci, pni := serviceIDToACIAndPNI(userID)
contact, err := s.Client.Store.RecipientStore.LoadAndUpdateRecipient(ctx, aci, pni, nil)
Expand Down
135 changes: 135 additions & 0 deletions pkg/connector/groupinfo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

package connector

import (
"context"
"time"

"github.com/rs/zerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"

"go.mau.fi/mautrix-signal/pkg/signalmeow"
"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)

func (s *SignalClient) getGroupInfo(ctx context.Context, groupID types.GroupIdentifier, minRevision uint32) (*bridgev2.PortalInfo, error) {
groupInfo, err := s.Client.RetrieveGroupByID(ctx, groupID, minRevision)
if err != nil {
return nil, err
}
isDM := false
isSpace := false
members := make([]networkid.UserID, len(groupInfo.Members))
for i, member := range groupInfo.Members {
members[i] = makeUserID(member.ACI)
}
return &bridgev2.PortalInfo{
Name: &groupInfo.Title,
Topic: &groupInfo.Description,
Avatar: s.makeGroupAvatar(groupInfo),
Disappear: &database.DisappearingSetting{
Type: database.DisappearingTypeAfterRead,
Timer: time.Duration(groupInfo.DisappearingMessagesDuration) * time.Second,
},
Members: members,
IsDirectChat: &isDM,
IsSpace: &isSpace,
ExtraUpdates: makeRevisionUpdater(groupInfo.Revision),
}, nil
}

func (s *SignalClient) makeGroupAvatar(meta signalmeow.GroupAvatarMeta) *bridgev2.Avatar {
path := meta.GetAvatarPath()
if path == nil {
return nil
}
return &bridgev2.Avatar{
ID: makeAvatarPathID(*path),
Get: func(ctx context.Context) ([]byte, error) {
return s.Client.DownloadGroupAvatar(ctx, meta)
},
Remove: *path == "",
}
}

func makeRevisionUpdater(rev uint32) func(ctx context.Context, portal *bridgev2.Portal) bool {
return func(ctx context.Context, portal *bridgev2.Portal) bool {
currentRev, _ := database.GetNumberFromMap[uint32](portal.Metadata.Extra, "revision")
if currentRev < rev {
portal.Metadata.Extra["revision"] = rev
return true
}
return false
}
}

func (s *SignalClient) groupChangeToChatInfoChange(ctx context.Context, rev uint32, groupChange *signalmeow.GroupChange) *bridgev2.ChatInfoChange {
ic := &bridgev2.ChatInfoChange{
PortalInfo: &bridgev2.PortalInfo{
ExtraUpdates: makeRevisionUpdater(rev),
Name: groupChange.ModifyTitle,
Topic: groupChange.ModifyDescription,
Avatar: s.makeGroupAvatar(groupChange),
},
}
if groupChange.ModifyDisappearingMessagesDuration != nil {
ic.PortalInfo.Disappear = &database.DisappearingSetting{
Type: database.DisappearingTypeAfterRead,
Timer: time.Duration(*groupChange.ModifyDisappearingMessagesDuration) * time.Second,
}
}
// TODO handle member/permission/etc changes
return ic
}

func (s *SignalClient) catchUpGroup(ctx context.Context, portal *bridgev2.Portal, fromRevision, toRevision uint32, ts uint64) {
if fromRevision >= toRevision {
return
}
log := zerolog.Ctx(ctx).With().
Str("action", "catch up group changes").
Uint32("from_revision", fromRevision).
Uint32("to_revision", toRevision).
Logger()
if fromRevision == 0 {
log.Info().Msg("Syncing full group info")
info, err := s.getGroupInfo(ctx, types.GroupIdentifier(portal.ID), toRevision)
if err != nil {
log.Err(err).Msg("Failed to get group info")
} else {
portal.UpdateInfo(ctx, info, s.UserLogin, nil, time.Time{})
}
} else {
log.Info().Msg("Syncing missed group changes")
groupChanges, err := s.Client.GetGroupHistoryPage(ctx, types.GroupIdentifier(portal.ID), fromRevision, false)
if err != nil {
log.Err(err).Msg("Failed to get group history page")
return
}
for _, gc := range groupChanges {
log.Debug().Uint32("current_rev", gc.GroupChange.Revision).Msg("Processing group change")
chatInfoChange := s.groupChangeToChatInfoChange(ctx, gc.GroupChange.Revision, gc.GroupChange)
portal.ProcessChatInfoChange(ctx, s.makeEventSender(gc.GroupChange.SourceACI), s.UserLogin, chatInfoChange, time.UnixMilli(int64(ts)))
if gc.GroupChange.Revision == toRevision {
break
}
}
}
}
59 changes: 36 additions & 23 deletions pkg/connector/handlesignal.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,10 @@ import (

"github.com/google/uuid"
"github.com/rs/zerolog"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/exzerolog"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"

"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
Expand Down Expand Up @@ -64,6 +62,8 @@ var (
_ bridgev2.RemoteReactionRemove = (*Bv2ChatEvent)(nil)
_ bridgev2.RemoteMessageRemove = (*Bv2ChatEvent)(nil)
_ bridgev2.RemoteTyping = (*Bv2ChatEvent)(nil)
_ bridgev2.RemotePreHandler = (*Bv2ChatEvent)(nil)
_ bridgev2.RemoteChatInfoChange = (*Bv2ChatEvent)(nil)
)

func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
Expand All @@ -82,6 +82,8 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
return bridgev2.RemoteEventReaction
case innerEvt.Delete != nil:
return bridgev2.RemoteEventMessageRemove
case innerEvt.GetGroupV2().GetGroupChange() != nil:
return bridgev2.RemoteEventChatInfoChange
}
case *signalpb.EditMessage:
return bridgev2.RemoteEventEdit
Expand All @@ -91,6 +93,34 @@ func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
return bridgev2.RemoteEventUnknown
}

func (evt *Bv2ChatEvent) GetChatInfoChange(ctx context.Context) (*bridgev2.ChatInfoChange, error) {
dm, _ := evt.Event.(*signalpb.DataMessage)
gv2 := dm.GetGroupV2()
if gv2 == nil || gv2.GroupChange == nil {
return nil, fmt.Errorf("GetChatInfoChange() called for non-GroupChange event")
}
groupChange, err := evt.s.Client.DecryptGroupChange(ctx, gv2)
if err != nil {
return nil, fmt.Errorf("failed to decrypt group change: %w", err)
}
return evt.s.groupChangeToChatInfoChange(ctx, gv2.GetRevision(), groupChange), nil
}

func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) {
dataMsg, ok := evt.Event.(*signalpb.DataMessage)
if !ok || dataMsg.GroupV2 == nil {
return
}
portalRev, _ := database.GetNumberFromMap[uint32](portal.Metadata.Extra, "revision")
if evt.Info.GroupRevision > portalRev {
toRevision := evt.Info.GroupRevision
if dataMsg.GetGroupV2().GetGroupChange() != nil {
toRevision--
}
evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp())
}
}

func (evt *Bv2ChatEvent) GetTimeout() time.Duration {
if evt.Event.(*signalpb.TypingMessage).GetAction() == signalpb.TypingMessage_STARTED {
return 15 * time.Second
Expand All @@ -104,7 +134,7 @@ func (evt *Bv2ChatEvent) GetPortalKey() networkid.PortalKey {
}

func (evt *Bv2ChatEvent) ShouldCreatePortal() bool {
return evt.GetType() == bridgev2.RemoteEventMessage
return evt.GetType() == bridgev2.RemoteEventMessage || evt.GetType() == bridgev2.RemoteEventChatInfoChange
}

func (evt *Bv2ChatEvent) AddLogContext(c zerolog.Context) zerolog.Context {
Expand Down Expand Up @@ -231,28 +261,11 @@ func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Po
Type: database.DisappearingTypeAfterRead,
Timer: time.Duration(converted.DisappearIn) * time.Second,
}
dataMsgTS := time.UnixMilli(int64(dataMsg.GetTimestamp()))
if evt.Info.Sender == evt.s.Client.Store.ACI {
disappear.DisappearAt = time.UnixMilli(int64(dataMsg.GetTimestamp())).Add(disappear.Timer)
}
if portal.Metadata.DisappearTimer != disappear.Timer {
portal.Metadata.DisappearType = disappear.Type
portal.Metadata.DisappearTimer = disappear.Timer
err := portal.Save(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save portal metadata after implicitly updating disappearing timer")
} else {
zerolog.Ctx(ctx).Debug().Dur("new_timer", portal.Metadata.DisappearTimer).Msg("Implicitly updated disappearing timer in portal")
}
_, err = portal.Bridge.Bot.SendMessage(ctx, portal.MXID, event.EventMessage, &event.Content{
Parsed: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: fmt.Sprintf("Automatically enabled disappearing message timer (%s) because incoming message is disappearing", exfmt.Duration(disappear.Timer)),
},
}, time.UnixMilli(int64(dataMsg.GetTimestamp())))
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to send notice about disappearing message timer changing implicitly")
}
disappear.DisappearAt = dataMsgTS.Add(disappear.Timer)
}
portal.UpdateDisappearingSetting(ctx, disappear, nil, dataMsgTS, true, true)
}
return &bridgev2.ConvertedMessage{
ReplyTo: replyTo,
Expand Down

0 comments on commit 6a92ccc

Please sign in to comment.