From 53c933c8f1564cf3c54df8b22de2cf1991e72480 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Fri, 10 Jan 2025 18:04:30 +0200 Subject: [PATCH] capabilities: update to new format --- go.mod | 14 +-- go.sum | 28 +++--- pkg/connector/capabilities.go | 160 ++++++++++++++++++++++++++++++---- pkg/connector/chatinfo.go | 17 +++- pkg/connector/client.go | 4 +- pkg/msgconv/from-matrix.go | 6 +- pkg/slackid/dbmeta.go | 4 +- 7 files changed, 185 insertions(+), 48 deletions(-) diff --git a/go.mod b/go.mod index cf2504f..beafc6f 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,10 @@ require ( github.com/slack-go/slack v0.15.0 github.com/stretchr/testify v1.10.0 github.com/yuin/goldmark v1.7.8 - go.mau.fi/util v0.8.3 - golang.org/x/net v0.32.0 + go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957 + golang.org/x/net v0.33.0 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.22.2-0.20241223114659-ba210a16b992 + maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268 ) require ( @@ -34,13 +34,13 @@ require ( github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/sjson v1.2.5 // indirect go.mau.fi/zeroconfig v0.1.3 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 // indirect golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect ) -replace github.com/slack-go/slack => github.com/beeper/slackgo v0.0.0-20241223114722-b8b7a4c49a18 +replace github.com/slack-go/slack => github.com/beeper/slackgo v0.0.0-20250110160232-abcc246721a4 diff --git a/go.sum b/go.sum index e95d07f..0164798 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= -github.com/beeper/slackgo v0.0.0-20241223114722-b8b7a4c49a18 h1:s0gVLeb7pgYBIlwuUmFzMvDbxVWEWUvjDJaqurE5LI4= -github.com/beeper/slackgo v0.0.0-20241223114722-b8b7a4c49a18/go.mod h1:axoegr/0xf8uWt4I+coY6x+CVKPbWGs4YqpoYbCBRr8= +github.com/beeper/slackgo v0.0.0-20250110160232-abcc246721a4 h1:2xOB+RTGKAbnbILUmuqh+bTg6a40mQjoa+P2s5I5CPc= +github.com/beeper/slackgo v0.0.0-20250110160232-abcc246721a4/go.mod h1:axoegr/0xf8uWt4I+coY6x+CVKPbWGs4YqpoYbCBRr8= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -51,23 +51,23 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic= github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E= -go.mau.fi/util v0.8.3 h1:sulhXtfquMrQjsOP67x9CzWVBYUwhYeoo8hNQIpCWZ4= -go.mau.fi/util v0.8.3/go.mod h1:c00Db8xog70JeIsEvhdHooylTkTkakgnAOsZ04hplQY= +go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957 h1:tsLt3t6ARc55niz+JMgJy6U4sL210Z0K/nyxF09xT0E= +go.mau.fi/util v0.8.4-0.20250110124612-64d4dbbec957/go.mod h1:MOfGTs1CBuK6ERTcSL4lb5YU7/ujz09eOPVEDckuazY= go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM= go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e h1:4qufH0hlUYs6AO6XmZC3GqfDPGSXHVXUFR6OND+iJX4= -golang.org/x/exp v0.0.0-20241215155358-4a5509556b9e/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329 h1:9kj3STMvgqy3YA4VQXBrN7925ICMxD5wzMRcgA30588= +golang.org/x/exp v0.0.0-20250103183323-7d7fa50e5329/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= @@ -78,5 +78,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.22.2-0.20241223114659-ba210a16b992 h1:7KLNq84HGVzpb7iBCtKTq5NhjDWh/r3Bj8Qmtv5gXy4= -maunium.net/go/mautrix v0.22.2-0.20241223114659-ba210a16b992/go.mod h1:1rhqwH34Rz54ZqzdQYkmNW6rQUymNeTdaLA4l9LK6AI= +maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268 h1:p+3TofdhqiVYIkLjgzidayg2XriGUEbj+nbWs3/UQbk= +maunium.net/go/mautrix v0.22.2-0.20250110154103-bbcb1904e268/go.mod h1:07i96D7BALyuAqxFhRzvaId8FC9NABgRQBPY5HWndf4= diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 855ff81..85e37b9 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -18,8 +18,16 @@ package connector import ( "context" + "strconv" + "time" + "go.mau.fi/util/ffmpeg" + "go.mau.fi/util/jsontime" + "go.mau.fi/util/ptr" "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/event" + + "go.mau.fi/mautrix-slack/pkg/slackid" ) func (s *SlackConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities { @@ -30,25 +38,139 @@ func (s *SlackConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities } } -var roomCaps = &bridgev2.NetworkRoomCapabilities{ - FormattedText: true, - UserMentions: true, - RoomMentions: true, - LocationMessages: false, - Captions: true, - MaxTextLength: 40000, - MaxCaptionLength: 40000, - Threads: true, - Replies: false, - Edits: true, - EditMaxAge: 0, // TODO workspaces can have edit max age limits - Deletes: true, - DefaultFileRestriction: &bridgev2.FileRestriction{MaxSize: 1 * 1000 * 1000 * 1000}, - ReadReceipts: false, - Reactions: true, - ReactionCount: 0, // unlimited +func (s *SlackConnector) GetBridgeInfoVersion() (info, caps int) { + return 1, 1 +} + +func supportedIfFFmpeg() event.CapabilitySupportLevel { + if ffmpeg.Supported() { + return event.CapLevelPartialSupport + } + return event.CapLevelRejected +} + +func capID() string { + base := "fi.mau.slack.capabilities.2025_01_10" + if ffmpeg.Supported() { + return base + "+ffmpeg" + } + return base } -func (s *SlackClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities { - return roomCaps +const MaxFileSize = 1 * 1000 * 1000 * 1000 +const MaxTextLength = 40000 + +var roomCaps = &event.RoomFeatures{ + ID: capID(), + Formatting: event.FormattingFeatureMap{ + event.FmtBold: event.CapLevelFullySupported, + event.FmtItalic: event.CapLevelFullySupported, + event.FmtStrikethrough: event.CapLevelFullySupported, + event.FmtInlineCode: event.CapLevelFullySupported, + event.FmtCodeBlock: event.CapLevelFullySupported, + event.FmtSyntaxHighlighting: event.CapLevelDropped, + event.FmtBlockquote: event.CapLevelFullySupported, + event.FmtInlineLink: event.CapLevelFullySupported, + event.FmtUserLink: event.CapLevelFullySupported, + event.FmtRoomLink: event.CapLevelFullySupported, + event.FmtEventLink: event.CapLevelUnsupported, + event.FmtAtRoomMention: event.CapLevelFullySupported, + event.FmtUnorderedList: event.CapLevelFullySupported, + event.FmtOrderedList: event.CapLevelFullySupported, + event.FmtListStart: event.CapLevelFullySupported, + event.FmtListJumpValue: event.CapLevelDropped, + event.FmtCustomEmoji: event.CapLevelFullySupported, + }, + File: event.FileFeatureMap{ + event.MsgImage: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + "image/jpeg": event.CapLevelFullySupported, + "image/png": event.CapLevelFullySupported, + "image/gif": event.CapLevelFullySupported, + "image/webp": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + }, + event.MsgVideo: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + "video/mp4": event.CapLevelFullySupported, + "video/webm": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + }, + event.MsgAudio: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + "audio/mp3": event.CapLevelFullySupported, + "audio/webm": event.CapLevelFullySupported, + "audio/wav": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + }, + event.MsgFile: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + // TODO Slack Connect rejects some types + // https://slack.com/intl/en-gb/help/articles/1500002249342-Restricted-file-types-in-Slack-Connect + "*/*": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + }, + event.CapMsgGIF: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + "image/gif": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + }, + event.CapMsgVoice: { + MimeTypes: map[string]event.CapabilitySupportLevel{ + "audio/ogg": supportedIfFFmpeg(), + "audio/webm; codecs=opus": event.CapLevelFullySupported, + }, + Caption: event.CapLevelFullySupported, + MaxCaptionLength: MaxTextLength, + MaxSize: MaxFileSize, + MaxDuration: ptr.Ptr(jsontime.S(5 * time.Minute)), + }, + }, + LocationMessage: event.CapLevelRejected, + MaxTextLength: MaxTextLength, + Thread: event.CapLevelFullySupported, + Edit: event.CapLevelFullySupported, + EditMaxAge: nil, + Delete: event.CapLevelFullySupported, + Reaction: event.CapLevelFullySupported, +} + +func (s *SlackClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures { + meta := &slackid.PortalMetadata{} + topLevel := portal.GetTopLevelParent() + if topLevel != nil { + meta = topLevel.Metadata.(*slackid.PortalMetadata) + } + caps := roomCaps + if meta.EditMaxAge != nil && *meta.EditMaxAge >= 0 { + caps = ptr.Clone(roomCaps) + caps.ID += "+edit_max_age=" + strconv.Itoa(*meta.EditMaxAge) + caps.EditMaxAge = ptr.Ptr(jsontime.S(time.Duration(*meta.EditMaxAge) * time.Minute)) + if *meta.EditMaxAge == 0 { + caps.Edit = event.CapLevelRejected + } + } + if meta.AllowDelete != nil && !*meta.AllowDelete { + if caps == roomCaps { + caps = ptr.Clone(roomCaps) + } + caps.ID += "+disallow_delete" + caps.Delete = event.CapLevelRejected + } + return caps } diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 277a623..e7578f8 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -214,7 +214,7 @@ func (s *SlackClient) wrapChatInfo(ctx context.Context, info *slack.Channel, isN if roomType != database.RoomTypeDM || len(members.MemberMap) == 1 { name = ptr.Ptr(s.Main.Config.FormatChannelName(&ChannelNameParams{ Channel: info, - Team: &s.BootResp.Team, + Team: &s.BootResp.Team.TeamInfo, IsNoteToSelf: info.IsIM && info.User == s.UserID, })) } @@ -242,7 +242,7 @@ func (s *SlackClient) fetchChatInfo(ctx context.Context, channelID string, isNew } func (s *SlackClient) getTeamInfo() *bridgev2.ChatInfo { - name := s.Main.Config.FormatTeamName(&s.BootResp.Team) + name := s.Main.Config.FormatTeamName(&s.BootResp.Team.TeamInfo) avatarURL, _ := s.BootResp.Team.Icon["image_230"].(string) if s.BootResp.Team.Icon["image_default"] == true { avatarURL = "" @@ -265,6 +265,15 @@ func (s *SlackClient) getTeamInfo() *bridgev2.ChatInfo { meta.TeamDomain = s.BootResp.Team.Domain changed = true } + prefs := s.BootResp.Team.Prefs + if prefs.MsgEditWindowMins != nil && (meta.EditMaxAge == nil || *meta.EditMaxAge != *prefs.MsgEditWindowMins) { + meta.EditMaxAge = prefs.MsgEditWindowMins + changed = true + } + if prefs.AllowMessageDeletion != nil && (meta.AllowDelete == nil || *meta.AllowDelete != *prefs.AllowMessageDeletion) { + meta.AllowDelete = prefs.AllowMessageDeletion + changed = true + } return }, } @@ -303,7 +312,7 @@ func (s *SlackClient) wrapUserInfo(userID string, info *slack.User, botInfo *sla if info != nil { name = ptr.Ptr(s.Main.Config.FormatDisplayname(&DisplaynameParams{ User: info, - Team: &s.BootResp.Team, + Team: &s.BootResp.Team.TeamInfo, })) avatarURL := info.Profile.ImageOriginal if avatarURL == "" && info.Profile.Image512 != "" { @@ -325,7 +334,7 @@ func (s *SlackClient) wrapUserInfo(userID string, info *slack.User, botInfo *sla } isBot = isBot || info.IsBot || info.IsAppUser } else if botInfo != nil { - name = ptr.Ptr(s.Main.Config.FormatBotDisplayname(botInfo, &s.BootResp.Team)) + name = ptr.Ptr(s.Main.Config.FormatBotDisplayname(botInfo, &s.BootResp.Team.TeamInfo)) avatar = makeAvatar(botInfo.Icons.Image72, botInfo.Icons.Image72) isBot = true } diff --git a/pkg/connector/client.go b/pkg/connector/client.go index bae04d6..97f62c2 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -189,7 +189,9 @@ func (s *SlackClient) Connect(ctx context.Context) { } bootResp = &slack.ClientUserBootResponse{ Self: *userResp, - Team: *teamResp, + Team: slack.BootTeam{ + TeamInfo: *teamResp, + }, } } err := s.connect(ctx, bootResp) diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 8966871..f35d3f7 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -143,14 +143,16 @@ func (mc *MessageConverter) ToSlack( caption = content.Body captionHTML = content.FormattedBody } - if content.MSC3245Voice != nil && ffmpeg.Supported() { + if content.MSC3245Voice != nil && content.Info.MimeType != "audio/webm; codecs=opus" && ffmpeg.Supported() { data, err = ffmpeg.ConvertBytes(ctx, data, ".webm", []string{}, []string{"-c:a", "copy"}, content.Info.MimeType) if err != nil { log.Err(err).Msg("Failed to convert voice message") return nil, ErrMediaConvertFailed } filename += ".webm" - content.Info.MimeType = "audio/webm;codecs=opus" + content.Info.MimeType = "audio/webm; codecs=opus" + subtype = "slack_audio" + } else if content.MSC3245Voice != nil && content.Info.MimeType == "audio/webm; codecs=opus" { subtype = "slack_audio" } _, channelID := slackid.ParsePortalID(portal.ID) diff --git a/pkg/slackid/dbmeta.go b/pkg/slackid/dbmeta.go index 86f8ac3..3fec94b 100644 --- a/pkg/slackid/dbmeta.go +++ b/pkg/slackid/dbmeta.go @@ -22,7 +22,9 @@ import ( type PortalMetadata struct { // Only present for team portals, not channels - TeamDomain string `json:"team_domain"` + TeamDomain string `json:"team_domain,omitempty"` + EditMaxAge *int `json:"edit_max_age,omitempty"` + AllowDelete *bool `json:"allow_delete,omitempty"` } type GhostMetadata struct {