1
0
Fork 0
mirror of https://github.com/mautrix/signal.git synced 2026-05-15 05:36:53 -04:00
mautrix-signal/pkg/connector/handlematrix.go

923 lines
32 KiB
Go
Raw Permalink Normal View History

// 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"
"crypto/sha256"
2024-06-27 11:53:21 +03:00
"errors"
"fmt"
2025-10-29 14:50:26 +02:00
"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"
2024-08-07 02:14:39 +03:00
"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)
)
2024-06-27 11:53:21 +03:00
func (s *SignalClient) sendMessage(ctx context.Context, portalID networkid.PortalID, content *signalpb.Content) error {
2024-08-07 02:14:39 +03:00
userID, groupID, err := signalid.ParsePortalID(portalID)
if err != nil {
2024-06-27 11:53:21 +03:00
return err
}
if groupID != "" {
2024-06-27 11:53:21 +03:00
result, err := s.Client.SendGroupMessage(ctx, groupID, content)
if err != nil {
2024-06-27 11:53:21 +03:00
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")
}
2024-06-27 11:53:21 +03:00
} else {
log.Debug().Msg("Sent event to all members of Signal group")
}
2024-06-27 11:53:21 +03:00
return nil
} else {
res := s.Client.SendMessage(ctx, userID, content)
2024-06-27 11:53:21 +03:00
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(
2025-11-24 16:53:26 +02:00
ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, msg.ReplyTo,
)
if err != nil {
return nil, err
}
2025-11-24 16:53:26 +02:00
return s.doSendMessage(ctx, msg, converted, &signalid.MessageMetadata{
2025-10-29 14:50:26 +02:00
ContainsAttachments: len(converted.Attachments) > 0,
})
}
func (s *SignalClient) doSendMessage(
ctx context.Context,
msg *bridgev2.MatrixMessage,
converted *signalpb.DataMessage,
meta *signalid.MessageMetadata,
) (*bridgev2.MatrixMessageResponse, error) {
2025-11-24 16:53:26 +02:00
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts
2025-10-29 14:50:26 +02:00
if meta == nil {
meta = &signalid.MessageMetadata{}
}
msgID := signalid.MakeMessageID(s.Client.Store.ACI, ts)
2024-09-06 14:04:29 +03:00
msg.AddPendingToIgnore(networkid.TransactionID(msgID))
2025-10-29 14:50:26 +02:00
err := s.sendMessage(ctx, msg.Portal.ID, &signalpb.Content{DataMessage: converted})
if err != nil {
return nil, bridgev2.WrapErrorInStatus(err).WithSendNotice(true)
}
dbMsg := &database.Message{
2024-09-06 14:04:29 +03:00
ID: msgID,
2024-08-07 02:14:39 +03:00
SenderID: signalid.MakeUserID(s.Client.Store.ACI),
Timestamp: time.UnixMilli(int64(ts)),
2025-10-29 14:50:26 +02:00
Metadata: meta,
}
return &bridgev2.MatrixMessageResponse{
2024-09-06 14:04:29 +03:00
DB: dbMsg,
RemovePending: networkid.TransactionID(msgID),
}, nil
}
func (s *SignalClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
2024-08-07 02:14:39 +03:00
_, targetSentTimestamp, err := signalid.ParseMessageID(msg.EditTarget.ID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
2024-08-07 02:14:39 +03:00
} else if msg.EditTarget.SenderID != signalid.MakeUserID(s.Client.Store.ACI) {
return fmt.Errorf("cannot edit other people's messages")
}
2024-08-07 02:14:39 +03:00
var replyTo *database.Message
2024-07-10 23:50:41 +03:00
if msg.EditTarget.ReplyTo.MessageID != "" {
2024-08-07 02:14:39 +03:00
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)
}
}
2025-11-24 16:53:26 +02:00
converted, err := s.Main.MsgConv.ToSignal(ctx, s.Client, msg.Portal, msg.Event, msg.Content, msg.OrigSender != nil, replyTo)
if err != nil {
return err
}
2025-11-24 16:53:26 +02:00
ts := getTimestampForEvent(msg.InputTransactionID, msg.Event, msg.OrigSender)
converted.Timestamp = &ts
2024-06-27 11:53:21 +03:00
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)
2024-08-07 02:14:39 +03:00
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{
2024-08-07 02:14:39 +03:00
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) {
2024-08-07 02:14:39 +03:00
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),
},
},
}
2024-06-27 11:53:21 +03:00
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 {
2024-08-07 02:14:39 +03:00
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),
},
},
}
2024-06-27 11:53:21 +03:00
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 {
2024-08-07 02:14:39 +03:00
_, targetSentTimestamp, err := signalid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
2024-08-07 02:14:39 +03:00
} 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),
},
},
}
2024-06-27 11:53:21 +03:00
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 {
2024-08-07 02:14:39 +03:00
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) {
2024-08-07 02:14:39 +03:00
_, groupID, err := signalid.ParsePortalID(portal.ID)
if err != nil || groupID == "" {
return false, err
}
2024-08-07 02:14:39 +03:00
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()
}
2024-08-07 02:14:39 +03:00
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) {
2024-08-07 02:14:39 +03:00
_, 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)
}
2025-12-02 15:26:16 +02:00
func (s *SignalClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
2026-03-31 19:56:36 +03:00
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:
2025-12-02 15:26:16 +02:00
return nil, fmt.Errorf("cannot invite additional user to dm")
default:
2025-12-02 15:26:16 +02:00
return nil, nil
}
}
targetSignalID, err = signalid.ParseGhostOrUserLoginID(msg.Target)
if err != nil {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
}
targetIntent = ghost.Intent
}
default:
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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 {
2025-12-02 15:26:16 +02:00
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:
2025-12-02 15:26:16 +02:00
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 == "" {
2025-12-02 15:26:16 +02:00
return nil, err
}
gc.Revision = msg.Portal.Metadata.(*signalid.PortalMetadata).Revision + 1
revision, err := s.Client.UpdateGroup(ctx, gc, groupID)
if err != nil {
2025-12-02 15:26:16 +02:00
return nil, err
}
if msg.Type == bridgev2.Invite && targetSignalID.Type != libsignalgo.ServiceIDTypePNI {
err = targetIntent.EnsureJoined(ctx, msg.Portal.MXID)
if err != nil {
2025-12-02 15:26:16 +02:00
return nil, err
}
}
msg.Portal.Metadata.(*signalid.PortalMetadata).Revision = revision
2025-12-02 15:26:16 +02:00
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
}
}
2025-10-29 14:50:26 +02:00
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
}
2025-10-29 14:50:26 +02:00
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)),
}
2025-11-24 16:53:26 +02:00
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, &signalid.MessageMetadata{
2025-10-29 14:50:26 +02:00
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,
2025-11-24 16:53:26 +02:00
VoteCount: proto.Uint32(1), // TODO
2025-10-29 14:50:26 +02:00
},
2025-11-24 16:53:26 +02:00
RequiredProtocolVersion: proto.Uint32(0),
2025-10-29 14:50:26 +02:00
}
2025-11-24 16:53:26 +02:00
return s.doSendMessage(ctx, &msg.MatrixMessage, converted, nil)
2025-10-29 14:50:26 +02:00
}
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
}