Skip to content

Commit

Permalink
connector: add matrix->slack message bridging
Browse files Browse the repository at this point in the history
  • Loading branch information
tulir committed Jul 10, 2024
1 parent b6b8a1a commit 2b8567a
Show file tree
Hide file tree
Showing 3 changed files with 126 additions and 61 deletions.
51 changes: 48 additions & 3 deletions pkg/connector/handlematrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,56 @@ package connector

import (
"context"
"errors"

"github.com/rs/zerolog"

"github.com/slack-go/slack"
"go.mau.fi/mautrix-slack/pkg/slackid"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
)

func (s *SlackClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (message *bridgev2.MatrixMessageResponse, err error) {
//TODO implement me
panic("implement me")
func (s *SlackClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
_, channelID := slackid.ParsePortalID(msg.Portal.ID)
if channelID == "" {
return nil, errors.New("invalid channel ID")
}
sendOpts, fileUpload, err := s.Main.MsgConv.ToSlack(ctx, msg.Portal, msg.Content, msg.Event, msg.ThreadRoot, nil)
if err != nil {
return nil, err
}
log := zerolog.Ctx(ctx)
var timestamp string
if sendOpts != nil {
log.Debug().Msg("Sending message to Slack")
_, timestamp, err = s.Client.PostMessageContext(ctx, channelID, slack.MsgOptionAsUser(true), sendOpts)
if err != nil {
return nil, err
}
} else if fileUpload != nil {
log.Debug().Msg("Uploading attachment to Slack")
file, err := s.Client.UploadFileContext(ctx, *fileUpload)
if err != nil {
log.Err(err).Msg("Failed to upload attachment to Slack")
return nil, err
}
var shareInfo slack.ShareFileInfo
// Slack puts the channel message info after uploading a file in either file.shares.private or file.shares.public
if info, found := file.Shares.Private[channelID]; found && len(info) > 0 {
shareInfo = info[0]
} else if info, found = file.Shares.Public[channelID]; found && len(info) > 0 {
shareInfo = info[0]
} else {
return nil, errors.New("failed to upload media to Slack")
}
timestamp = shareInfo.Ts
}
return &bridgev2.MatrixMessageResponse{
DB: &database.Message{
ID: slackid.MakeMessageID(s.TeamID, channelID, timestamp),
SenderID: slackid.MakeUserID(s.TeamID, s.UserID),
Timestamp: slackid.ParseSlackTimestamp(timestamp),
},
}, nil
}
54 changes: 53 additions & 1 deletion pkg/connector/handleslack.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"github.com/rs/zerolog"
"github.com/slack-go/slack"
"maunium.net/go/mautrix/bridgev2/database"

"maunium.net/go/mautrix/bridge/status"
"maunium.net/go/mautrix/bridgev2"
Expand Down Expand Up @@ -381,8 +382,59 @@ type SlackMessage struct {
Client *SlackClient
}

var _ bridgev2.RemoteMessage = (*SlackMessage)(nil)
var (
_ bridgev2.RemoteMessage = (*SlackMessage)(nil)
_ bridgev2.RemoteEdit = (*SlackMessage)(nil)
_ bridgev2.RemoteMessageRemove = (*SlackMessage)(nil)
_ bridgev2.RemoteChatInfoChange = (*SlackMessage)(nil)
)

func (s *SlackMessage) GetType() bridgev2.RemoteEventType {
switch s.Data.SubType {
case slack.MsgSubTypeMessageChanged:
return bridgev2.RemoteEventEdit
case slack.MsgSubTypeMessageDeleted:
return bridgev2.RemoteEventMessageRemove
case slack.MsgSubTypeChannelTopic, slack.MsgSubTypeChannelPurpose, slack.MsgSubTypeChannelName,
slack.MsgSubTypeGroupTopic, slack.MsgSubTypeGroupPurpose, slack.MsgSubTypeGroupName:
return bridgev2.RemoteEventChatInfoChange
case slack.MsgSubTypeMessageReplied, slack.MsgSubTypeGroupJoin, slack.MsgSubTypeGroupLeave,
slack.MsgSubTypeChannelJoin, slack.MsgSubTypeChannelLeave:
return bridgev2.RemoteEventUnknown
default:
return bridgev2.RemoteEventMessage
}
}

func (s *SlackMessage) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
return s.Client.Main.MsgConv.ToMatrix(ctx, portal, intent, s.Client.UserLogin, &s.Data.Msg), nil
}

func (s *SlackMessage) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
//msg := s.Data.SubMessage
//oldMsg := s.Data.PreviousMessage
//TODO implement me
panic("implement me")
}

func (s *SlackMessage) GetTimestamp() time.Time {
switch s.Data.SubType {
case slack.MsgSubTypeMessageChanged:
return slackid.ParseSlackTimestamp(s.Data.SubMessage.Timestamp)
case slack.MsgSubTypeMessageDeleted:
return slackid.ParseSlackTimestamp(s.Data.DeletedTimestamp)
default:
return s.Timestamp
}
}

func (s *SlackMessage) GetTargetMessage() networkid.MessageID {
return s.ID
}

func (s *SlackMessage) GetChatInfoChange(ctx context.Context) (*bridgev2.ChatInfoChange, error) {
switch s.Data.SubType {
// TODO
}
return nil, nil
}
82 changes: 25 additions & 57 deletions pkg/msgconv/from-matrix.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,86 +20,54 @@ import (
"bytes"
"context"
"errors"
"fmt"
"image"
"strings"

"github.com/rs/zerolog"

"github.com/slack-go/slack"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"github.com/slack-go/slack"

"go.mau.fi/mautrix-slack/pkg/slackid"
)

var (
ErrUnexpectedParsedContentType = errors.New("unexpected parsed content type")
ErrUnknownMsgType = errors.New("unknown msgtype")
ErrMediaDownloadFailed = errors.New("failed to download media")
ErrMediaOnlyEditCaption = errors.New("only media message caption can be edited")
ErrEditTargetNotFound = errors.New("edit target message not found")
ErrThreadRootNotFound = errors.New("thread root message not found")
ErrUnknownMsgType = errors.New("unknown msgtype")
ErrMediaDownloadFailed = errors.New("failed to download media")
ErrMediaOnlyEditCaption = errors.New("only media message caption can be edited")
)

func isMediaMsgtype(msgType event.MessageType) bool {
return msgType == event.MsgImage || msgType == event.MsgAudio || msgType == event.MsgVideo || msgType == event.MsgFile
}

func (mc *MessageConverter) ToSlack(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, evt *event.Event) (sendReq slack.MsgOption, fileUpload *slack.FileUploadParameters, threadRootID string, editTarget *database.Message, err error) {
func (mc *MessageConverter) ToSlack(
ctx context.Context,
portal *bridgev2.Portal,
content *event.MessageEventContent,
evt *event.Event,
threadRoot *database.Message,
editTarget []*database.Message,
) (sendReq slack.MsgOption, fileUpload *slack.FileUploadParameters, err error) {
log := zerolog.Ctx(ctx)
content, ok := evt.Content.Parsed.(*event.MessageEventContent)
if !ok {
return nil, nil, "", nil, ErrUnexpectedParsedContentType
}

if evt.Type == event.EventSticker {
// Slack doesn't have stickers, just bridge stickers as images
content.MsgType = event.MsgImage
}

var editTargetID string
if replaceEventID := content.RelatesTo.GetReplaceID(); replaceEventID != "" {
existing, err := mc.Bridge.DB.Message.GetPartByMXID(ctx, replaceEventID)
if err != nil {
log.Err(err).Msg("Failed to get edit target message")
return nil, nil, "", nil, fmt.Errorf("failed to get edit target message: %w", err)
} else if existing == nil {
return nil, nil, "", nil, ErrEditTargetNotFound
} else {
_, _, editTargetID, _ = slackid.ParseMessageID(existing.ID)
editTarget = existing
if content.NewContent != nil {
content = content.NewContent
}
}
} else {
var threadMXID id.EventID
threadMXID = content.RelatesTo.GetThreadParent()
if threadMXID == "" {
threadMXID = content.RelatesTo.GetReplyTo()
}
if threadMXID != "" {
rootMessage, err := mc.Bridge.DB.Message.GetPartByMXID(ctx, threadMXID)
if err != nil {
return nil, nil, "", nil, fmt.Errorf("failed to get thread root message: %w", err)
} else if rootMessage == nil {
return nil, nil, "", nil, ErrThreadRootNotFound
} else if rootMessage.RelatesToRowID != 0 {
// TODO get real thread root
} else {
_, _, threadRootID, _ = slackid.ParseMessageID(rootMessage.ID)
}
}
}

if editTargetID != "" && isMediaMsgtype(content.MsgType) {
var editTargetID, threadRootID string
if editTarget != nil && isMediaMsgtype(content.MsgType) {
content.MsgType = event.MsgText
if content.FileName == "" || content.FileName == content.Body {
return nil, nil, "", editTarget, ErrMediaOnlyEditCaption
return nil, nil, ErrMediaOnlyEditCaption
}
_, _, editTargetID, _ = slackid.ParseMessageID(editTarget[0].ID)
}
if threadRoot != nil {
_, _, threadRootID, _ = slackid.ParseMessageID(threadRoot.ID)
}

switch content.MsgType {
Expand All @@ -120,12 +88,12 @@ func (mc *MessageConverter) ToSlack(ctx context.Context, portal *bridgev2.Portal
if content.MsgType == event.MsgEmote {
options = append(options, slack.MsgOptionMeMessage())
}
return slack.MsgOptionCompose(options...), nil, threadRootID, editTarget, nil
return slack.MsgOptionCompose(options...), nil, nil
case event.MsgAudio, event.MsgFile, event.MsgImage, event.MsgVideo:
data, err := intent.DownloadMedia(ctx, content.URL, content.File)
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
if err != nil {
log.Err(err).Msg("Failed to download Matrix attachment")
return nil, nil, "", editTarget, ErrMediaDownloadFailed
return nil, nil, ErrMediaDownloadFailed
}

var filename, caption string
Expand All @@ -146,9 +114,9 @@ func (mc *MessageConverter) ToSlack(ctx context.Context, portal *bridgev2.Portal
if caption != "" {
fileUpload.InitialComment = caption
}
return nil, fileUpload, threadRootID, editTarget, nil
return nil, fileUpload, nil
default:
return nil, nil, "", editTarget, ErrUnknownMsgType
return nil, nil, ErrUnknownMsgType
}
}

Expand Down

0 comments on commit 2b8567a

Please sign in to comment.