Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ability to Edit and Unsend Messages for BlueBubbles Bridge #203

Merged
merged 15 commits into from
Mar 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
|----------------------------------|-----|-----------|-------------|
Expand Down
70 changes: 70 additions & 0 deletions imessage/bluebubbles/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions imessage/bluebubbles/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
5 changes: 5 additions & 0 deletions imessage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions imessage/struct.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,8 @@ type ConnectorCapabilities struct {
SendReadReceipts bool
SendTypingNotifications bool
SendCaptions bool
UnsendMessages bool
EditMessages bool
BridgeState bool
MessageStatusCheckpoints bool
DeliveredStatus bool
Expand Down
72 changes: 66 additions & 6 deletions portal.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand All @@ -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"))
}

Expand Down