f56f520308
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
407 lines
12 KiB
Go
407 lines
12 KiB
Go
package connector
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"math/rand"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gotd/td/telegram/message"
|
|
"github.com/gotd/td/telegram/uploader"
|
|
"github.com/gotd/td/tg"
|
|
"github.com/rs/zerolog"
|
|
"maunium.net/go/mautrix/bridgev2"
|
|
"maunium.net/go/mautrix/bridgev2/database"
|
|
"maunium.net/go/mautrix/bridgev2/networkid"
|
|
"maunium.net/go/mautrix/event"
|
|
"maunium.net/go/mautrix/id"
|
|
|
|
"go.mau.fi/util/variationselector"
|
|
|
|
"go.mau.fi/mautrix-telegram/pkg/connector/emojis"
|
|
"go.mau.fi/mautrix-telegram/pkg/connector/ids"
|
|
"go.mau.fi/mautrix-telegram/pkg/connector/matrixfmt"
|
|
"go.mau.fi/mautrix-telegram/pkg/connector/waveform"
|
|
)
|
|
|
|
func getMediaFilename(content *event.MessageEventContent) string {
|
|
if content.FileName != "" {
|
|
return content.FileName
|
|
} else {
|
|
return content.Body
|
|
}
|
|
}
|
|
|
|
func (t *TelegramClient) transferMediaToTelegram(ctx context.Context, content *event.MessageEventContent) (tg.InputMediaClass, error) {
|
|
filename := getMediaFilename(content)
|
|
var fileData []byte
|
|
fileData, err := t.main.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to download media from Matrix: %w", err)
|
|
}
|
|
uploader := uploader.NewUploader(t.client.API())
|
|
var upload tg.InputFileClass
|
|
upload, err = uploader.FromBytes(ctx, filename, fileData)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to upload media to Telegram: %w", err)
|
|
}
|
|
|
|
if content.MsgType == event.MsgImage {
|
|
return &tg.InputMediaUploadedPhoto{File: upload}, nil
|
|
}
|
|
|
|
var attributes []tg.DocumentAttributeClass
|
|
attributes = append(attributes, &tg.DocumentAttributeFilename{FileName: filename})
|
|
|
|
if content.MsgType == event.MsgAudio {
|
|
audioAttr := tg.DocumentAttributeAudio{
|
|
Voice: content.MSC3245Voice != nil,
|
|
}
|
|
if content.MSC1767Audio != nil {
|
|
audioAttr.Duration = content.MSC1767Audio.Duration / 1000
|
|
if len(content.MSC1767Audio.Waveform) > 0 {
|
|
audioAttr.Waveform = waveform.Encode(content.MSC1767Audio.Waveform)
|
|
}
|
|
}
|
|
attributes = append(attributes, &audioAttr)
|
|
}
|
|
|
|
return &tg.InputMediaUploadedDocument{
|
|
File: upload,
|
|
MimeType: content.Info.MimeType,
|
|
Attributes: attributes,
|
|
}, nil
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (resp *bridgev2.MatrixMessageResponse, err error) {
|
|
peer, err := t.inputPeerForPortalID(ctx, msg.Portal.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var contentURI id.ContentURIString
|
|
// TODO handle sticker
|
|
|
|
noWebpage := msg.Content.BeeperLinkPreviews != nil && len(msg.Content.BeeperLinkPreviews) == 0
|
|
|
|
message, entities := matrixfmt.Parse(ctx, t.matrixParser, msg.Content)
|
|
|
|
var replyTo tg.InputReplyToClass
|
|
if msg.ReplyTo != nil {
|
|
messageID, err := ids.ParseMessageID(msg.ReplyTo.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
replyTo = &tg.InputReplyToMessage{ReplyToMsgID: messageID}
|
|
}
|
|
|
|
var updates tg.UpdatesClass
|
|
switch msg.Content.MsgType {
|
|
case event.MsgText, event.MsgNotice:
|
|
updates, err = t.client.API().MessagesSendMessage(ctx, &tg.MessagesSendMessageRequest{
|
|
Peer: peer,
|
|
NoWebpage: noWebpage,
|
|
Message: message,
|
|
Entities: entities,
|
|
ReplyTo: replyTo,
|
|
RandomID: rand.Int63(),
|
|
})
|
|
case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo:
|
|
var media tg.InputMediaClass
|
|
media, err = t.transferMediaToTelegram(ctx, msg.Content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
updates, err = t.client.API().MessagesSendMedia(ctx, &tg.MessagesSendMediaRequest{
|
|
Peer: peer,
|
|
Message: message,
|
|
Entities: entities,
|
|
Media: media,
|
|
ReplyTo: replyTo,
|
|
RandomID: rand.Int63(),
|
|
})
|
|
case event.MsgLocation:
|
|
var uri GeoURI
|
|
uri, err = ParseGeoURI(msg.Content.GeoURI)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
message = ""
|
|
if location, ok := msg.Event.Content.Raw["org.matrix.msc3488.location"].(map[string]any); ok {
|
|
if desc, ok := location["description"].(string); ok {
|
|
message = desc
|
|
}
|
|
}
|
|
updates, err = t.client.API().MessagesSendMedia(ctx, &tg.MessagesSendMediaRequest{
|
|
Peer: peer,
|
|
Message: message,
|
|
Media: &tg.InputMediaGeoPoint{
|
|
GeoPoint: &tg.InputGeoPoint{Lat: uri.Lat, Long: uri.Long},
|
|
},
|
|
ReplyTo: replyTo,
|
|
RandomID: rand.Int63(),
|
|
})
|
|
default:
|
|
return nil, fmt.Errorf("unsupported message type %s", msg.Content.MsgType)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
|
|
var tgMessageID, tgDate int
|
|
switch sentMessage := updates.(type) {
|
|
case *tg.UpdateShortSentMessage:
|
|
tgMessageID = sentMessage.ID
|
|
tgDate = sentMessage.Date
|
|
hasher.Write([]byte(msg.Content.Body))
|
|
case *tg.Updates:
|
|
tgDate = sentMessage.Date
|
|
for _, u := range sentMessage.Updates {
|
|
switch update := u.(type) {
|
|
case *tg.UpdateMessageID:
|
|
tgMessageID = update.ID
|
|
case *tg.UpdateNewMessage:
|
|
msg := update.Message.(*tg.Message)
|
|
hasher.Write([]byte(msg.Message))
|
|
hasher.Write(mediaHashID(msg.Media))
|
|
}
|
|
}
|
|
if tgMessageID == 0 {
|
|
return nil, fmt.Errorf("couldn't find update message ID update")
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("unknown update from message response %T", updates)
|
|
}
|
|
|
|
resp = &bridgev2.MatrixMessageResponse{
|
|
DB: &database.Message{
|
|
ID: ids.MakeMessageID(tgMessageID),
|
|
MXID: msg.Event.ID,
|
|
Room: networkid.PortalKey{ID: msg.Portal.ID},
|
|
SenderID: t.userID,
|
|
Timestamp: time.Unix(int64(tgDate), 0),
|
|
Metadata: &MessageMetadata{
|
|
ContentHash: hasher.Sum(nil),
|
|
ContentURI: contentURI,
|
|
},
|
|
},
|
|
}
|
|
return
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
|
|
log := zerolog.Ctx(ctx).With().
|
|
Str("conversion_direction", "to_telegram").
|
|
Str("handler", "matrix_edit").
|
|
Logger()
|
|
|
|
peer, err := t.inputPeerForPortalID(ctx, msg.Portal.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
targetID, err := ids.ParseMessageID(msg.EditTarget.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
message, entities := matrixfmt.Parse(ctx, t.matrixParser, msg.Content)
|
|
|
|
var newContentURI id.ContentURIString
|
|
req := tg.MessagesEditMessageRequest{
|
|
Peer: peer,
|
|
NoWebpage: msg.Content.BeeperLinkPreviews != nil && len(msg.Content.BeeperLinkPreviews) == 0,
|
|
Message: message,
|
|
Entities: entities,
|
|
ID: targetID,
|
|
}
|
|
if msg.Content.MsgType.IsMedia() {
|
|
newContentURI = msg.Content.URL
|
|
if newContentURI == "" {
|
|
newContentURI = msg.Content.File.URL
|
|
}
|
|
if msg.EditTarget.Metadata.(*MessageMetadata).ContentURI == newContentURI {
|
|
log.Info().Msg("media URI unchanged, skipping re-upload, just editing text")
|
|
} else {
|
|
log.Info().Msg("media URI changed, re-uploading media")
|
|
req.Media, err = t.transferMediaToTelegram(ctx, msg.Content)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
} else if !msg.Content.MsgType.IsText() {
|
|
return fmt.Errorf("editing message type %s is unsupported", msg.Content.MsgType)
|
|
}
|
|
updates, err := t.client.API().MessagesEditMessage(ctx, &req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
hasher := sha256.New()
|
|
|
|
switch sentMessage := updates.(type) {
|
|
case *tg.UpdateShortSentMessage:
|
|
hasher.Write([]byte(msg.Content.Body))
|
|
case *tg.Updates:
|
|
for _, u := range sentMessage.Updates {
|
|
switch update := u.(type) {
|
|
case *tg.UpdateNewMessage:
|
|
msg := update.Message.(*tg.Message)
|
|
hasher.Write([]byte(msg.Message))
|
|
hasher.Write(mediaHashID(msg.Media))
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("unknown update from message response %T", updates)
|
|
}
|
|
|
|
metadata := msg.EditTarget.Metadata.(*MessageMetadata)
|
|
metadata.ContentHash = hasher.Sum(nil)
|
|
metadata.ContentURI = newContentURI
|
|
return nil
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
|
|
if dbMsg, err := t.main.Bridge.DB.Message.GetPartByMXID(ctx, msg.TargetMessage.MXID); err != nil {
|
|
return err
|
|
} else if messageID, err := ids.ParseMessageID(dbMsg.ID); err != nil {
|
|
return err
|
|
} else if peer, err := t.inputPeerForPortalID(ctx, msg.Portal.ID); err != nil {
|
|
return err
|
|
} else {
|
|
_, err := message.NewSender(t.client.API()).
|
|
To(peer).
|
|
Revoke().
|
|
Messages(ctx, messageID)
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
|
var resp bridgev2.MatrixReactionPreResponse
|
|
|
|
var maxReactions int
|
|
maxReactions, err := t.getReactionLimit(ctx, t.userID)
|
|
if err != nil {
|
|
return resp, err
|
|
}
|
|
|
|
var emojiID networkid.EmojiID
|
|
if strings.HasPrefix(msg.Content.RelatesTo.Key, "mxc://") {
|
|
if file, err := t.main.Store.TelegramFile.GetByMXC(ctx, msg.Content.RelatesTo.Key); err != nil {
|
|
return resp, err
|
|
} else if file == nil {
|
|
return resp, fmt.Errorf("reaction MXC URI %s does not correspond with any known Telegram files", msg.Content.RelatesTo.Key)
|
|
} else if documentID, err := strconv.ParseInt(string(file.LocationID), 10, 64); err != nil {
|
|
return resp, err
|
|
} else {
|
|
emojiID = ids.MakeEmojiIDFromDocumentID(documentID)
|
|
}
|
|
} else if documentID, ok := emojis.GetEmojiDocumentID(msg.Content.RelatesTo.Key); ok {
|
|
emojiID = ids.MakeEmojiIDFromDocumentID(documentID)
|
|
} else {
|
|
emojiID = ids.MakeEmojiIDFromEmoticon(msg.Content.RelatesTo.Key)
|
|
}
|
|
|
|
return bridgev2.MatrixReactionPreResponse{
|
|
SenderID: t.userID,
|
|
EmojiID: emojiID,
|
|
Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key),
|
|
MaxReactions: maxReactions,
|
|
}, nil
|
|
}
|
|
|
|
func (t *TelegramClient) appendEmojiID(reactionList []tg.ReactionClass, emojiID networkid.EmojiID) ([]tg.ReactionClass, error) {
|
|
if documentID, emoticon, err := ids.ParseEmojiID(emojiID); err != nil {
|
|
return nil, err
|
|
} else if documentID > 0 {
|
|
return append(reactionList, &tg.ReactionCustomEmoji{DocumentID: documentID}), nil
|
|
} else {
|
|
return append(reactionList, &tg.ReactionEmoji{Emoticon: emoticon}), nil
|
|
}
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) {
|
|
peer, err := t.inputPeerForPortalID(ctx, msg.Portal.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
targetMessageID, err := ids.ParseMessageID(msg.TargetMessage.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var newReactions []tg.ReactionClass
|
|
for _, existing := range msg.ExistingReactionsToKeep {
|
|
newReactions, err = t.appendEmojiID(newReactions, existing.EmojiID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
newReactions, err = t.appendEmojiID(newReactions, msg.PreHandleResp.EmojiID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = t.client.API().MessagesSendReaction(ctx, &tg.MessagesSendReactionRequest{
|
|
Peer: peer,
|
|
AddToRecent: true,
|
|
MsgID: targetMessageID,
|
|
Reaction: newReactions,
|
|
})
|
|
return &database.Reaction{}, err
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
|
|
peer, err := t.inputPeerForPortalID(ctx, msg.Portal.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var newReactions []tg.ReactionClass
|
|
|
|
if maxReactions, err := t.getReactionLimit(ctx, t.userID); err != nil {
|
|
return err
|
|
} else if maxReactions > 1 {
|
|
existing, err := t.main.Bridge.DB.Reaction.GetAllToMessageBySender(ctx, msg.TargetReaction.MessageID, msg.TargetReaction.SenderID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, existing := range existing {
|
|
if msg.TargetReaction.EmojiID != existing.EmojiID {
|
|
newReactions, err = t.appendEmojiID(newReactions, existing.EmojiID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
messageID, err := ids.ParseMessageID(msg.TargetReaction.MessageID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = t.client.API().MessagesSendReaction(ctx, &tg.MessagesSendReactionRequest{
|
|
Peer: peer,
|
|
AddToRecent: true,
|
|
MsgID: messageID,
|
|
Reaction: newReactions,
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixReadReceipt(ctx context.Context, msg *bridgev2.MatrixReadReceipt) error {
|
|
// TODO
|
|
return nil
|
|
}
|
|
|
|
func (t *TelegramClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
|
|
// TODO
|
|
return nil
|
|
}
|