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/commands.go b/commands.go
index bef20657..a27112be 100644
--- a/commands.go
+++ b/commands.go
@@ -51,6 +51,8 @@ func (br *SignalBridge) RegisterCommands() {
cmdLogin,
cmdPM,
cmdDisconnect,
+ cmdSetRelay,
+ cmdUnsetRelay,
)
}
@@ -66,6 +68,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(context.TODO())
+ 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(context.TODO())
+ 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/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/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/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(),
diff --git a/pkg/signalmeow/sending.go b/pkg/signalmeow/sending.go
index ddfc481b..de90b706 100644
--- a/pkg/signalmeow/sending.go
+++ b/pkg/signalmeow/sending.go
@@ -431,6 +431,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)
@@ -462,13 +464,15 @@ func DataMessageForDelete(targetMessageTimestamp uint64) *SignalContent {
}
func AddQuoteToDataMessage(content *SignalContent, quotedMessageSender uuid.UUID, 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.String()),
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{{}},
}
}
diff --git a/portal.go b/portal.go
index fb8ecaba..0382d51b 100644
--- a/portal.go
+++ b/portal.go
@@ -35,6 +35,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"
@@ -85,6 +86,8 @@ type Portal struct {
currentlyTypingLock sync.Mutex
latestReadTimestamp uint64 // Cache the latest read timestamp to avoid unnecessary read receipts
+
+ relayUser *User
}
const recentMessageBufferSize = 32
@@ -120,11 +123,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
@@ -264,8 +276,8 @@ 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() {
- portal.sendMessageStatusCheckpointFailed(msg.evt, errUserNotLoggedIn)
+ 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
}
@@ -352,7 +364,9 @@ func (portal *Portal) handleMatrixMessage(ctx context.Context, sender *User, evt
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)
@@ -383,6 +397,11 @@ func (portal *Portal) handleMatrixRedaction(ctx context.Context, sender *User, e
return
}
+ if !sender.IsLoggedIn() {
+ sender = portal.GetRelayUser()
+ }
+
+ // If this is a message redaction, send a redaction to Signal
if dbMessage != nil {
msg := signalmeow.DataMessageForDelete(dbMessage.Timestamp)
err = portal.sendSignalMessage(ctx, msg, sender, evt.ID)
@@ -440,6 +459,10 @@ func (portal *Portal) handleMatrixRedaction(ctx context.Context, sender *User, e
func (portal *Portal) handleMatrixReaction(ctx context.Context, sender *User, evt *event.Event) {
log := zerolog.Ctx(ctx)
+ if !sender.IsLoggedIn() {
+ log.Error().Msg("Cannot relay reaction from non-logged-in user. Ignoring")
+ return
+ }
// Find the original signal message based on eventID
relatedEventID := evt.Content.AsReaction().RelatesTo.EventID
dbMessage, err := portal.bridge.DB.Message.GetByMXID(ctx, relatedEventID)
@@ -637,28 +660,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 {
@@ -668,15 +669,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
@@ -691,7 +716,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,9 +732,16 @@ 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):
+ 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
@@ -722,12 +754,29 @@ func (portal *Portal) convertMatrixMessage(ctx context.Context, sender *User, ev
if err != nil {
return nil, err
}
- outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, "", nil)
+ 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())),
+ 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
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)
}
@@ -749,29 +798,38 @@ 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)
}
- 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 {
+ attachmentPointer.Flags = proto.Uint32(uint32(signalpb.AttachmentPointer_VOICE_MESSAGE))
+ }
outgoingMessage = signalmeow.DataMessageForAttachment(attachmentPointer, caption, ranges)
case event.MsgFile:
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)
}
@@ -1813,3 +1871,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/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";
-}
diff --git a/user.go b/user.go
index da1df5bf..00668670 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
@@ -75,7 +76,7 @@ func (user *User) IsLoggedIn() bool {
user.Lock()
defer user.Unlock()
- return user.SignalUsername != ""
+ return user.SignalDevice.IsDeviceLoggedIn()
}
func (user *User) GetManagementRoomID() id.RoomID {
@@ -201,6 +202,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
}
@@ -781,13 +783,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)