diff --git a/ROADMAP.md b/ROADMAP.md index 31c786a..5fdc415 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -12,14 +12,16 @@ Note that Barcelona, which the mac-nosip connector uses, is no longer maintained | Media/files | ✔️ | ✔️ | ✔️ | | Replies | 🛑 | ✔️ | ✔️† | | Reactions | 🛑 | ✔️ | ✔️ | -| Edits | 🛑 | ❌ | ❌ | -| Unsends | 🛑 | ❌ | ❌ | +| Edits | 🛑 | ❌ | ✔️* | +| Unsends | 🛑 | ❌ | ✔️* | | Redactions | 🛑 | ✔️ | ✔️ | | Read receipts | 🛑 | ✔️ | ✔️ | | Typing notifications | 🛑 | ✔️ | ✔️ | † BlueBubbles had bugs with replies until v1.9.5 +\* macOS Ventura or higher is required + ## iMessage → Matrix | Feature | mac | mac-nosip | bluebubbles | |----------------------------------|-----|-----------|-------------| diff --git a/imessage/bluebubbles/api.go b/imessage/bluebubbles/api.go index 721f94c..d4a3177 100644 --- a/imessage/bluebubbles/api.go +++ b/imessage/bluebubbles/api.go @@ -1093,6 +1093,74 @@ func (bb *blueBubbles) SendMessage(chatID, text string, replyTo string, replyToP }, nil } +func (bb *blueBubbles) UnsendMessage(chatID, targetGUID string, targetPart int) (*imessage.SendResponse, error) { + bb.log.Trace().Str("chatID", chatID).Str("targetGUID", targetGUID).Int("targetPart", targetPart).Msg("UnsendMessage") + + if !bb.usingPrivateAPI { + bb.log.Warn().Str("chatID", chatID).Str("targetGUID", targetGUID).Int("targetPart", targetPart).Msg("The private-api isn't enabled in BlueBubbles, can't unsend message") + return nil, errors.ErrUnsupported + } + + request := UnsendMessage{ + PartIndex: targetPart, + } + + var res UnsendMessageResponse + + err := bb.apiPost("/api/v1/message/"+targetGUID+"/unsend", request, &res) + if err != nil { + bb.log.Error().Any("response", res).Msg("Failure when unsending message in BlueBubbles") + return nil, err + } + + if res.Status != 200 { + bb.log.Error().Int64("statusCode", res.Status).Any("response", res).Msg("Failure when unsending message in BlueBubbles") + + return nil, errors.New("could not unsend message") + } + + return &imessage.SendResponse{ + GUID: res.Data.GUID, + Service: res.Data.Handle.Service, + Time: time.UnixMilli(res.Data.DateCreated), + }, nil +} + +func (bb *blueBubbles) EditMessage(chatID string, targetGUID string, newText string, targetPart int) (*imessage.SendResponse, error) { + bb.log.Trace().Str("chatID", chatID).Str("targetGUID", targetGUID).Str("newText", newText).Int("targetPart", targetPart).Msg("EditMessage") + + if !bb.usingPrivateAPI { + bb.log.Warn().Str("chatID", chatID).Str("targetGUID", targetGUID).Str("newText", newText).Int("targetPart", targetPart).Msg("The private-api isn't enabled in BlueBubbles, can't edit message") + return nil, errors.ErrUnsupported + } + + request := EditMessage{ + EditedMessage: newText, + BackwwardsCompatibilityMessage: "Edited to \"" + newText + "\"", + PartIndex: targetPart, + } + + var res EditMessageResponse + + err := bb.apiPost("/api/v1/message/"+targetGUID+"/edit", request, &res) + if err != nil { + bb.log.Error().Any("response", res).Msg("Failure when editing message in BlueBubbles") + return nil, err + } + + if res.Status != 200 { + bb.log.Error().Int64("statusCode", res.Status).Any("response", res).Msg("Failure when editing message in BlueBubbles") + + return nil, errors.New("could not edit message") + } + + return &imessage.SendResponse{ + GUID: res.Data.GUID, + Service: res.Data.Handle.Service, + Time: time.UnixMilli(res.Data.DateCreated), + }, nil +} + func (bb *blueBubbles) isPrivateAPI() bool { var serverInfo ServerInfoResponse err := bb.apiGet("/api/v1/server/info", nil, &serverInfo) @@ -1766,6 +1834,8 @@ func (bb *blueBubbles) Capabilities() imessage.ConnectorCapabilities { return imessage.ConnectorCapabilities{ MessageSendResponses: true, SendTapbacks: bb.usingPrivateAPI, + UnsendMessages: bb.usingPrivateAPI, + EditMessages: bb.usingPrivateAPI, SendReadReceipts: bb.usingPrivateAPI, SendTypingNotifications: bb.usingPrivateAPI, SendCaptions: true, diff --git a/imessage/bluebubbles/interface.go b/imessage/bluebubbles/interface.go index c1567d6..f9c12bd 100644 --- a/imessage/bluebubbles/interface.go +++ b/imessage/bluebubbles/interface.go @@ -244,6 +244,30 @@ type SendTextRequest struct { PartIndex int `json:"partIndex,omitempty"` } +type UnsendMessage struct { + PartIndex int `json:"partIndex"` +} + +type EditMessage struct { + EditedMessage string `json:"editedMessage"` + BackwwardsCompatibilityMessage string `json:"backwardsCompatibilityMessage"` + PartIndex int `json:"partIndex"` +} + +type UnsendMessageResponse struct { + Status int64 `json:"status"` + Message string `json:"message"` + Data Message `json:"data,omitempty"` + Error any `json:"error"` +} + +type EditMessageResponse struct { + Status int64 `json:"status"` + Message string `json:"message"` + Data Message `json:"data,omitempty"` + Error any `json:"error"` +} + type SendTextResponse struct { Status int64 `json:"status"` Message string `json:"message"` diff --git a/imessage/interface.go b/imessage/interface.go index 0915070..45e96eb 100644 --- a/imessage/interface.go +++ b/imessage/interface.go @@ -47,6 +47,11 @@ type ChatInfoAPI interface { GetGroupAvatar(chatID string) (*Attachment, error) } +type VenturaFeatures interface { + UnsendMessage(chatID, targetGUID string, targetPart int) (*SendResponse, error) + EditMessage(chatID, targetGUID string, newText string, targetPart int) (*SendResponse, error) +} + type API interface { Start(readyCallback func()) error Stop() diff --git a/imessage/struct.go b/imessage/struct.go index 1812a75..a26b0e4 100644 --- a/imessage/struct.go +++ b/imessage/struct.go @@ -269,6 +269,8 @@ type ConnectorCapabilities struct { SendReadReceipts bool SendTypingNotifications bool SendCaptions bool + UnsendMessages bool + EditMessages bool BridgeState bool MessageStatusCheckpoints bool DeliveredStatus bool diff --git a/portal.go b/portal.go index 7461e37..3c30f59 100644 --- a/portal.go +++ b/portal.go @@ -693,7 +693,7 @@ func (portal *Portal) getBridgeInfo() (string, CustomBridgeInfoContent) { } else if portal.bridge.Config.IMessage.Platform == "mac-nosip" { bridgeInfo.Protocol.ID = "imessage-nosip" } else if portal.bridge.Config.IMessage.Platform == "bluebubbles" { - bridgeInfo.Protocol.ID = "imessage-nosip" + bridgeInfo.Protocol.ID = "imessagego" } return portal.getBridgeInfoStateKey(), bridgeInfo } @@ -1239,15 +1239,43 @@ func (portal *Portal) HandleMatrixMessage(evt *event.Event) { return } + editEventID := msg.RelatesTo.GetReplaceID() + if editEventID != "" && msg.NewContent != nil { + msg = msg.NewContent + } + + var err error + var resp *imessage.SendResponse + var wasEdit bool + + if editEventID != "" { + wasEdit = true + if !portal.bridge.IM.Capabilities().EditMessages { + portal.zlog.Err(errors.ErrUnsupported).Msg("Bridge doesn't support editing messages!") + return + } + + editedMessage := portal.bridge.DB.Message.GetByMXID(editEventID) + if editedMessage == nil { + portal.zlog.Error().Msg("Failed to get message by MXID") + return + } + + if portal.bridge.IM.(imessage.VenturaFeatures) != nil { + resp, err = portal.bridge.IM.(imessage.VenturaFeatures).EditMessage(portal.getTargetGUID("message edit", evt.ID, editedMessage.HandleGUID), editedMessage.GUID, msg.Body, editedMessage.Part) + } else { + portal.zlog.Err(errors.ErrUnsupported).Msg("Bridge didn't implment EditMessage!") + return + } + } + var imessageRichLink *imessage.RichLink if portal.bridge.IM.Capabilities().RichLinks { imessageRichLink = portal.convertURLPreviewToIMessage(evt) } metadata, _ := evt.Content.Raw["com.beeper.message_metadata"].(imessage.MessageMetadata) - var err error - var resp *imessage.SendResponse - if msg.MsgType == event.MsgText || msg.MsgType == event.MsgNotice || msg.MsgType == event.MsgEmote { + if (msg.MsgType == event.MsgText || msg.MsgType == event.MsgNotice || msg.MsgType == event.MsgEmote) && !wasEdit { if evt.Sender != portal.bridge.user.MXID { portal.addRelaybotFormat(evt.Sender, msg) if len(msg.Body) == 0 { @@ -1261,6 +1289,7 @@ func (portal *Portal) HandleMatrixMessage(evt *event.Event) { } else if len(msg.URL) > 0 || msg.File != nil { resp, err = portal.handleMatrixMedia(msg, evt, messageReplyID, messageReplyPart, metadata) } + if err != nil { portal.log.Errorln("Error sending to iMessage:", err) statusCode := status.MsgStatusPermFailure @@ -1552,7 +1581,7 @@ func (portal *Portal) HandleMatrixReaction(evt *event.Event) { func (portal *Portal) HandleMatrixRedaction(evt *event.Event) { if !portal.bridge.IM.Capabilities().SendTapbacks { - portal.sendUnsupportedCheckpoint(evt, status.MsgStepRemote, errors.New("redactions are not supported")) + portal.sendUnsupportedCheckpoint(evt, status.MsgStepRemote, errors.New("reactions are not supported")) return } @@ -1564,7 +1593,7 @@ func (portal *Portal) HandleMatrixRedaction(evt *event.Event) { redactedTapback := portal.bridge.DB.Tapback.GetByMXID(evt.Redacts) if redactedTapback != nil { - portal.log.Debugln("Starting handling of Matrix redaction", evt.ID) + portal.log.Debugln("Starting handling of Matrix redaction of tapback", evt.ID) redactedTapback.Delete() _, err := portal.bridge.IM.SendTapback(portal.getTargetGUID("tapback redaction", evt.ID, redactedTapback.HandleGUID), redactedTapback.MessageGUID, redactedTapback.MessagePart, redactedTapback.Type, true) if err != nil { @@ -1578,6 +1607,37 @@ func (portal *Portal) HandleMatrixRedaction(evt *event.Event) { } return } + + if !portal.bridge.IM.Capabilities().UnsendMessages { + portal.sendUnsupportedCheckpoint(evt, status.MsgStepRemote, errors.New("redactions of messages are not supported")) + return + } + + redactedText := portal.bridge.DB.Message.GetByMXID(evt.Redacts) + if redactedText != nil { + portal.log.Debugln("Starting handling of Matrix redaction of text", evt.ID) + redactedText.Delete() + + var err error + if portal.bridge.IM.(imessage.VenturaFeatures) != nil { + _, err = portal.bridge.IM.(imessage.VenturaFeatures).UnsendMessage(portal.getTargetGUID("message redaction", evt.ID, redactedText.HandleGUID), redactedText.GUID, redactedText.Part) + } else { + portal.zlog.Err(errors.ErrUnsupported).Msg("Bridge didn't implment UnsendMessage!") + return + } + + //_, err := portal.bridge.IM.UnsendMessage(portal.getTargetGUID("message redaction", evt.ID, redactedText.HandleGUID), redactedText.GUID, redactedText.Part) + if err != nil { + portal.log.Errorfln("Failed to send unsend of message %s/%d: %v", redactedText.GUID, redactedText.Part, err) + portal.bridge.SendMessageErrorCheckpoint(evt, status.MsgStepRemote, err, true, 0) + } else { + portal.log.Debugfln("Handled Matrix redaction %s of iMessage message %s/%d", evt.ID, redactedText.GUID, redactedText.Part) + if !portal.bridge.IM.Capabilities().MessageStatusCheckpoints { + portal.bridge.SendMessageSuccessCheckpoint(evt, status.MsgStepRemote, 0) + } + } + return + } portal.sendUnsupportedCheckpoint(evt, status.MsgStepRemote, fmt.Errorf("can't redact non-reaction event")) }