diff --git a/messagix/socket/threads.go b/messagix/socket/threads.go
index abd444c..da986db 100644
--- a/messagix/socket/threads.go
+++ b/messagix/socket/threads.go
@@ -53,7 +53,7 @@ type MentionData struct {
}
func (md *MentionData) Parse() (Mentions, error) {
- if len(md.MentionIDs) == 0 {
+ if md == nil || len(md.MentionIDs) == 0 {
return nil, nil
}
mentionIDs := strings.Split(md.MentionIDs, ",")
diff --git a/pkg/connector/client.go b/pkg/connector/client.go
index a9c1173..d70a797 100644
--- a/pkg/connector/client.go
+++ b/pkg/connector/client.go
@@ -2,13 +2,14 @@ package connector
import (
"context"
- "errors"
"fmt"
"strconv"
"time"
"github.com/rs/zerolog"
+ "go.mau.fi/util/variationselector"
+
"go.mau.fi/mautrix-meta/config"
"go.mau.fi/mautrix-meta/messagix"
"go.mau.fi/mautrix-meta/messagix/cookies"
@@ -26,8 +27,6 @@ import (
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/simplevent"
"maunium.net/go/mautrix/event"
-
- metaTypes "go.mau.fi/mautrix-meta/messagix/types"
)
type metaEvent struct {
@@ -364,9 +363,8 @@ func (m *MetaClient) handleTable(ctx context.Context, tbl *table.LSTable) {
Sender: m.senderFromID(reaction.ActorId),
PortalKey: networkid.PortalKey{ID: networkid.PortalID(strconv.Itoa(int(reaction.ThreadKey)))},
TargetMessage: networkid.MessageID(reaction.MessageId),
- // only 1 reaction can be used per message, so just use a hardcoded ID
- EmojiID: networkid.EmojiID("reaction"),
- Emoji: reaction.Reaction,
+ EmojiID: networkid.EmojiID(""),
+ Emoji: reaction.Reaction,
}
m.Main.Bridge.QueueRemoteEvent(m.login, evt)
}
@@ -383,10 +381,48 @@ func (m *MetaClient) handleTable(ctx context.Context, tbl *table.LSTable) {
Sender: m.senderFromID(reaction.ActorId),
PortalKey: networkid.PortalKey{ID: networkid.PortalID(strconv.Itoa(int(reaction.ThreadKey)))},
TargetMessage: networkid.MessageID(reaction.MessageId),
- EmojiID: networkid.EmojiID("reaction"),
+ EmojiID: networkid.EmojiID(""),
}
m.Main.Bridge.QueueRemoteEvent(m.login, evt)
}
+
+ for _, edit := range tbl.LSEditMessage {
+ // Get the existing message by ID
+ editId := networkid.MessageID(edit.MessageID)
+ originalMsg, err := m.Main.Bridge.DB.Message.GetFirstPartByID(ctx, m.login.ID, editId)
+ if err != nil {
+ log.Err(err).Str("message_id", string(editId)).Msg("Failed to get original message")
+ continue
+ }
+
+ m.Main.Bridge.QueueRemoteEvent(m.login, &simplevent.Message[*table.LSEditMessage]{
+ EventMeta: simplevent.EventMeta{
+ Type: bridgev2.RemoteEventEdit,
+ LogContext: func(c zerolog.Context) zerolog.Context {
+ return c.
+ Str("message_id", edit.MessageID)
+ },
+ PortalKey: originalMsg.Room,
+ },
+ Data: edit,
+ ID: editId,
+ TargetMessage: editId,
+ ConvertEditFunc: func(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message, data *table.LSEditMessage) (*bridgev2.ConvertedEdit, error) {
+ textPart := existing[0] // TODO: Figure out a better way to get the text part, esp. if there are attachments etc.
+
+ return &bridgev2.ConvertedEdit{
+ ModifiedParts: []*bridgev2.ConvertedEditPart{
+ {
+ Part: textPart,
+ Type: event.EventMessage,
+ Content: m.messageConverter.MetaToMatrixText(ctx, data.Text, nil, portal),
+ },
+ },
+ }, nil
+ },
+ },
+ )
+ }
}
func (m *MetaClient) insertMessage(ctx context.Context, msg *table.WrappedMessage) {
@@ -455,9 +491,20 @@ func (m *MetaClient) Disconnect() {
m.client = nil
}
+var metaCaps = &bridgev2.NetworkRoomCapabilities{
+ FormattedText: true,
+ UserMentions: true,
+ Replies: true,
+ Edits: true,
+ EditMaxCount: 10,
+ EditMaxAge: 24 * time.Hour,
+ Reactions: true,
+ ReactionCount: 1,
+}
+
// GetCapabilities implements bridgev2.NetworkAPI.
func (m *MetaClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *bridgev2.NetworkRoomCapabilities {
- return &bridgev2.NetworkRoomCapabilities{}
+ return metaCaps
}
func (m *MetaClient) GetChatInfo(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
@@ -469,58 +516,20 @@ func (m *MetaClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*b
panic("GetUserInfo should never be called")
}
-type msgconvContextKey int
-
-const (
- msgconvContextKeyIntent msgconvContextKey = iota
- msgconvContextKeyClient
- msgconvContextKeyE2EEClient
- msgconvContextKeyBackfill
-)
-
// HandleMatrixMessage implements bridgev2.NetworkAPI.
func (m *MetaClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
log := zerolog.Ctx(ctx)
- content, ok := msg.Event.Content.Parsed.(*event.MessageEventContent)
- if !ok {
- log.Error().Type("content_type", content).Msg("Unexpected parsed content type")
- return nil, fmt.Errorf("unexpected parsed content type: %T", content)
- }
- if content.MsgType == event.MsgNotice /*&& !portal.bridge.Config.Bridge.BridgeNotices*/ {
+ if msg.Content.MsgType == event.MsgNotice /*&& !portal.bridge.Config.Bridge.BridgeNotices*/ {
log.Warn().Msg("Ignoring notice message")
return nil, nil
}
- ctx = context.WithValue(ctx, msgconvContextKeyClient, m.client)
-
- thread, err := strconv.Atoi(string(msg.Portal.ID))
- if err != nil {
- log.Err(err).Str("thread_id", string(msg.Portal.ID)).Msg("Failed to parse thread ID")
- return nil, fmt.Errorf("failed to parse thread ID: %w", err)
- }
-
log.Trace().Any("event", msg.Event).Msg("Handling Matrix message")
- tasks, otid, err := m.messageConverter.ToMeta(ctx, msg.Event, content, false, int64(thread), msg.Portal)
- if errors.Is(err, metaTypes.ErrPleaseReloadPage) {
- log.Err(err).Msg("Got please reload page error while converting message, reloading page in background")
- // go m.client.Disconnect()
- // err = errReloading
- panic("unimplemented")
- } else if errors.Is(err, messagix.ErrTokenInvalidated) {
- panic("unimplemented")
- // go sender.DisconnectFromError(status.BridgeState{
- // StateEvent: status.StateBadCredentials,
- // Error: MetaCookieRemoved,
- // })
- // err = errLoggedOut
- }
-
+ tasks, otid, err := m.messageConverter.ToMeta(ctx, msg.Event, msg.Content, false, ids.ParsePortalID(msg.Portal.ID), msg.Portal)
if err != nil {
- log.Err(err).Msg("Failed to convert message")
- //go ms.sendMessageMetrics(evt, err, "Error converting", true)
- return nil, err
+ return nil, fmt.Errorf("failed to convert message: %w", err)
}
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
@@ -529,8 +538,7 @@ func (m *MetaClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Matr
log.Debug().Msg("Sending Matrix message to Meta")
otidStr := strconv.FormatInt(otid, 10)
- //portal.pendingMessages[otid] = evt.ID
- //messageTS := time.Now()
+
var resp *table.LSTable
retries := 0
@@ -573,20 +581,8 @@ func (m *MetaClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Matr
log.Warn().Msg("Message send response didn't include message ID")
}
}
- // if msgID != "" {
- // portal.pendingMessagesLock.Lock()
- // _, ok = portal.pendingMessages[otid]
- // if ok {
- // portal.storeMessageInDB(ctx, evt.ID, msgID, otid, sender.MetaID, messageTS, 0)
- // delete(portal.pendingMessages, otid)
- // } else {
- // log.Debug().Msg("Not storing message send response: pending message was already removed from map")
- // }
- // portal.pendingMessagesLock.Unlock()
- // }
if m.login.User.MXID != msg.Event.Sender {
- log.Warn().Any("sender", msg.Event.Sender).Msg("Sender mismatch with user login")
return nil, fmt.Errorf("sender mismatch with user login: %s", msg.Event.Sender)
}
@@ -599,9 +595,105 @@ func (m *MetaClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.Matr
Timestamp: time.Time{},
},
}, nil
+}
+
+func (m *MetaClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
+ log := zerolog.Ctx(ctx)
+
+ log.Debug().Any("reaction", msg).Msg("Handling Matrix reaction")
+
+ resp, err := m.client.ExecuteTasks(&socket.SendReactionTask{
+ ThreadKey: ids.ParsePortalID(msg.Portal.ID),
+ TimestampMs: msg.Event.Timestamp,
+ MessageID: string(msg.TargetMessage.ID),
+ Reaction: msg.PreHandleResp.Emoji,
+ ActorID: ids.ParseUserID(msg.PreHandleResp.SenderID),
+ SyncGroup: 1,
+ SendAttribution: table.MESSENGER_INBOX_IN_THREAD,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to send reaction to Meta: %w", err)
+ }
+
+ log.Trace().Any("response", resp).Msg("Meta reaction response")
+
+ return &database.Reaction{}, nil
+}
+
+func (m *MetaClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
+ log := zerolog.Ctx(ctx)
+
+ log.Debug().Any("reaction", msg).Msg("Removing Matrix reaction")
+
+ resp, err := m.client.ExecuteTasks(&socket.SendReactionTask{
+ ThreadKey: ids.ParsePortalID(msg.Portal.ID),
+ TimestampMs: msg.Event.Timestamp,
+ MessageID: string(msg.TargetReaction.MessageID),
+ Reaction: "",
+ ActorID: ids.ParseUserID(msg.TargetReaction.SenderID),
+ SyncGroup: 1,
+ SendAttribution: table.MESSENGER_INBOX_IN_THREAD,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to send reaction to Meta: %w", err)
+ }
+
+ log.Trace().Any("response", resp).Msg("Meta reaction remove response")
+
+ return nil
+}
+
+func (m *MetaClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
+ return bridgev2.MatrixReactionPreResponse{
+ SenderID: networkid.UserID(m.login.ID),
+ EmojiID: networkid.EmojiID(""),
+ Emoji: variationselector.Remove(msg.Content.RelatesTo.Key),
+ MaxReactions: 1,
+ }, nil
+}
+
+func (m *MetaClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
+ log := zerolog.Ctx(ctx)
+
+ log.Debug().Any("edit", edit).Msg("Handling Matrix edit")
+
+ // TODO: The conversion stuff wants a SendMessageTask, and I don't feel like rewriting it yet
+ fakeSendTasks, _, err := m.messageConverter.ToMeta(ctx, edit.Event, edit.Content, false, ids.ParsePortalID(edit.Portal.ID), edit.Portal)
+ if err != nil {
+ return fmt.Errorf("failed to convert message: %w", err)
+ }
+
+ fakeTask := fakeSendTasks[0].(*socket.SendMessageTask)
+
+ editTask := &socket.EditMessageTask{
+ MessageID: string(edit.EditTarget.ID),
+ Text: fakeTask.Text,
+ }
+
+ newEditCount := int64(edit.EditTarget.EditCount) + 1
- // timings.totalSend = time.Since(start)
- // go ms.sendMessageMetrics(evt, err, "Error sending", true)
+ var resp *table.LSTable
+ resp, err = m.client.ExecuteTasks(editTask)
+ log.Trace().Any("response", resp).Msg("Meta edit response")
+ if err != nil {
+ return fmt.Errorf("failed to send edit to Meta: %w", err)
+ }
+
+ if len(resp.LSEditMessage) == 0 {
+ log.Debug().Msg("Edit response didn't contain new edit?")
+ } else if resp.LSEditMessage[0].MessageID != editTask.MessageID {
+ log.Debug().Msg("Edit response contained different message ID")
+ } else if resp.LSEditMessage[0].Text != editTask.Text {
+ log.Warn().Msg("Server returned edit with different text")
+ return fmt.Errorf("edit reverted")
+ } else if resp.LSEditMessage[0].EditCount != newEditCount {
+ log.Warn().
+ Int64("expected_edit_count", newEditCount).
+ Int64("actual_edit_count", resp.LSEditMessage[0].EditCount).
+ Msg("Edit count mismatch")
+ }
+
+ return nil
}
// IsLoggedIn implements bridgev2.NetworkAPI.
@@ -732,10 +824,10 @@ func (m *MetaClient) SearchUsers(ctx context.Context, search string) ([]*bridgev
}
var (
- _ bridgev2.NetworkAPI = (*MetaClient)(nil)
- _ bridgev2.UserSearchingNetworkAPI = (*MetaClient)(nil)
- // _ bridgev2.EditHandlingNetworkAPI = (*MetaClient)(nil)
- // _ bridgev2.ReactionHandlingNetworkAPI = (*MetaClient)(nil)
+ _ bridgev2.NetworkAPI = (*MetaClient)(nil)
+ _ bridgev2.UserSearchingNetworkAPI = (*MetaClient)(nil)
+ _ bridgev2.EditHandlingNetworkAPI = (*MetaClient)(nil)
+ _ bridgev2.ReactionHandlingNetworkAPI = (*MetaClient)(nil)
// _ bridgev2.RedactionHandlingNetworkAPI = (*MetaClient)(nil)
// _ bridgev2.ReadReceiptHandlingNetworkAPI = (*MetaClient)(nil)
// _ bridgev2.ReadReceiptHandlingNetworkAPI = (*MetaClient)(nil)
diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go
index 6d007be..3107f5f 100644
--- a/pkg/connector/connector.go
+++ b/pkg/connector/connector.go
@@ -57,11 +57,12 @@ func (m *MetaConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
func (s *MetaConnector) GetName() bridgev2.BridgeName {
if s.Config == nil || s.Config.Mode == "" {
return bridgev2.BridgeName{
- DisplayName: "Meta",
- NetworkURL: "https://meta.com",
- NetworkIcon: "mxc://maunium.net/DxpVrwwzPUwaUSazpsjXgcKB",
- NetworkID: "meta",
- BeeperBridgeType: "meta",
+ DisplayName: "Meta",
+ NetworkURL: "https://meta.com",
+ NetworkIcon: "mxc://maunium.net/DxpVrwwzPUwaUSazpsjXgcKB",
+ NetworkID: "meta",
+ // this should be changed to "meta", this is just for compatibility with existing clients during development
+ BeeperBridgeType: "facebookgo",
DefaultPort: 29319,
}
} else {
@@ -71,7 +72,7 @@ func (s *MetaConnector) GetName() bridgev2.BridgeName {
NetworkURL: "https://instagram.com",
NetworkIcon: "mxc://maunium.net/JxjlbZUlCPULEeHZSwleUXQv",
NetworkID: "instagram",
- BeeperBridgeType: "meta",
+ BeeperBridgeType: "instagramgo",
DefaultPort: 29319,
}
} else if s.Config.Mode == "facebook" {
@@ -80,7 +81,7 @@ func (s *MetaConnector) GetName() bridgev2.BridgeName {
NetworkURL: "https://www.facebook.com/messenger",
NetworkIcon: "mxc://maunium.net/ygtkteZsXnGJLJHRchUwYWak",
NetworkID: "facebook",
- BeeperBridgeType: "meta",
+ BeeperBridgeType: "facebookgo",
DefaultPort: 29319,
}
} else {
diff --git a/pkg/connector/ids/ids.go b/pkg/connector/ids/ids.go
index 18124f2..5e9ff55 100644
--- a/pkg/connector/ids/ids.go
+++ b/pkg/connector/ids/ids.go
@@ -30,3 +30,13 @@ func ParseUserID(user networkid.UserID) int64 {
i, _ := strconv.Atoi(string(user))
return int64(i)
}
+
+func ParseMessageID(message networkid.MessageID) int64 {
+ i, _ := strconv.Atoi(string(message))
+ return int64(i)
+}
+
+func ParsePortalID(portal networkid.PortalID) int64 {
+ i, _ := strconv.Atoi(string(portal))
+ return int64(i)
+}
diff --git a/pkg/connector/msgconv/from-meta.go b/pkg/connector/msgconv/from-meta.go
index adf32f2..039fe8e 100644
--- a/pkg/connector/msgconv/from-meta.go
+++ b/pkg/connector/msgconv/from-meta.go
@@ -179,7 +179,7 @@ func (mc *MessageConverter) ToMatrix(ctx context.Context, msg *table.WrappedMess
MentionLengths: msg.MentionLengths,
MentionTypes: msg.MentionTypes,
}
- content := mc.metaToMatrixText(ctx, msg.Text, mentions, portal)
+ content := mc.MetaToMatrixText(ctx, msg.Text, mentions, portal)
if msg.IsAdminMessage {
content.MsgType = event.MsgNotice
}
diff --git a/pkg/connector/msgconv/mentions.go b/pkg/connector/msgconv/mentions.go
index 0f14d29..5d74c66 100644
--- a/pkg/connector/msgconv/mentions.go
+++ b/pkg/connector/msgconv/mentions.go
@@ -18,9 +18,6 @@ package msgconv
import (
"context"
- "regexp"
- "slices"
-
//"log"
"strings"
"unicode/utf16"
@@ -43,15 +40,7 @@ func (u UTF16String) String() string {
return string(utf16.Decode(u))
}
-var (
- META_BOLD_REGEX = regexp.MustCompile(`\*([^*]+)\*`)
- META_ITALIC_REGEX = regexp.MustCompile(`_([^_]+)_`)
- META_STRIKE_REGEX = regexp.MustCompile(`~([^~]+)~`)
- META_MONOSPACE_REGEX = regexp.MustCompile("`([^`]+)`")
- META_MONOSPACE_BLOCK_REGEX = regexp.MustCompile("```([^`]+)```")
-)
-
-func (mc *MessageConverter) metaToMatrixText(ctx context.Context, text string, rawMentions *socket.MentionData, portal *bridgev2.Portal) (content *event.MessageEventContent) {
+func (mc *MessageConverter) MetaToMatrixText(ctx context.Context, text string, rawMentions *socket.MentionData, portal *bridgev2.Portal) (content *event.MessageEventContent) {
content = &event.MessageEventContent{
MsgType: event.MsgText,
Body: text,
@@ -61,64 +50,50 @@ func (mc *MessageConverter) metaToMatrixText(ctx context.Context, text string, r
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to parse mentions")
}
-
- outputString := text
-
- if mentions != nil {
- utf16Text := NewUTF16String(text)
- prevEnd := 0
- var output strings.Builder
- for _, mention := range mentions {
- if mention.Offset < prevEnd {
- zerolog.Ctx(ctx).Warn().Msg("Ignoring overlapping mentions in message")
- continue
- } else if mention.Offset >= len(utf16Text) {
- zerolog.Ctx(ctx).Warn().Msg("Ignoring mention outside of message")
- continue
- }
- end := mention.Offset + mention.Length
- if end > len(utf16Text) {
- end = len(utf16Text)
- }
- var mentionLink string
- switch mention.Type {
- case socket.MentionTypePerson:
- info, err := mc.getBasicUserInfo(ctx, portal, ids.MakeUserID(mention.ID))
- if err != nil {
- zerolog.Ctx(ctx).Err(err).Msg("Failed to get user info for mention")
- continue
- }
- if !slices.Contains(content.Mentions.UserIDs, info.MXID) {
- content.Mentions.UserIDs = append(content.Mentions.UserIDs, info.MXID)
- }
- mentionLink = info.MXID.URI().MatrixToURL()
- case socket.MentionTypeThread:
- // TODO: how does one send thread mentions?
- }
- if mentionLink == "" {
+ if mentions == nil {
+ return
+ }
+ utf16Text := NewUTF16String(text)
+ prevEnd := 0
+ var output strings.Builder
+ for _, mention := range mentions {
+ if mention.Offset < prevEnd {
+ zerolog.Ctx(ctx).Warn().Msg("Ignoring overlapping mentions in message")
+ continue
+ } else if mention.Offset >= len(utf16Text) {
+ zerolog.Ctx(ctx).Warn().Msg("Ignoring mention outside of message")
+ continue
+ }
+ end := mention.Offset + mention.Length
+ if end > len(utf16Text) {
+ end = len(utf16Text)
+ }
+ var mentionLink string
+ switch mention.Type {
+ case socket.MentionTypePerson:
+ info, err := mc.getBasicUserInfo(ctx, portal, ids.MakeUserID(mention.ID))
+ if err != nil {
+ zerolog.Ctx(ctx).Err(err).Msg("Failed to get user info for mention")
continue
}
-
- output.WriteString(utf16Text[prevEnd:mention.Offset].String() + `` + utf16Text[mention.Offset:end].String() + ``)
- prevEnd = end
+ content.Mentions.Add(info.MXID)
+ mentionLink = info.MXID.URI().MatrixToURL()
+ case socket.MentionTypeThread:
+ // TODO: how does one send thread mentions?
}
- output.WriteString(utf16Text[prevEnd:].String())
-
- outputString = output.String()
+ if mentionLink == "" {
+ continue
+ }
+ output.WriteString(utf16Text[prevEnd:mention.Offset].String())
+ output.WriteString(``)
+ output.WriteString(utf16Text[mention.Offset:end].String())
+ output.WriteString(``)
+ prevEnd = end
}
-
- // Second parsing pass, replacing other formatting:
- outputString = META_BOLD_REGEX.ReplaceAllString(outputString, "$1")
- outputString = META_ITALIC_REGEX.ReplaceAllString(outputString, "$1")
- outputString = META_STRIKE_REGEX.ReplaceAllString(outputString, "$1")
- outputString = META_MONOSPACE_REGEX.ReplaceAllString(outputString, "$1
")
- outputString = META_MONOSPACE_BLOCK_REGEX.ReplaceAllString(outputString, "
$1") - + output.WriteString(utf16Text[prevEnd:].String()) content.Format = event.FormatHTML - content.FormattedBody = outputString - - log := zerolog.Ctx(ctx) - log.Debug().Str("text", text).Str("formatted_body", content.FormattedBody).Msg("Converted message to Matrix text") - + content.FormattedBody = output.String() return content }