diff --git a/go.mod b/go.mod index fee3d523..726ed5a7 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( golang.org/x/net v0.25.0 google.golang.org/protobuf v1.34.1 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.18.2-0.20240606131110-a0e309fa55ab + maunium.net/go/mautrix v0.18.2-0.20240606175822-a150a4760419 nhooyr.io/websocket v1.8.11 ) diff --git a/go.sum b/go.sum index b80546d5..d6720a8d 100644 --- a/go.sum +++ b/go.sum @@ -95,7 +95,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.18.2-0.20240606131110-a0e309fa55ab h1:e0Zo3/K+quT6p+U2Gsmw8R1kinzZ0wOkPbVPwoMkrBY= -maunium.net/go/mautrix v0.18.2-0.20240606131110-a0e309fa55ab/go.mod h1:P/FV8cXY262MezYX7ViuhfzeJ0nK4+M8K6ZmxEC/aEA= +maunium.net/go/mautrix v0.18.2-0.20240606175822-a150a4760419 h1:6JOCYkxS/nfhK/J3o+kkJgUDRaPRGgQXaxZj5HX5jys= +maunium.net/go/mautrix v0.18.2-0.20240606175822-a150a4760419/go.mod h1:P/FV8cXY262MezYX7ViuhfzeJ0nK4+M8K6ZmxEC/aEA= nhooyr.io/websocket v1.8.11 h1:f/qXNc2/3DpoSZkHt1DQu6rj4zGC8JmkkLkWss0MgN0= nhooyr.io/websocket v1.8.11/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 9075567a..f46270e8 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -435,16 +435,157 @@ type msgconvContext struct { ReplyTo *database.Message } -func (s *SignalClient) convertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data *events.ChatEvent) (*bridgev2.ConvertedMessage, error) { +type Bv2ChatEvent struct { + *events.ChatEvent + s *SignalClient +} + +var ( + _ bridgev2.RemoteMessage = (*Bv2ChatEvent)(nil) + _ bridgev2.RemoteEdit = (*Bv2ChatEvent)(nil) + _ bridgev2.RemoteEventWithTimestamp = (*Bv2ChatEvent)(nil) + _ bridgev2.RemoteReactionWithMeta = (*Bv2ChatEvent)(nil) + _ bridgev2.RemoteMessageRemove = (*Bv2ChatEvent)(nil) +) + +func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType { + switch innerEvt := evt.Event.(type) { + case *signalpb.DataMessage: + switch { + case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil: + return bridgev2.RemoteEventMessage + case innerEvt.Reaction != nil: + if innerEvt.Reaction.GetRemove() { + return bridgev2.RemoteEventReactionRemove + } + return bridgev2.RemoteEventReaction + case innerEvt.Delete != nil: + return bridgev2.RemoteEventMessageRemove + } + case *signalpb.EditMessage: + return bridgev2.RemoteEventEdit + case *signalpb.TypingMessage: + //return bridgev2.RemoteEventTyping + } + return bridgev2.RemoteEventUnknown +} + +func (evt *Bv2ChatEvent) GetPortalID() networkid.PortalID { + return evt.s.getPortalID(evt.Info.ChatID) +} + +func (evt *Bv2ChatEvent) ShouldCreatePortal() bool { + return evt.GetType() == bridgev2.RemoteEventMessage +} + +func (evt *Bv2ChatEvent) AddLogContext(c zerolog.Context) zerolog.Context { + c = c.Stringer("sender_id", evt.Info.Sender) + switch innerEvt := evt.Event.(type) { + case *signalpb.DataMessage: + c = c.Uint64("message_ts", innerEvt.GetTimestamp()) + switch { + case innerEvt.Reaction != nil: + c = c.Uint64("reaction_target_ts", innerEvt.Reaction.GetTargetSentTimestamp()) + case innerEvt.Delete != nil: + c = c.Uint64("delete_target_ts", innerEvt.Delete.GetTargetSentTimestamp()) + } + case *signalpb.EditMessage: + c = c. + Uint64("edit_target_ts", innerEvt.GetTargetSentTimestamp()). + Uint64("edit_ts", innerEvt.GetDataMessage().GetTimestamp()) + } + return c +} + +func (evt *Bv2ChatEvent) GetSender() bridgev2.EventSender { + return evt.s.makeEventSender(evt.Info.Sender) +} + +func (evt *Bv2ChatEvent) GetID() networkid.MessageID { + ts := evt.getDataMsgTimestamp() + if ts == 0 { + panic(fmt.Errorf("GetID() called for non-DataMessage event")) + } + return makeMessageID(evt.Info.Sender, ts) +} + +func (evt *Bv2ChatEvent) getDataMsgTimestamp() uint64 { + switch innerEvt := evt.Event.(type) { + case *signalpb.DataMessage: + return innerEvt.GetTimestamp() + case *signalpb.EditMessage: + return innerEvt.GetDataMessage().GetTimestamp() + default: + return 0 + } +} + +func (evt *Bv2ChatEvent) GetTimestamp() time.Time { + ts := evt.getDataMsgTimestamp() + if ts == 0 { + return time.Now() + } + return time.UnixMilli(int64(ts)) +} + +func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID { + var targetAuthorACI string + var targetSentTS uint64 + switch innerEvt := evt.Event.(type) { + case *signalpb.DataMessage: + switch { + case innerEvt.Reaction != nil: + targetAuthorACI = innerEvt.Reaction.GetTargetAuthorAci() + targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp() + case innerEvt.Delete != nil: + targetSentTS = innerEvt.Delete.GetTargetSentTimestamp() + default: + panic(fmt.Errorf("GetTargetMessage() called for message type without target")) + } + case *signalpb.EditMessage: + targetSentTS = innerEvt.GetTargetSentTimestamp() + default: + panic(fmt.Errorf("GetTargetMessage() called for message type without target")) + } + targetAuthorUUID := evt.Info.Sender + if targetAuthorACI != "" { + targetAuthorUUID, _ = uuid.Parse(targetAuthorACI) + } + return makeMessageID(targetAuthorUUID, targetSentTS) +} + +func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) { + dataMsg, ok := evt.Event.(*signalpb.DataMessage) + if !ok || dataMsg.Reaction == nil { + panic(fmt.Errorf("GetReactionEmoji() called for non-reaction event")) + } + return dataMsg.GetReaction().GetEmoji(), "" +} + +func (evt *Bv2ChatEvent) GetReactionDBMetadata() map[string]any { + dataMsg, ok := evt.Event.(*signalpb.DataMessage) + if !ok || dataMsg.Reaction == nil { + return map[string]any{} + } + return map[string]any{ + "emoji": dataMsg.GetReaction().GetEmoji(), + } +} + +func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { mcCtx := &msgconvContext{ - Connector: s.Main, + Connector: evt.s.Main, Intent: intent, - Client: s, + Client: evt.s, Portal: portal, } ctx = context.WithValue(ctx, msgconvContextKey, mcCtx) - dataMsg := data.Event.(*signalpb.DataMessage) - converted := s.Main.MsgConv.ToMatrix(ctx, dataMsg) + dataMsg, ok := evt.Event.(*signalpb.DataMessage) + if !ok { + return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event") + } + converted := evt.s.Main.MsgConv.ToMatrix(ctx, dataMsg) + converted.MergeCaption() var replyTo *networkid.MessageOptionalPartID if dataMsg.GetQuote() != nil { quoteAuthor, _ := uuid.Parse(dataMsg.Quote.GetAuthorAci()) @@ -463,37 +604,42 @@ func (s *SignalClient) convertMessage(ctx context.Context, portal *bridgev2.Port } return &bridgev2.ConvertedMessage{ - //ID: makeMessageID(data.Info.Sender, dataMsg.GetTimestamp()), - //EventSender: s.makeEventSender(data.Info.Sender), - Timestamp: time.UnixMilli(int64(converted.Timestamp)), - ReplyTo: replyTo, - Parts: convertedParts, + ReplyTo: replyTo, + Parts: convertedParts, }, nil } +func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) { + mcCtx := &msgconvContext{ + Connector: evt.s.Main, + Intent: intent, + Client: evt.s, + Portal: portal, + } + ctx = context.WithValue(ctx, msgconvContextKey, mcCtx) + editMsg, ok := evt.Event.(*signalpb.EditMessage) + if !ok { + return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event") + } + // TODO tell converter about existing parts to avoid reupload? + converted := evt.s.Main.MsgConv.ToMatrix(ctx, editMsg.GetDataMessage()) + converted.MergeCaption() + convertedEdit := &bridgev2.ConvertedEdit{} + // TODO can anything other than the text be edited? + lastPart := converted.Parts[len(converted.Parts)-1] + convertedEdit.ModifiedParts = append(convertedEdit.ModifiedParts, &bridgev2.ConvertedEditPart{ + Part: existing[len(existing)-1], + Type: lastPart.Type, + Content: lastPart.Content, + Extra: lastPart.Extra, + }) + return convertedEdit, nil +} + func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) { switch evt := rawEvt.(type) { case *events.ChatEvent: - switch innerEvt := evt.Event.(type) { - case *signalpb.DataMessage: - s.Main.Bridge.QueueRemoteEvent(s.UserLogin, &bridgev2.SimpleRemoteEvent[*events.ChatEvent]{ - Type: bridgev2.RemoteEventMessage, - LogContext: func(c zerolog.Context) zerolog.Context { - return c. - Uint64("message_id", innerEvt.GetTimestamp()). - Stringer("sender_id", evt.Info.Sender) - }, - ID: makeMessageID(evt.Info.Sender, innerEvt.GetTimestamp()), - Sender: s.makeEventSender(evt.Info.Sender), - PortalID: s.getPortalID(evt.Info.ChatID), - Data: evt, - CreatePortal: true, - - ConvertMessageFunc: s.convertMessage, - }) - case *signalpb.EditMessage: - case *signalpb.TypingMessage: - } + s.Main.Bridge.QueueRemoteEvent(s.UserLogin, &Bv2ChatEvent{ChatEvent: evt, s: s}) case *events.DecryptionError: case *events.Receipt: case *events.ReadSelf: