From c6d22f403bf31a403213d8b11c0949545d34c931 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 17:56:43 +0100 Subject: [PATCH 01/12] Fix sending captions. Fixes #393 --- pkg/signalmeow/sending.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index 08c4b17f..e64446a0 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -430,6 +430,8 @@ func DataMessageForAttachment(attachmentPointer *AttachmentPointer, caption stri } if caption != "" { ap.Caption = proto.String(caption) + dm.Body = proto.String(caption) + dm.BodyRanges = ranges } dm.Attachments = append(dm.Attachments, ap) return wrapDataMessageInContent(dm) From 9201bff003425116566ee0e2e040b501abc117cb Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 18:00:48 +0100 Subject: [PATCH 02/12] Add more checks to weird name hack. Fixes #394 --- user.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/user.go b/user.go index 705aadd7..86fb0bca 100644 --- a/user.go +++ b/user.go @@ -747,13 +747,9 @@ func (user *User) incomingMessageHandler(incomingMessage signalmeow.IncomingSign // ensure everyone is invited to the group portal.ensureUserInvited(user) _ = ensureGroupPuppetsAreJoinedToPortal(context.Background(), user, portal) - } else { - if portal.shouldSetDMRoomMetadata() { - if senderPuppet.Name != portal.Name { - portal.Name = senderPuppet.Name - updatePortal = true - } - } + } else if senderPuppet.SignalID != user.SignalID && senderPuppet.Name != portal.Name && portal.shouldSetDMRoomMetadata() { + portal.Name = senderPuppet.Name + updatePortal = true } if updatePortal { _, err := portal.MainIntent().SetRoomName(portal.MXID, portal.Name) From ed4c7c93b597e01717cb546b5f9eb0ebc18060c8 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 18:24:53 +0100 Subject: [PATCH 03/12] Reply with error when trying to send without being logged in --- portal.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/portal.go b/portal.go index 3bbb792d..ceb0bec9 100644 --- a/portal.go +++ b/portal.go @@ -263,7 +263,7 @@ func (portal *Portal) handleMatrixMessages(msg portalMatrixMessage) { // If we have no SignalDevice, the bridge isn't logged in properly, // so send BAD_CREDENTIALS so the user knows if !msg.user.SignalDevice.IsDeviceLoggedIn() { - portal.sendMessageStatusCheckpointFailed(msg.evt, errUserNotLoggedIn) + go portal.sendMessageMetrics(msg.evt, errUserNotLoggedIn, "Ignoring", nil) msg.user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) return } From 2dbc42dc773fda9c14424b256ee22f25414be799 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 18:26:22 +0100 Subject: [PATCH 04/12] Fix IsLoggedIn check Should fix the event handler panic when handling typing notifications --- user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/user.go b/user.go index 86fb0bca..4bc04d7d 100644 --- a/user.go +++ b/user.go @@ -75,7 +75,7 @@ func (user *User) IsLoggedIn() bool { user.Lock() defer user.Unlock() - return user.SignalUsername != "" + return user.SignalDevice.IsDeviceLoggedIn() } func (user *User) GetManagementRoomID() id.RoomID { From 203f80cc0b0a76cec59714da0817ca37c516f126 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 18:28:00 +0100 Subject: [PATCH 05/12] Send stickers as real stickers instead of images --- portal.go | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/portal.go b/portal.go index ceb0bec9..11349b44 100644 --- a/portal.go +++ b/portal.go @@ -40,6 +40,7 @@ import ( cwebp "go.mau.fi/webp" "golang.org/x/exp/slices" "golang.org/x/image/webp" + "google.golang.org/protobuf/proto" "maunium.net/go/mautrix" "maunium.net/go/mautrix/appservice" "maunium.net/go/mautrix/bridge" @@ -687,6 +688,11 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, caption, ranges) case event.MessageType(event.EventSticker.Type): + var emoji *string + // TODO check for single grapheme cluster? + if len([]rune(content.Body)) == 1 { + emoji = proto.String(variationselector.Remove(content.Body)) + } image, err := portal.downloadAndDecryptMatrixMedia(ctx, content) if err != nil { return nil, err @@ -699,7 +705,21 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if err != nil { return nil, err } - outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, "", nil) + outgoingMessage = &signalmeow.SignalContent{ + DataMessage: &signalpb.DataMessage{ + Timestamp: proto.Uint64(uint64(time.Now().UnixMilli())), + Sticker: &signalpb.DataMessage_Sticker{ + // Signal iOS validates that pack id/key are of the correct length. + // Android is fine with any non-nil values (like a zero-length byte string). + PackId: make([]byte, 16), + PackKey: make([]byte, 32), + StickerId: proto.Uint32(0), + + Data: (*signalpb.AttachmentPointer)(attachmentPointer), + Emoji: emoji, + }, + }, + } case event.MsgVideo: fileName := content.Body var caption string From d78e006bc632f57f7769e43d446b2be8776ae9e3 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 19:39:01 +0100 Subject: [PATCH 06/12] Fix panics when receiving stickers --- pkg/signalmeow/receiving.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/receiving.go b/pkg/signalmeow/receiving.go index 3eb88a3a..69e3d001 100644 --- a/pkg/signalmeow/receiving.go +++ b/pkg/signalmeow/receiving.go @@ -904,9 +904,9 @@ func incomingDataMessage(ctx context.Context, device *Device, dataMessage *signa Quote: quoteData, ExpiresIn: expiresIn, }, - Width: *dataMessage.Sticker.Data.Width, - Height: *dataMessage.Sticker.Data.Height, - ContentType: *dataMessage.Sticker.Data.ContentType, + Width: dataMessage.Sticker.Data.GetWidth(), + Height: dataMessage.Sticker.Data.GetHeight(), + ContentType: dataMessage.Sticker.Data.GetContentType(), Filename: dataMessage.Sticker.Data.GetFileName(), Sticker: bytes, Emoji: dataMessage.GetSticker().GetEmoji(), From dd77f9d4b60add3a402597ba50c8d8cd6f4548bc Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 19:39:14 +0100 Subject: [PATCH 07/12] Add more info to outgoing attachment pointers --- portal.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/portal.go b/portal.go index 11349b44..f9269e28 100644 --- a/portal.go +++ b/portal.go @@ -685,6 +685,8 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if err != nil { return nil, err } + attachmentPointer.Height = proto.Uint32(uint32(content.GetInfo().Height)) + attachmentPointer.Width = proto.Uint32(uint32(content.GetInfo().Width)) outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, caption, ranges) case event.MessageType(event.EventSticker.Type): @@ -705,6 +707,9 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if err != nil { return nil, err } + attachmentPointer.Height = proto.Uint32(uint32(content.GetInfo().Height)) + attachmentPointer.Width = proto.Uint32(uint32(content.GetInfo().Width)) + attachmentPointer.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_BORDERLESS)) outgoingMessage = &signalmeow.SignalContent{ DataMessage: &signalpb.DataMessage{ Timestamp: proto.Uint64(uint64(time.Now().UnixMilli())), @@ -762,6 +767,9 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if err != nil { return nil, err } + if _, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"]; isVoice { + attachmentPointer.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE)) + } outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, caption, ranges) case event.MsgFile: From da879806fd636eed4396bb45f3c48c2e7fdaa79d Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 19:49:20 +0100 Subject: [PATCH 08/12] Fix sending voice messages --- portal.go | 40 ++++++++++++---------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/portal.go b/portal.go index f9269e28..e513bd2f 100644 --- a/portal.go +++ b/portal.go @@ -615,28 +615,6 @@ func convertVideo(ctx context.Context, mimeType string, video []byte) (string, [ return outMimeType, outVideo, nil } -func convertAudio(ctx context.Context, mimeType string, audio []byte) (string, []byte, error) { - var outMimeType string - var outAudio []byte - var err error - switch mimeType { - case "audio/aac", "audio/mp4", "audio/amr", "audio/mpeg", "audio/ogg; codecs=opus": - // Allowed - outMimeType = mimeType - outAudio = audio - case "audio/ogg": - // Hopefully it's opus already - outMimeType = "audio/ogg; codecs=opus" - outAudio = audio - default: - return "", nil, fmt.Errorf("%w %q in audio message", errMediaUnsupportedType, mimeType) - } - if err != nil { - return "", nil, fmt.Errorf("%w (%s to %s)", errMediaConvertFailed, mimeType, "video/mp4") - } - return outMimeType, outAudio, nil -} - func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, evt *event.Event) (*signalmeow.SignalContent, error) { content, ok := evt.Content.Parsed.(*event.MessageEventContent) if !ok { @@ -755,19 +733,25 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev fileName = content.FileName caption, ranges = matrixfmt.Parse(matrixFormatParams, content) } - image, err := portal.downloadAndDecryptMatrixMedia(ctx, content) + data, err := portal.downloadAndDecryptMatrixMedia(ctx, content) if err != nil { return nil, err } - newMimeType, convertedAudio, err := convertAudio(ctx, content.GetInfo().MimeType, image) - if err != nil { - return nil, err + _, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"] + mime := content.GetInfo().MimeType + if isVoice { + data, err = ffmpeg.ConvertBytes(ctx, data, ".m4a", []string{}, []string{"-c:a", "aac"}, mime) + if err != nil { + return nil, err + } + mime = "audio/aac" + fileName += ".m4a" } - attachmentPointer, err := signalmeow.UploadAttachment(sender.SignalDevice, convertedAudio, newMimeType, fileName) + attachmentPointer, err := signalmeow.UploadAttachment(sender.SignalDevice, data, mime, fileName) if err != nil { return nil, err } - if _, isVoice := evt.Content.Raw["org.matrix.msc3245.voice"]; isVoice { + if isVoice { attachmentPointer.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE)) } outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, caption, ranges) From 38e0c5fca6f7dc75a1aca76a1ea45cad21710e6b Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 19:53:49 +0100 Subject: [PATCH 09/12] Partially fix replying to files --- pkg/signalmeow/sending.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go index e64446a0..0ca75b9f 100644 --- a/pkg/signalmeow/sending.go +++ b/pkg/signalmeow/sending.go @@ -463,13 +463,15 @@ func DataMessageForDelete(targetMessageTimestamp uint64) *SignalContent { } func AddQuoteToDataMessage(content *SignalContent, quotedMessageSender string, quotedMessageTimestamp uint64) { - // Note: We're supposed to send the quoted message content too as a fallback, - // but it only seems to be necessary to quote image messages on iOS and Desktop. - // Android seems to render every quote fine, and iOS and Desktop render text quotes fine. content.DataMessage.Quote = &signalpb.DataMessage_Quote{ AuthorAci: proto.String(quotedMessageSender), Id: proto.Uint64(quotedMessageTimestamp), Type: signalpb.DataMessage_Quote_NORMAL.Enum(), + + // This is a hack to make Signal iOS and desktop render replies to file messages. + // Unfortunately it also makes Signal Desktop show a file icon on replies to text messages. + // TODO store file or text flag in database and fill this field only when replying to file messages. + Attachments: []*signalpb.DataMessage_Quote_QuotedAttachment{{}}, } } From a57b51af3d2265b34f41f6a06182d4055054f551 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Tue, 26 Dec 2023 20:01:39 +0100 Subject: [PATCH 10/12] Allow body to be same as filename if there's a formatted_body for caption --- portal.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/portal.go b/portal.go index e513bd2f..dd9c90f1 100644 --- a/portal.go +++ b/portal.go @@ -647,7 +647,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev fileName := content.Body var caption string var ranges []*signalpb.BodyRange - if content.FileName != "" && content.Body != content.FileName { + if content.FileName != "" && (content.Body != content.FileName || content.Format == event.FormatHTML) { fileName = content.FileName caption, ranges = matrixfmt.Parse(matrixFormatParams, content) } @@ -707,7 +707,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev fileName := content.Body var caption string var ranges []*signalpb.BodyRange - if content.FileName != "" && content.Body != content.FileName { + if content.FileName != "" && (content.Body != content.FileName || content.Format == event.FormatHTML) { fileName = content.FileName caption, ranges = matrixfmt.Parse(matrixFormatParams, content) } @@ -729,7 +729,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev fileName := content.Body var caption string var ranges []*signalpb.BodyRange - if content.FileName != "" && content.Body != content.FileName { + if content.FileName != "" && (content.Body != content.FileName || content.Format == event.FormatHTML) { fileName = content.FileName caption, ranges = matrixfmt.Parse(matrixFormatParams, content) } @@ -760,7 +760,7 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev fileName := content.Body var caption string var ranges []*signalpb.BodyRange - if content.FileName != "" && content.Body != content.FileName { + if content.FileName != "" && (content.Body != content.FileName || content.Format == event.FormatHTML) { fileName = content.FileName caption, ranges = matrixfmt.Parse(matrixFormatParams, content) } From c345091e3dcac20dfba5166d378fc4ed5a0cd993 Mon Sep 17 00:00:00 2001 From: Malte E <97891689+maltee1@users.noreply.github.com> Date: Tue, 26 Dec 2023 21:47:27 +0100 Subject: [PATCH 11/12] Add relay mode (#392) --- commands.go | 47 ++++++++++++++++++++++++++++ config/bridge.go | 58 +++++++++++++++++++++++++++++++++++ config/upgrade.go | 3 ++ example-config.yaml | 18 +++++++++++ messagetracking.go | 7 +++-- portal.go | 74 ++++++++++++++++++++++++++++++++++++++++++--- user.go | 2 ++ 7 files changed, 201 insertions(+), 8 deletions(-) diff --git a/commands.go b/commands.go index 87b29b4f..d1307408 100644 --- a/commands.go +++ b/commands.go @@ -49,6 +49,8 @@ func (br *SignalBridge) RegisterCommands() { cmdLogin, cmdPM, cmdDisconnect, + cmdSetRelay, + cmdUnsetRelay, ) } @@ -64,6 +66,51 @@ func wrapCommand(handler func(*WrappedCommandEvent)) func(*commands.Event) { } } +var cmdSetRelay = &commands.FullHandler{ + Func: wrapCommand(fnSetRelay), + Name: "set-relay", + Help: commands.HelpMeta{ + Section: HelpSectionPortalManagement, + Description: "Relay messages in this room through your Signal account.", + }, + RequiresPortal: true, + RequiresLogin: true, +} + +func fnSetRelay(ce *WrappedCommandEvent) { + if !ce.Bridge.Config.Bridge.Relay.Enabled { + ce.Reply("Relay mode is not enabled on this instance of the bridge") + } else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin { + ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge") + } else { + ce.Portal.RelayUserID = ce.User.MXID + ce.Portal.Update() + ce.Reply("Messages from non-logged-in users in this room will now be bridged through your Signal account") + } +} + +var cmdUnsetRelay = &commands.FullHandler{ + Func: wrapCommand(fnUnsetRelay), + Name: "unset-relay", + Help: commands.HelpMeta{ + Section: HelpSectionPortalManagement, + Description: "Stop relaying messages in this room.", + }, + RequiresPortal: true, +} + +func fnUnsetRelay(ce *WrappedCommandEvent) { + if !ce.Bridge.Config.Bridge.Relay.Enabled { + ce.Reply("Relay mode is not enabled on this instance of the bridge") + } else if ce.Bridge.Config.Bridge.Relay.AdminOnly && !ce.User.Admin { + ce.Reply("Only bridge admins are allowed to enable relay mode on this instance of the bridge") + } else { + ce.Portal.RelayUserID = "" + ce.Portal.Update() + ce.Reply("Messages from non-logged-in users will no longer be bridged in this room") + } +} + var cmdDisconnect = &commands.FullHandler{ Func: wrapCommand(fnDisconnect), Name: "disconnect", diff --git a/config/bridge.go b/config/bridge.go index cb2a53ac..728a0742 100644 --- a/config/bridge.go +++ b/config/bridge.go @@ -24,6 +24,8 @@ import ( "time" "maunium.net/go/mautrix/bridge/bridgeconfig" + "maunium.net/go/mautrix/event" + "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-signal/pkg/signalmeow" ) @@ -68,6 +70,8 @@ type BridgeConfig struct { Permissions bridgeconfig.PermissionConfig `yaml:"permissions"` + Relay RelaybotConfig `yaml:"relay"` + usernameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"` } @@ -169,3 +173,57 @@ func (bc BridgeConfig) FormatDisplayname(contact *signalmeow.Contact) string { }) return buffer.String() } + +type RelaybotConfig struct { + Enabled bool `yaml:"enabled"` + AdminOnly bool `yaml:"admin_only"` + MessageFormats map[event.MessageType]string `yaml:"message_formats"` + messageTemplates *template.Template `yaml:"-"` +} + +type umRelaybotConfig RelaybotConfig + +func (rc *RelaybotConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + err := unmarshal((*umRelaybotConfig)(rc)) + if err != nil { + return err + } + + rc.messageTemplates = template.New("messageTemplates") + for key, format := range rc.MessageFormats { + _, err := rc.messageTemplates.New(string(key)).Parse(format) + if err != nil { + return err + } + } + + return nil +} + +type Sender struct { + UserID string + event.MemberEventContent +} + +type formatData struct { + Sender Sender + Message string + Content *event.MessageEventContent +} + +func (rc *RelaybotConfig) FormatMessage(content *event.MessageEventContent, sender id.UserID, member event.MemberEventContent) (string, error) { + if len(member.Displayname) == 0 { + member.Displayname = sender.String() + } + member.Displayname = template.HTMLEscapeString(member.Displayname) + var output strings.Builder + err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), formatData{ + Sender: Sender{ + UserID: template.HTMLEscapeString(sender.String()), + MemberEventContent: member, + }, + Content: content, + Message: content.FormattedBody, + }) + return output.String(), err +} diff --git a/config/upgrade.go b/config/upgrade.go index ef797744..1cb11dd9 100644 --- a/config/upgrade.go +++ b/config/upgrade.go @@ -128,6 +128,9 @@ func DoUpgrade(helper *up.Helper) { helper.Copy(up.Bool, "bridge", "provisioning", "debug_endpoints") helper.Copy(up.Map, "bridge", "permissions") + helper.Copy(up.Bool, "bridge", "relay", "enabled") + helper.Copy(up.Bool, "bridge", "relay", "admin_only") + helper.Copy(up.Map, "bridge", "relay", "message_formats") } var SpacedBlocks = [][]string{ diff --git a/example-config.yaml b/example-config.yaml index 2cf9612e..1044ce95 100644 --- a/example-config.yaml +++ b/example-config.yaml @@ -262,6 +262,24 @@ bridge: "example.com": user "@admin:example.com": admin + # Settings for relay mode + relay: + # Whether relay mode should be allowed. If allowed, `!wa set-relay` can be used to turn any + # authenticated user into a relaybot for that chat. + enabled: false + # Should only admins be allowed to set themselves as relay users? + admin_only: true + # The formats to use when sending messages to Signal via the relaybot. + message_formats: + m.text: "{{ .Sender.Displayname }}: {{ .Message }}" + m.notice: "{{ .Sender.Displayname }}: {{ .Message }}" + m.emote: "* {{ .Sender.Displayname }} {{ .Message }}" + m.file: "{{ .Sender.Displayname }} sent a file" + m.image: "{{ .Sender.Displayname }} sent an image" + m.audio: "{{ .Sender.Displayname }} sent an audio file" + m.video: "{{ .Sender.Displayname }} sent a video" + m.location: "{{ .Sender.Displayname }} sent a location" + # Logging config. See https://github.com/tulir/zeroconfig for details. logging: min_level: debug diff --git a/messagetracking.go b/messagetracking.go index 50f0a554..62db364f 100644 --- a/messagetracking.go +++ b/messagetracking.go @@ -33,6 +33,7 @@ var ( errUserNotConnected = errors.New("you are not connected to Signal") errDifferentUser = errors.New("user is not the recipient of this private chat portal") errUserNotLoggedIn = errors.New("user is not logged in and chat has no relay bot") + errRelaybotNotLoggedIn = errors.New("neither user nor relay bot of chat are logged in") errMNoticeDisabled = errors.New("bridging m.notice messages is disabled") errUnexpectedParsedContentType = errors.New("unexpected parsed content type") errInvalidGeoURI = errors.New("invalid `geo:` URI in message") @@ -96,7 +97,8 @@ func errorToStatusReason(err error) (reason event.MessageStatusReason, status ev case errors.Is(err, errUserNotConnected): return event.MessageStatusGenericError, event.MessageStatusRetriable, true, true, "" case errors.Is(err, errUserNotLoggedIn), - errors.Is(err, errDifferentUser): + errors.Is(err, errDifferentUser), + errors.Is(err, errRelaybotNotLoggedIn): return event.MessageStatusGenericError, event.MessageStatusRetriable, true, false, "" default: return event.MessageStatusGenericError, event.MessageStatusRetriable, false, true, "" @@ -267,8 +269,7 @@ func (mt *messageTimings) String() string { mt.preproc = niceRound(mt.preproc) mt.convert = niceRound(mt.convert) mt.totalSend = niceRound(mt.totalSend) - whatsmeowTimings := "N/A" - return fmt.Sprintf("BRIDGE: receive: %s, decrypt: %s, queue: %s, total hs->portal: %s, implicit rr: %s -- PORTAL: preprocess: %s, convert: %s, total send: %s -- WHATSMEOW: %s", mt.initReceive, mt.decrypt, mt.implicitRR, mt.portalQueue, mt.totalReceive, mt.preproc, mt.convert, mt.totalSend, whatsmeowTimings) + return fmt.Sprintf("BRIDGE: receive: %s, decrypt: %s, queue: %s, total hs->portal: %s, implicit rr: %s -- PORTAL: preprocess: %s, convert: %s, total send: %s ", mt.initReceive, mt.decrypt, mt.implicitRR, mt.portalQueue, mt.totalReceive, mt.preproc, mt.convert, mt.totalSend) } type metricSender struct { diff --git a/portal.go b/portal.go index dd9c90f1..2fb499b1 100644 --- a/portal.go +++ b/portal.go @@ -85,6 +85,8 @@ type Portal struct { currentlyTypingLock sync.Mutex latestReadTimestamp uint64 // Cache the latest read timestamp to avoid unnecessary read receipts + + relayUser *User } const recentMessageBufferSize = 32 @@ -117,11 +119,20 @@ func (portal *Portal) MarkEncrypted() { } func (portal *Portal) ReceiveMatrixEvent(user bridge.User, evt *event.Event) { - if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser { + if user.GetPermissionLevel() >= bridgeconfig.PermissionLevelUser || portal.HasRelaybot() { portal.matrixMessages <- portalMatrixMessage{user: user.(*User), evt: evt} } } +func (portal *Portal) GetRelayUser() *User { + if !portal.HasRelaybot() { + return nil + } else if portal.relayUser == nil { + portal.relayUser = portal.bridge.GetUserByMXID(portal.RelayUserID) + } + return portal.relayUser +} + func isUUID(s string) bool { if _, uuidErr := uuid.Parse(s); uuidErr == nil { return true @@ -263,7 +274,7 @@ func (portal *Portal) messageLoop() { func (portal *Portal) handleMatrixMessages(msg portalMatrixMessage) { // If we have no SignalDevice, the bridge isn't logged in properly, // so send BAD_CREDENTIALS so the user knows - if !msg.user.SignalDevice.IsDeviceLoggedIn() { + if !msg.user.SignalDevice.IsDeviceLoggedIn() && !portal.HasRelaybot() { go portal.sendMessageMetrics(msg.evt, errUserNotLoggedIn, "Ignoring", nil) msg.user.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: "You have been logged out of Signal, please reconnect"}) return @@ -367,7 +378,9 @@ func (portal *Portal) handleMatrixMessage(sender *User, evt *event.Event) { if portal.ExpirationTime > 0 { signalmeow.AddExpiryToDataMessage(msg, uint32(portal.ExpirationTime)) } - + if !sender.IsLoggedIn() { + sender = portal.GetRelayUser() + } err = portal.sendSignalMessage(ctx, msg, sender, evt.ID) timings.totalSend = time.Since(start) @@ -397,6 +410,10 @@ func (portal *Portal) handleMatrixRedaction(sender *User, evt *event.Event) { return } + if !sender.IsLoggedIn() { + sender = portal.GetRelayUser() + } + // If this is a message redaction, send a redaction to Signal if dbMessage != nil { msg := signalmeow.DataMessageForDelete(dedupedTimestamp(dbMessage)) @@ -428,6 +445,10 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { // Find the original signal message based on eventID relatedEventID := evt.Content.AsReaction().RelatesTo.EventID dbMessage := portal.bridge.DB.Message.GetByMXID(relatedEventID) + if !sender.IsLoggedIn() { + portal.log.Error().Msgf("Cannot relay reaction from non-logged-in user. Ignoring") + return + } if dbMessage == nil { portal.sendMessageStatusCheckpointFailed(evt, errors.New("could not find original message for reaction")) portal.log.Error().Msgf("Could not find original message for reaction %s", evt.ID) @@ -438,6 +459,7 @@ func (portal *Portal) handleMatrixReaction(sender *User, evt *event.Event) { targetAuthorUUID := dbMessage.Sender targetTimestamp := dedupedTimestamp(dbMessage) msg := signalmeow.DataMessageForReaction(signalEmoji, targetAuthorUUID, targetTimestamp, false) + err := portal.sendSignalMessage(context.Background(), msg, sender, evt.ID) if err != nil { portal.sendMessageStatusCheckpointFailed(evt, err) @@ -624,15 +646,39 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev if evt.Type == event.EventSticker { content.MsgType = event.MessageType(event.EventSticker.Type) } - + realSenderMXID := sender.MXID + isRelay := false + if !sender.IsLoggedIn() { + if !portal.HasRelaybot() { + return nil, errUserNotLoggedIn + } + sender = portal.GetRelayUser() + if !sender.IsLoggedIn() { + return nil, errRelaybotNotLoggedIn + } + isRelay = true + } var outgoingMessage *signalmeow.SignalContent + relaybotFormatted := isRelay && portal.addRelaybotFormat(realSenderMXID, content) + if relaybotFormatted && content.FileName == "" { + content.FileName = content.Body + } + + if evt.Type == event.EventSticker { + if relaybotFormatted { + // Stickers can't have captions, so force relaybot stickers to be images + content.MsgType = event.MsgImage + } else { + content.MsgType = event.MessageType(event.EventSticker.Type) + } + } switch content.MsgType { case event.MsgText, event.MsgEmote, event.MsgNotice: if content.MsgType == event.MsgNotice && !portal.bridge.Config.Bridge.BridgeNotices { return nil, errMNoticeDisabled } - if content.MsgType == event.MsgEmote { + if content.MsgType == event.MsgEmote && !relaybotFormatted { content.Body = "/me " + content.Body if content.FormattedBody != "" { content.FormattedBody = "/me " + content.FormattedBody @@ -1836,3 +1882,21 @@ func (portal *Portal) HandleNewDisappearingMessageTime(newTimer uint32) { intent.SendNotice(portal.MXID, fmt.Sprintf("Disappearing messages set to %s", exfmt.Duration(time.Duration(newTimer)*time.Second))) } } + +func (portal *Portal) HasRelaybot() bool { + return portal.bridge.Config.Bridge.Relay.Enabled && len(portal.RelayUserID) > 0 +} + +func (portal *Portal) addRelaybotFormat(userID id.UserID, content *event.MessageEventContent) bool { + member := portal.MainIntent().Member(portal.MXID, userID) + if member == nil { + member = &event.MemberEventContent{} + } + content.EnsureHasHTML() + data, err := portal.bridge.Config.Bridge.Relay.FormatMessage(content, userID, *member) + if err != nil { + portal.log.Err(err).Msg("Failed to apply relaybot format") + } + content.FormattedBody = data + return true +} diff --git a/user.go b/user.go index 4bc04d7d..ba866e5f 100644 --- a/user.go +++ b/user.go @@ -54,6 +54,7 @@ type User struct { bridge *SignalBridge log zerolog.Logger + Admin bool PermissionLevel bridgeconfig.PermissionLevel SignalDevice *signalmeow.Device @@ -187,6 +188,7 @@ func (br *SignalBridge) NewUser(dbUser *database.User) *User { PermissionLevel: br.Config.Bridge.Permissions.Get(dbUser.MXID), } + user.Admin = user.PermissionLevel >= bridgeconfig.PermissionLevelAdmin user.BridgeState = br.NewBridgeStateQueue(user) return user } From fb18dcebcf611375ef4887041bc3b79008246178 Mon Sep 17 00:00:00 2001 From: Sumner Evans Date: Tue, 26 Dec 2023 18:39:42 -0700 Subject: [PATCH 12/12] nix: shell -> flake Signed-off-by: Sumner Evans --- .envrc | 6 +++++- flake.lock | 61 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ flake.nix | 33 +++++++++++++++++++++++++++++ shell.nix | 20 ------------------ 4 files changed, 99 insertions(+), 21 deletions(-) create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 shell.nix diff --git a/.envrc b/.envrc index 9d49e6ca..4520fd18 100644 --- a/.envrc +++ b/.envrc @@ -1,4 +1,8 @@ if [[ $(uname -s) == "Linux" && $(uname --kernel-version | grep "NixOS") ]]; then echo "The best OS (NixOS) has been detected. Using nice tools." - use nix + if ! has nix_direnv_version || ! nix_direnv_version 3.0.0; then + source_url "https://raw.githubusercontent.com/nix-community/nix-direnv/3.0.0/direnvrc" "sha256-21TMnI2xWX7HkSTjFFri2UaohXVj854mgvWapWrxRXg=" + fi + + use flake fi diff --git a/flake.lock b/flake.lock new file mode 100644 index 00000000..80c53416 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1703255338, + "narHash": "sha256-Z6wfYJQKmDN9xciTwU3cOiOk+NElxdZwy/FiHctCzjU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "6df37dc6a77654682fe9f071c62b4242b5342e04", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 00000000..cbb2b616 --- /dev/null +++ b/flake.nix @@ -0,0 +1,33 @@ +{ + description = "mautrix-signal development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + (flake-utils.lib.eachDefaultSystem (system: + let pkgs = import nixpkgs { inherit system; }; + in { + devShells.default = pkgs.mkShell { + LIBCLANG_PATH = "${pkgs.llvmPackages_11.libclang.lib}/lib"; + + buildInputs = with pkgs; [ + clang + cmake + gnumake + protobuf + rust-cbindgen + rustup + olm + + go_1_20 + go-tools + gotools + + pre-commit + ]; + }; + })); +} diff --git a/shell.nix b/shell.nix deleted file mode 100644 index 3326dac0..00000000 --- a/shell.nix +++ /dev/null @@ -1,20 +0,0 @@ -with import { }; -let -in -mkShell rec { - buildInputs = [ - clang - cmake - gnumake - protobuf - rust-cbindgen - rustup - olm - - go_1_20 - - pre-commit - ]; - - LIBCLANG_PATH = "${pkgs.llvmPackages_11.libclang.lib}/lib"; -}