// 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 . package connector import ( "context" "crypto/sha256" "errors" "fmt" "slices" "strconv" "time" "github.com/google/uuid" "github.com/rs/zerolog" "go.mau.fi/util/ptr" "go.mau.fi/util/variationselector" "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/simplevent" "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-signal/pkg/libsignalgo" "go.mau.fi/mautrix-signal/pkg/signalid" "go.mau.fi/mautrix-signal/pkg/signalmeow" signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf" ) var ( _ bridgev2.EditHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ReactionHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RedactionHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ReadReceiptHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.TypingHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomNameHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomAvatarHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.RoomTopicHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*SignalClient)(nil) _ bridgev2.DisappearTimerChangingNetworkAPI = (*SignalClient)(nil) _ bridgev2.DeleteChatHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.PollHandlingNetworkAPI = (*SignalClient)(nil) _ bridgev2.MessageRequestAcceptingNetworkAPI = (*SignalClient)(nil) ) func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error { userID, groupID, err := signalid.ParsePortalID(portalID) if err != nil { return err } if groupID != "" { result, err := s.Client.SendGroupMessage(ctx, groupID, content) if err != nil { return err } totalRecipients := len(result.FailedToSendTo) + len(result.SuccessfullySentTo) log := zerolog.Ctx(ctx).With(). Int("total_recipients", totalRecipients). Int("failed_to_send_to_count", len(result.FailedToSendTo)). Int("successfully_sent_to_count", len(result.SuccessfullySentTo)). Logger() if len(result.SuccessfullySentTo) == 0 && len(result.FailedToSendTo) == 0 { log.Debug().Msg("No successes or failures - Probably sent to myself") } else if len(result.SuccessfullySentTo) == 0 { log.Error().Msg("Failed to send event to all members of Signal group") return errors.New("failed to send to any members of Signal group") } else if len(result.SuccessfullySentTo) < totalRecipients { if len(result.FailedToSendTo) > 0 { log.Warn().Msg("Failed to send event to some members of Signal group") } else { log.Warn().Msg("Only sent event to some members of Signal group") } } else { log.Debug().Msg("Sent event to all members of Signal group") } return nil } else { res := s.Client.SendMessage(ctx, userID, content) if !res.WasSuccessful { return res.Error } return nil } } func getTimestampForEvent(txnID networkid.RawTransactionID, evt *event.Event, origSender *bridgev2.OrigSender) uint64 { if origSender != nil { // Relaybot messages are never allowed to set the timestamp return uint64(time.Now().UnixMilli()) } if len(txnID) > 0 { parsed, err := strconv.ParseUint(string(txnID), 10, 64) if err == nil { return parsed } } return uint64(evt.Timestamp) } func (s *SignalClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) { converted, err := s.Main.MsgConv.ToSignal( ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo, ) if err != nil { return nil, err } return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{ ContainsAttachments: len(converted.Attachments) > 0, }) } func (s *SignalClient) doSendMessage( ctx context.Context, msg *bridgev2.MatrixMessage, converted *signalpb.DataMessage, meta *signalid.MessageMetadata, ) (*bridgev2.MatrixMessageResponse, error) { ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) converted.Timestamp = &ts if meta == nil { meta = &signalid.MessageMetadata{} } msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts) msg.AddPendingToIgnore(networkid.TransactionID(msgID)) err := s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{DataMessage: converted}) if err != nil { return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true) } dbMsg := &database.Message{ ID: msgID, SenderID: signalid.MakeUserID(s.Client.Store.ACI), Timestamp: time.UnixMilli(int64(ts)), Metadata: meta, } return &bridgev2.MatrixMessageResponse{ DB: dbMsg, RemovePending: networkid.TransactionID(msgID), }, nil } func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error { _, targetSentTimestamp, err := signalid.ParseMessageID(msg.EditTarget.ID) if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) } else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) { return fmt.Errorf("cannot edit other people's messages") } var replyTo *database.Message if msg.EditTarget.ReplyTo.MessageID != "" { replyTo, err = s.Main.Bridge.DB.Message.GetFirstOrSpecificPartByID(ctx, msg.Portal.Receiver, msg.EditTarget.ReplyTo) if err != nil { return fmt.Errorf("failed to get message reply target: %w", err) } } converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo) if err != nil { return err } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) converted.Timestamp = &ts err = s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{EditMessage: &signalpb.EditMessage{ TargetSentTimestamp: proto.Uint64(targetSentTimestamp), DataMessage: converted, }}) if err != nil { return bridgev2.WrapErrorInStatus(err).WithSendNotice(true) } msg.EditTarget.ID = signalid.MakeMessageID(s.Client.Store.ACI, ts) msg.EditTarget.Metadata = &signalid.MessageMetadata{ContainsAttachments: len(converted.Attachments) > 0} msg.EditTarget.EditCount++ return nil } func (s *SignalClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { return bridgev2.MatrixReactionPreResponse{ SenderID: signalid.MakeUserID(s.Client.Store.ACI), EmojiID: "", Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key), }, nil } func (s *SignalClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) { targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID) if err != nil { return nil, fmt.Errorf("failed to parse target message ID: %w", err) } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) wrappedContent := &signalpb.Content{ DataMessage: &signalpb.DataMessage{ Timestamp: proto.Uint64(ts), RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), Reaction: &signalpb.DataMessage_Reaction{ Emoji: proto.String(msg.PreHandleResp.Emoji), Remove: proto.Bool(false), TargetAuthorAciBinary: targetAuthorACI[:], TargetSentTimestamp: proto.Uint64(targetSentTimestamp), }, }, } err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent) if err != nil { return nil, err } return &database.Reaction{}, nil } func (s *SignalClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error { targetAuthorACI, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetReaction.MessageID) if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) wrappedContent := &signalpb.Content{ DataMessage: &signalpb.DataMessage{ Timestamp: proto.Uint64(ts), RequiredProtocolVersion: proto.Uint32(uint32(signalpb.DataMessage_REACTIONS)), Reaction: &signalpb.DataMessage_Reaction{ Emoji: proto.String(msg.TargetReaction.Emoji), Remove: proto.Bool(true), TargetAuthorAciBinary: targetAuthorACI[:], TargetSentTimestamp: proto.Uint64(targetSentTimestamp), }, }, } err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent) if err != nil { return err } return nil } func (s *SignalClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { _, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID) if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) } else if msg.TargetMessage.SenderID != signalid.MakeUserID(s.Client.Store.ACI) { return fmt.Errorf("cannot delete other people's messages") } ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) wrappedContent := &signalpb.Content{ DataMessage: &signalpb.DataMessage{ Timestamp: proto.Uint64(ts), Delete: &signalpb.DataMessage_Delete{ TargetSentTimestamp: proto.Uint64(targetSentTimestamp), }, }, } err = s.sendMessage(ctx, msg.Portal.ID, wrappedContent) if err != nil { return err } return nil } func (s *SignalClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error { if !receipt.ReadUpTo.After(receipt.LastRead) { return nil } if receipt.LastRead.IsZero() { receipt.LastRead = receipt.ReadUpTo.Add(-5 * time.Second) } dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery(ctx, receipt.Portal.PortalKey, receipt.LastRead, receipt.ReadUpTo) if err != nil { return fmt.Errorf("failed to get messages to mark as read: %w", err) } else if len(dbMessages) == 0 { return nil } messagesToRead := map[uuid.UUID][]uint64{} for _, msg := range dbMessages { userID, timestamp, err := signalid.ParseMessageID(msg.ID) if err != nil { continue } messagesToRead[userID] = append(messagesToRead[userID], timestamp) } zerolog.Ctx(ctx).Debug(). Any("targets", messagesToRead). Msg("Collected read receipt target messages") // TODO send sync message manually containing all read receipts instead of a separate message for each recipient for destination, messages := range messagesToRead { // Don't send read receipts for own messages if destination == s.Client.Store.ACI { continue } // Don't use portal.sendSignalMessage because we're sending this straight to // who sent the original message, not the portal's ChatID ctx, cancel := context.WithTimeout(ctx, 10*time.Second) result := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(destination), signalmeow.ReadReceptMessageForTimestamps(messages)) cancel() if !result.WasSuccessful { zerolog.Ctx(ctx).Err(result.Error). Stringer("destination", destination). Uints64("message_ids", messages). Msg("Failed to send read receipt to Signal") } else { zerolog.Ctx(ctx).Debug(). Stringer("destination", destination). Uints64("message_ids", messages). Msg("Sent read receipt to Signal") } } return nil } func (s *SignalClient) HandleMatrixTyping(ctx context.Context, typing *bridgev2.MatrixTyping) error { userID, groupID, err := signalid.ParsePortalID(typing.Portal.ID) if err != nil { return err } typingMessage := signalmeow.TypingMessage(typing.IsTyping) if !userID.IsEmpty() && userID.Type == libsignalgo.ServiceIDTypeACI { result := s.Client.SendMessage(ctx, userID, typingMessage) if !result.WasSuccessful { return result.Error } } else if groupID != "" { _, err = s.Client.SendGroupMessage(ctx, groupID, typingMessage) if err != nil { return err } } return nil } func (s *SignalClient) handleMatrixRoomMeta(ctx context.Context, portal *bridgev2.Portal, gc *signalmeow.GroupChange, postUpdatePortal func()) (bool, error) { _, groupID, err := signalid.ParsePortalID(portal.ID) if err != nil || groupID == "" { return false, err } gc.Revision = portal.Metadata.(*signalid.PortalMetadata).Revision + 1 revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { return false, err } if gc.ModifyTitle != nil { portal.Name = *gc.ModifyTitle portal.NameSet = true } if gc.ModifyDescription != nil { portal.Topic = *gc.ModifyDescription portal.TopicSet = true } if gc.ModifyAvatar != nil { portal.AvatarID = makeAvatarPathID(*gc.ModifyAvatar) portal.AvatarSet = true } if postUpdatePortal != nil { postUpdatePortal() } portal.Metadata.(*signalid.PortalMetadata).Revision = revision return true, nil } func (s *SignalClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) { return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{ ModifyTitle: &msg.Content.Name, }, nil) } func (s *SignalClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) { _, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil || groupID == "" { return false, err } var avatarPath string var avatarHash [32]byte if msg.Content.URL != "" { data, err := s.Main.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, nil) if err != nil { return false, fmt.Errorf("failed to download avatar: %w", err) } avatarHash = sha256.Sum256(data) avatarPath, err = s.Client.UploadGroupAvatar(ctx, data, groupID, "") if err != nil { return false, fmt.Errorf("failed to reupload avatar: %w", err) } } return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{ ModifyAvatar: &avatarPath, }, func() { msg.Portal.AvatarMXC = msg.Content.URL msg.Portal.AvatarHash = avatarHash }) } func (s *SignalClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) { return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{ ModifyDescription: &msg.Content.Topic, }, nil) } func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { if msg.Type.IsSelf && msg.OrigSender != nil { return nil, nil } var targetIntent bridgev2.MatrixAPI var targetSignalID libsignalgo.ServiceID var err error if msg.Portal.RoomType == database.RoomTypeDM { switch msg.Type { case bridgev2.Invite: return nil, fmt.Errorf("cannot invite additional user to dm") default: return nil, nil } } targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target) if err != nil { return nil, fmt.Errorf("failed to parse target signal id: %w", err) } switch target := msg.Target.(type) { case *bridgev2.Ghost: targetIntent = target.Intent case *bridgev2.UserLogin: targetIntent = target.User.DoublePuppet(ctx) if targetIntent == nil { ghost, err := s.Main.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID)) if err != nil { return nil, fmt.Errorf("failed to get ghost for user: %w", err) } targetIntent = ghost.Intent } default: return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target) } log := zerolog.Ctx(ctx).With(). Str("From Membership", string(msg.Type.From)). Str("To Membership", string(msg.Type.To)). Logger() gc := &signalmeow.GroupChange{} role := signalmeow.GroupMember_DEFAULT if msg.Type.To == event.MembershipInvite || msg.Type == bridgev2.AcceptKnock { levels, err := msg.Portal.Bridge.Matrix.GetPowerLevels(ctx, msg.Portal.MXID) if err != nil { log.Err(err).Msg("Couldn't get power levels") if levels.GetUserLevel(targetIntent.GetMXID()) >= moderatorPL { role = signalmeow.GroupMember_ADMINISTRATOR } } } switch msg.Type { case bridgev2.AcceptInvite: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't accept invite for non-ACI service ID") } gc.PromotePendingMembers = []*signalmeow.PromotePendingMember{{ ACI: targetSignalID.UUID, }} case bridgev2.RevokeInvite, bridgev2.RejectInvite: gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} case bridgev2.Leave, bridgev2.Kick: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't kick non-ACI service ID") } gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID} case bridgev2.Invite: if targetSignalID.Type == libsignalgo.ServiceIDTypeACI { gc.AddMembers = []*signalmeow.AddMember{{ GroupMember: signalmeow.GroupMember{ ACI: targetSignalID.UUID, Role: role, }, }} } else { gc.AddPendingMembers = []*signalmeow.PendingMember{{ ServiceID: targetSignalID, Role: role, AddedByUserID: s.Client.Store.ACI, Timestamp: uint64(msg.Event.Timestamp), }} } // TODO: joining and knocking requires a way to obtain the invite link // because the joining/knocking member doesn't have the GroupMasterKey yet // case bridgev2.Join: // gc.AddMembers = []*signalmeow.AddMember{{ // GroupMember: signalmeow.GroupMember{ // ACI: targetSignalID, // Role: role, // }, // JoinFromInviteLink: true, // }} // case bridgev2.Knock: // gc.AddRequestingMembers = []*signalmeow.RequestingMember{{ // ACI: targetSignalID, // Timestamp: uint64(time.Now().UnixMilli()), // }} case bridgev2.AcceptKnock: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't accept knock from non-ACI service ID") } gc.PromoteRequestingMembers = []*signalmeow.RoleMember{{ ACI: targetSignalID.UUID, Role: role, }} case bridgev2.RetractKnock, bridgev2.RejectKnock: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't reject knock from non-ACI service ID") } gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID} case bridgev2.BanKnocked, bridgev2.BanInvited, bridgev2.BanJoined, bridgev2.BanLeft: gc.AddBannedMembers = []*signalmeow.BannedMember{{ ServiceID: targetSignalID, Timestamp: uint64(time.Now().UnixMilli()), }} switch msg.Type { case bridgev2.BanJoined: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't ban joined non-ACI service ID") } gc.DeleteMembers = []*uuid.UUID{&targetSignalID.UUID} case bridgev2.BanInvited: gc.DeletePendingMembers = []*libsignalgo.ServiceID{&targetSignalID} case bridgev2.BanKnocked: if targetSignalID.Type != libsignalgo.ServiceIDTypeACI { return nil, fmt.Errorf("can't ban knocked non-ACI service ID") } gc.DeleteRequestingMembers = []*uuid.UUID{&targetSignalID.UUID} } case bridgev2.Unban: gc.DeleteBannedMembers = []*libsignalgo.ServiceID{&targetSignalID} default: return nil, fmt.Errorf("unsupported membership change: %s -> %s", msg.Type.From, msg.Type.To) } _, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil || groupID == "" { return nil, err } gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1 revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { return nil, err } if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI { err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID) if err != nil { return nil, err } } msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision return nil, nil } func plToRole(pl int) signalmeow.GroupMemberRole { if pl >= moderatorPL { return signalmeow.GroupMember_ADMINISTRATOR } else { return signalmeow.GroupMember_DEFAULT } } func plToAccessControl(pl int) *signalmeow.AccessControl { var accessControl signalmeow.AccessControl if pl >= moderatorPL { accessControl = signalmeow.AccessControl_ADMINISTRATOR } else { accessControl = signalmeow.AccessControl_MEMBER } return &accessControl } func hasAdminChanged(plc *bridgev2.SinglePowerLevelChange) bool { if plc == nil { return false } return (plc.NewLevel < moderatorPL) != (plc.OrigLevel < moderatorPL) } func (s *SignalClient) HandleMatrixPowerLevels(ctx context.Context, msg *bridgev2.MatrixPowerLevelChange) (bool, error) { if msg.Portal.RoomType == database.RoomTypeDM { return false, nil } gc := &signalmeow.GroupChange{} for _, plc := range msg.Users { if !hasAdminChanged(&plc.SinglePowerLevelChange) { continue } serviceID, err := signalid.ParseGhostOrUserLoginID(plc.Target) if err != nil || serviceID.Type != libsignalgo.ServiceIDTypeACI { continue } gc.ModifyMemberRoles = append(gc.ModifyMemberRoles, &signalmeow.RoleMember{ ACI: serviceID.UUID, Role: plToRole(plc.NewLevel), }) } if hasAdminChanged(msg.EventsDefault) { announcementsOnly := msg.EventsDefault.NewLevel >= moderatorPL gc.ModifyAnnouncementsOnly = &announcementsOnly } if hasAdminChanged(msg.StateDefault) { gc.ModifyAttributesAccess = plToAccessControl(msg.StateDefault.NewLevel) } if hasAdminChanged(msg.Invite) { gc.ModifyMemberAccess = plToAccessControl(msg.Invite.NewLevel) } _, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil || groupID == "" { return false, err } revision, err := s.Client.UpdateGroup(ctx, gc, groupID) if err != nil { return false, err } msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision return true, nil } func (s *SignalClient) HandleMatrixViewingChat(ctx context.Context, msg *bridgev2.MatrixViewingChat) error { if msg.Portal == nil { return nil } // Sync the other users ghost in DMs if msg.Portal.OtherUserID != "" { ghost, err := s.Main.Bridge.GetExistingGhostByID(ctx, msg.Portal.OtherUserID) if err != nil { return fmt.Errorf("failed to get ghost for sync: %w", err) } else if ghost == nil { zerolog.Ctx(ctx).Warn(). Str("other_user_id", string(msg.Portal.OtherUserID)). Msg("No ghost found for other user in portal") } else { meta := ghost.Metadata.(*signalid.GhostMetadata) if meta.ProfileFetchedAt.Time.Add(5 * time.Minute).Before(time.Now()) { // Reset, but don't save, portal last sync time for immediate sync now meta.ProfileFetchedAt.Time = time.Time{} info, err := s.GetUserInfoWithRefreshAfter(ctx, ghost, 5*time.Minute) if err != nil { return fmt.Errorf("failed to get user info: %w", err) } ghost.UpdateInfo(ctx, info) } } } // always resync the portal if its stale portalMeta := msg.Portal.Metadata.(*signalid.PortalMetadata) if portalMeta.LastSync.Add(24 * time.Hour).Before(time.Now()) { s.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, PortalKey: msg.Portal.PortalKey, }, GetChatInfoFunc: s.GetChatInfo, }) } return nil } func (s *SignalClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) { if msg.Content.Type != event.DisappearingTypeAfterRead && msg.Content.Timer.Duration != 0 { return false, fmt.Errorf("unsupported disappearing timer type: %s", msg.Content.Type) } userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil { return false, err } newSetting := database.DisappearingSetting{ Type: event.DisappearingTypeAfterRead, Timer: msg.Content.Timer.Duration, } if newSetting.Timer == 0 { newSetting.Type = event.DisappearingTypeNone } if groupID != "" { return s.handleMatrixRoomMeta(ctx, msg.Portal, &signalmeow.GroupChange{ ModifyDisappearingMessagesDuration: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), }, func() { msg.Portal.Disappear = newSetting }) } else { ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender) res := s.Client.SendMessage(ctx, userID, &signalpb.Content{ DataMessage: &signalpb.DataMessage{ Timestamp: ptr.Ptr(ts), Flags: ptr.Ptr(uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE)), ExpireTimer: ptr.Ptr(uint32(msg.Content.Timer.Seconds())), }, }) if !res.WasSuccessful { return false, res.Error } msg.Portal.Disappear = newSetting return true, nil } } func (s *SignalClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error { userID, groupID, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil { return fmt.Errorf("failed to parse portal ID: %w", err) } if msg.Content.FromMessageRequest { // TODO block and delete support? err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_DELETE) if err != nil { return fmt.Errorf("failed to send message request delete sync: %w", err) } } // Build ConversationIdentifier based on portal type var conversationID *signalpb.ConversationIdentifier if groupID == "" { conversationID = &signalpb.ConversationIdentifier{ Identifier: &signalpb.ConversationIdentifier_ThreadServiceIdBinary{ ThreadServiceIdBinary: userID.Bytes(), }, } } else { gid, err := groupID.Bytes() if err != nil { return fmt.Errorf("failed to parse group ID: %w", err) } conversationID = &signalpb.ConversationIdentifier{ Identifier: &signalpb.ConversationIdentifier_ThreadGroupId{ ThreadGroupId: gid[:], }, } } // Retrieve most recent messages from the portal var mostRecentMessages []*signalpb.AddressableMessage dbMessages, err := s.Main.Bridge.DB.Message.GetMessagesBetweenTimeQuery( ctx, msg.Portal.PortalKey, time.Now().Add(-30*24*time.Hour), // Last 30 days time.Now(), ) if err != nil { zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get recent messages for conversation delete") } else if len(dbMessages) > 0 { // Limit to the 5 most recent messages overall limit := 5 startIdx := 0 if len(dbMessages) > limit { startIdx = len(dbMessages) - limit } // Create AddressableMessage for most recent messages for _, dbMsg := range dbMessages[startIdx:] { senderACI, timestamp, err := signalid.ParseMessageID(dbMsg.ID) if err != nil { continue } mostRecentMessages = append(mostRecentMessages, &signalpb.AddressableMessage{ Author: &signalpb.AddressableMessage_AuthorServiceIdBinary{ AuthorServiceIdBinary: senderACI[:], }, SentTimestamp: proto.Uint64(timestamp), }) } } recipientID := s.Client.Store.ACIServiceID() // Send DeleteForMe sync message to self result := s.Client.SendMessage(ctx, recipientID, &signalpb.Content{ SyncMessage: &signalpb.SyncMessage{ DeleteForMe: &signalpb.SyncMessage_DeleteForMe{ ConversationDeletes: []*signalpb.SyncMessage_DeleteForMe_ConversationDelete{{ Conversation: conversationID, MostRecentMessages: mostRecentMessages, IsFullDelete: proto.Bool(true), }}, }, }, }) zerolog.Ctx(ctx).Debug(). Str("portal_id", string(msg.Portal.ID)). Int("recent_messages_count", len(mostRecentMessages)). Msg("Sent conversation deletion to Signal") if !result.WasSuccessful { return fmt.Errorf("failed to send delete conversation sync message: %w %s %s", result.Error, userID, groupID) } return nil } func (s *SignalClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) { optionNames := make([]string, len(msg.Content.PollStart.Answers)) optionIDs := make([]string, len(msg.Content.PollStart.Answers)) for i, option := range msg.Content.PollStart.Answers { optionNames[i] = option.Text optionIDs[i] = option.ID } converted := &signalpb.DataMessage{ PollCreate: &signalpb.DataMessage_PollCreate{ Question: ptr.Ptr(msg.Content.PollStart.Question.Text), AllowMultiple: ptr.Ptr(msg.Content.PollStart.MaxSelections != 1), Options: optionNames, }, RequiredProtocolVersion: ptr.Ptr(uint32(signalpb.DataMessage_POLLS)), } return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{ MatrixPollOptionIDs: optionIDs, }) } func (s *SignalClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) { senderACI, msgTS, err := signalid.ParseMessageID(msg.VoteTo.ID) if err != nil { return nil, err } mxOptions := msg.VoteTo.Metadata.(*signalid.MessageMetadata).MatrixPollOptionIDs optionIndexes := make([]uint32, len(msg.Content.Response.Answers)) for i, answer := range msg.Content.Response.Answers { if idx := slices.Index(mxOptions, answer); idx >= 0 { optionIndexes[i] = uint32(idx) } else if idx, err = strconv.Atoi(answer); err == nil && idx >= 0 { optionIndexes[i] = uint32(idx) } else { return nil, fmt.Errorf("unknown poll answer ID: %s", answer) } } converted := &signalpb.DataMessage{ PollVote: &signalpb.DataMessage_PollVote{ TargetAuthorAciBinary: senderACI[:], TargetSentTimestamp: &msgTS, OptionIndexes: optionIndexes, VoteCount: proto.Uint32(1), // TODO }, RequiredProtocolVersion: proto.Uint32(0), } return s.doSendMessage(ctx, &msg.MatrixMessage, converted, nil) } func (s *SignalClient) syncMessageRequestResponse( ctx context.Context, portal *bridgev2.Portal, respType signalpb.SyncMessage_MessageRequestResponse_Type, ) error { userID, groupID, err := signalid.ParsePortalID(portal.ID) if err != nil { return err } accept := &signalpb.SyncMessage_MessageRequestResponse{ Type: respType.Enum(), } if groupID != "" { gidBytes, err := groupID.Bytes() if err != nil { return fmt.Errorf("failed to parse group ID: %w", err) } accept.GroupId = gidBytes[:] } else if userID.Type == libsignalgo.ServiceIDTypeACI { accept.ThreadAciBinary = userID.UUID[:] } else { return fmt.Errorf("invalid portal ID for message request response: %s", portal.ID) } res := s.Client.SendMessage(ctx, libsignalgo.NewACIServiceID(s.Client.Store.ACI), &signalpb.Content{ SyncMessage: &signalpb.SyncMessage{ MessageRequestResponse: accept, }, }) if !res.WasSuccessful { return res.Error } return nil } func (s *SignalClient) HandleMatrixAcceptMessageRequest(ctx context.Context, msg *bridgev2.MatrixAcceptMessageRequest) error { userID, _, err := signalid.ParsePortalID(msg.Portal.ID) if err != nil { return err } err = s.syncMessageRequestResponse(ctx, msg.Portal, signalpb.SyncMessage_MessageRequestResponse_ACCEPT) if err != nil { return fmt.Errorf("failed to sync message request acceptance: %w", err) } if userID.Type == libsignalgo.ServiceIDTypeACI { profileKey, err := s.Client.ProfileKeyForSignalID(ctx, s.Client.Store.ACI) if err != nil { return fmt.Errorf("failed to get own profile key: %w", err) } var pniSig *signalpb.PniSignatureMessage if s.Client.Store.AccountRecord.GetPhoneNumberSharingMode() == signalpb.AccountRecord_EVERYBODY { sig, err := s.Client.Store.PNIIdentityKeyPair.SignAlternateIdentity(s.Client.Store.ACIIdentityKeyPair.GetIdentityKey()) if err != nil { return fmt.Errorf("failed to generate PNI signature: %w", err) } pniSig = &signalpb.PniSignatureMessage{ Pni: s.Client.Store.PNI[:], Signature: sig, } } res := s.Client.SendMessage(ctx, userID, &signalpb.Content{ DataMessage: &signalpb.DataMessage{ Flags: proto.Uint32(uint32(signalpb.DataMessage_PROFILE_KEY_UPDATE)), ProfileKey: profileKey.Slice(), Timestamp: proto.Uint64(getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)), RequiredProtocolVersion: proto.Uint32(0), }, PniSignatureMessage: pniSig, }) if !res.WasSuccessful { return fmt.Errorf("failed to share profile key to accept message request: %w", res.Error) } // TODO send read receipts too? } return nil }