Files
mautrix-telegram/pkg/connector/matrix.go
T
Sumner Evans a63f264804 reactions: support custom emojis
Signed-off-by: Sumner Evans <sumner.evans@automattic.com>
2024-07-09 14:04:25 -06:00

257 lines
7.7 KiB
Go

package connector
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/gotd/td/telegram/message"
"github.com/gotd/td/telegram/message/html"
"github.com/gotd/td/telegram/message/styling"
"github.com/gotd/td/telegram/uploader"
"github.com/gotd/td/tg"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"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/msgconv"
"go.mau.fi/mautrix-telegram/pkg/connector/waveform"
)
func getMediaFilenameAndCaption(content *event.MessageEventContent) (filename, caption string) {
if content.FileName != "" {
filename = content.FileName
caption = content.FormattedBody
if caption == "" {
caption = content.Body
}
} else {
filename = content.Body
}
return
}
func (t *TelegramClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (resp *bridgev2.MatrixMessageResponse, err error) {
sender := message.NewSender(t.client.API())
peer, err := ids.InputPeerForPortalID(msg.Portal.ID)
if err != nil {
return nil, err
}
builder := sender.To(peer)
// TODO handle sticker
var updates tg.UpdatesClass
switch msg.Content.MsgType {
case event.MsgText:
if msg.Content.BeeperLinkPreviews != nil && len(msg.Content.BeeperLinkPreviews) == 0 {
builder.NoWebpage()
}
updates, err = builder.Text(ctx, msg.Content.Body)
case event.MsgImage, event.MsgFile, event.MsgAudio, event.MsgVideo:
filename, caption := getMediaFilenameAndCaption(msg.Content)
var fileData []byte
fileData, err = t.main.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, msg.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)
}
var styling []styling.StyledTextOption
if caption != "" {
// TODO resolver?
// TODO HTML
styling = append(styling, html.String(nil, caption))
}
if msg.Content.MsgType == event.MsgImage {
updates, err = builder.Media(ctx, message.UploadedPhoto(upload, styling...))
break
} else {
document := message.UploadedDocument(upload, styling...).Filename(filename)
if msg.Content.Info != nil {
document.MIME(msg.Content.Info.MimeType)
}
var media message.MediaOption
switch msg.Content.MsgType {
case event.MsgAudio:
audioBuilder := document.Audio()
if msg.Content.MSC1767Audio != nil {
audioBuilder.Duration(time.Duration(msg.Content.MSC1767Audio.Duration) * time.Millisecond)
if len(msg.Content.MSC1767Audio.Waveform) > 0 {
audioBuilder.Waveform(waveform.Encode(msg.Content.MSC1767Audio.Waveform))
}
}
if msg.Content.MSC3245Voice != nil {
audioBuilder.Voice()
}
media = audioBuilder
default:
media = document
}
updates, err = builder.Media(ctx, media)
}
case event.MsgLocation:
var uri msgconv.GeoURI
uri, err = msgconv.ParseGeoURI(msg.Content.GeoURI)
if err != nil {
return nil, err
}
var styling []styling.StyledTextOption
if location, ok := msg.Event.Content.Raw["org.matrix.msc3488.location"].(map[string]any); ok {
if desc, ok := location["description"].(string); ok {
// TODO resolver?
// TODO HTML
styling = append(styling, html.String(nil, desc))
}
}
updates, err = builder.Media(ctx, message.Media(&tg.InputMediaGeoPoint{
GeoPoint: &tg.InputGeoPoint{
Lat: uri.Lat,
Long: uri.Long,
},
}, styling...))
default:
return nil, fmt.Errorf("unsupported message type %s", msg.Content.MsgType)
}
if err != nil {
return nil, err
}
var tgMessageID, tgDate int
switch sentMessage := updates.(type) {
case *tg.UpdateShortSentMessage:
tgMessageID = sentMessage.ID
tgDate = sentMessage.Date
case *tg.Updates:
tgDate = sentMessage.Date
for _, u := range sentMessage.Updates {
if update, ok := u.(*tg.UpdateMessageID); ok {
tgMessageID = update.ID
break
}
}
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: ids.MakeUserID(t.loginID),
Timestamp: time.Unix(int64(tgDate), 0),
},
}
return
}
func (t *TelegramClient) HandleMatrixEdit(ctx context.Context, msg *bridgev2.MatrixEdit) error {
panic("unimplemented edit")
}
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 {
_, err = message.NewSender(t.client.API()).Self().Revoke().Messages(ctx, messageID)
return err
}
}
func (t *TelegramClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
var resp bridgev2.MatrixReactionPreResponse
sender := ids.MakeUserID(t.loginID)
var maxReactions int
maxReactions, err := t.getReactionLimit(ctx, sender)
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: sender,
EmojiID: emojiID,
Emoji: variationselector.FullyQualify(msg.Content.RelatesTo.Key),
MaxReactions: maxReactions,
}, nil
}
func (t *TelegramClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (reaction *database.Reaction, err error) {
peer, err := ids.InputPeerForPortalID(msg.Portal.ID)
if err != nil {
return nil, err
}
targetMessageID, err := ids.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return nil, err
}
req := tg.MessagesSendReactionRequest{
Peer: peer,
AddToRecent: true,
MsgID: targetMessageID,
}
if documentID, emoticon, err := ids.ParseEmojiID(msg.PreHandleResp.EmojiID); err != nil {
return nil, err
} else if documentID > 0 {
req.SetReaction([]tg.ReactionClass{&tg.ReactionCustomEmoji{DocumentID: documentID}})
} else {
req.SetReaction([]tg.ReactionClass{&tg.ReactionEmoji{Emoticon: emoticon}})
}
_, err = t.client.API().MessagesSendReaction(ctx, &req)
return &database.Reaction{}, err
}
func (t *TelegramClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
panic("unimplemented reaction remove")
}
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
}