diff --git a/go.mod b/go.mod index 9f87f43..f9cb70d 100644 --- a/go.mod +++ b/go.mod @@ -20,6 +20,7 @@ require ( require ( github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a github.com/gorilla/mux v1.8.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/lib/pq v1.10.9 // indirect diff --git a/go.sum b/go.sum index bd7942b..321fd99 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20O github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a h1:etIrTD8BQqzColk9nKRusM9um5+1q0iOEJLqfBMIK64= +github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a/go.mod h1:emQhSYTXqB0xxjLITTw4EaWZ+8IIQYw+kx9GqNUKdLg= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= @@ -51,13 +53,19 @@ golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= +golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.18.0 h1:mIYleuAkSbHh0tCv7RvjL3F6ZVbLjq4+R7zbOn3Kokg= golang.org/x/net v0.18.0/go.mod h1:/czyP5RqHAH4odGYxBJ1qz0+CE5WZ+2j1YgoEo8F2jQ= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= diff --git a/imessage/bluebubbles/api.go b/imessage/bluebubbles/api.go index 3d3d86f..45ab6ff 100644 --- a/imessage/bluebubbles/api.go +++ b/imessage/bluebubbles/api.go @@ -10,6 +10,7 @@ import ( "math" "math/rand" "mime/multipart" + "net" "net/http" "net/url" "os" @@ -19,6 +20,7 @@ import ( "time" "unicode" + "github.com/dyatlov/go-opengraph/opengraph" "github.com/gorilla/websocket" "github.com/rs/zerolog" "github.com/sahilm/fuzzy" @@ -1107,6 +1109,7 @@ func (bb *blueBubbles) SendMessage(chatID, text string, replyTo string, replyToP TempGUID: fmt.Sprintf("temp-%s", RandString(8)), SelectedMessageGUID: replyTo, PartIndex: replyToPart, + DDScan: bb.usingPrivateAPI, } var res SendTextResponse @@ -1705,14 +1708,118 @@ func (bb *blueBubbles) convertBBMessageToiMessage(bbMessage Message) (*imessage. message.GroupActionType = imessage.GroupActionType(bbMessage.GroupActionType) message.NewGroupName = bbMessage.GroupTitle - // TODO Richlinks - // message.RichLink = - message.ThreadID = bbMessage.ThreadOriginatorGUID + // Was the text a URL, does BB have a ddScan for it, did it come with any attachements because of the scan + if IsUrl(bbMessage.Text) && bbMessage.HasDDResults && len(message.Attachments) > 0 { + var reader io.Reader + resp, err := http.Get(bbMessage.Text) + if err != nil { + bb.bridge.GetLog().Errorfln("Error while fetching url: %v", err) + return &message, nil + } + reader = resp.Body + + // Fetch the data ourselves because the payloadData object is raw from the iMessage DB, from BB. + // That would require a ton of parsing that the BB app does client side to become something useable but it's written in Dart + // See https://github.com/BlueBubblesApp/bluebubbles-app/blob/d0257c9080e82140602340b48f85fa148721553c/lib/models/global/payload_data.dart + og := opengraph.NewOpenGraph() + if err := og.ProcessHTML(reader); err != nil { + bb.bridge.GetLog().Errorfln("Error processing html: %v", err) + return &message, nil + } + + // Was there any OpenGraph tags? Sometimes ddScan is able to produce data without them + // But this is the compromise for not wanting to parse payloadData + if og != nil && og.SiteName != "" { + message.RichLink = &imessage.RichLink{} + message.RichLink.OriginalURL = og.URL + message.RichLink.URL = og.URL + message.RichLink.SiteName = og.SiteName + message.RichLink.Title = og.Title + if og.Description != "" { + message.RichLink.Summary = og.Description + } else { + message.RichLink.Summary = og.URL + } + if og.Profile != nil { + message.RichLink.Creator = og.Profile.Username + } + + // It's always assumed that the first attachment is the icon, the second is the banenr image + var icon []byte + var err error + if len(message.Attachments) > 0 && message.Attachments[0] != nil { + icon, err = message.Attachments[0].Read() + } + if err == nil { + message.RichLink.Icon = &imessage.RichLinkAsset{} + // Don't add URLs, if the icon is empty then it wil attempt to fetch the icon with the URL + // The library used also doesn't provide anything for an icon + //message.RichLink.Icon.OriginalURL = og.URL + "/favicon.ico" + message.RichLink.Icon.Source = &imessage.RichLinkAssetSource{} + message.RichLink.Icon.Source.Data = icon + //message.RichLink.Icon.Source.URL = og.URL + "/favicon.ico" + } + + if len(og.Images) > 0 { + message.RichLink.Image = &imessage.RichLinkAsset{} + message.RichLink.Image.OriginalURL = og.Images[0].URL + var image []byte + var err error + if len(message.Attachments) > 1 && message.Attachments[1] != nil { + image, err = message.Attachments[1].Read() + } + if err == nil && image != nil { + message.RichLink.Image.Source = &imessage.RichLinkAssetSource{} + message.RichLink.Image.Source.URL = og.Images[0].URL + message.RichLink.Image.Source.Data = image + } + message.RichLink.Image.MimeType = og.Images[0].Type + message.RichLink.Image.Size = &imessage.RichLinkAssetSize{} + message.RichLink.Image.Size.Height = float64(og.Images[0].Height) + message.RichLink.Image.Size.Width = float64(og.Images[0].Width) + } + + if len(og.Videos) > 0 { + message.RichLink.Video = &imessage.RichLinkVideoAsset{} + message.RichLink.Video.StreamingURL = og.Videos[0].URL + message.RichLink.Video.YouTubeURL = og.Videos[0].URL + message.RichLink.Video.Asset.OriginalURL = og.Videos[0].URL + message.RichLink.Video.Asset.Source = &imessage.RichLinkAssetSource{} + message.RichLink.Video.Asset.Source.URL = og.Videos[0].URL + message.RichLink.Video.Asset.MimeType = og.Videos[0].Type + message.RichLink.Video.Asset.Size = &imessage.RichLinkAssetSize{} + message.RichLink.Video.Asset.Size.Height = float64(og.Videos[0].Height) + message.RichLink.Video.Asset.Size.Width = float64(og.Videos[0].Width) + } + } + resp.Body.Close() + + // Remove the attachments iMessage sent to display the link + // We want to remove them even if we don't have OpenGraph data because ddScan probably sent attachments + message.Attachments = make([]*imessage.Attachment, 0) + } + return &message, nil } + +func IsUrl(str string) bool { + url, err := url.ParseRequestURI(str) + if err != nil { + return false + } + + address := net.ParseIP(url.Host) + + if address == nil { + return strings.Contains(url.Host, ".") + } + + return true +} + func (bb *blueBubbles) convertBBTapbackToImessageTapback(associatedMessageType string) (tbType imessage.TapbackType) { if strings.Contains(associatedMessageType, "love") { tbType = imessage.TapbackLove @@ -1901,7 +2008,7 @@ func (bb *blueBubbles) Capabilities() imessage.ConnectorCapabilities { MessageStatusCheckpoints: false, DeliveredStatus: bb.usingPrivateAPI, ContactChatMerging: false, - RichLinks: false, + RichLinks: bb.usingPrivateAPI, ChatBridgeResult: false, } } diff --git a/imessage/bluebubbles/interface.go b/imessage/bluebubbles/interface.go index f9c12bd..08a3789 100644 --- a/imessage/bluebubbles/interface.go +++ b/imessage/bluebubbles/interface.go @@ -242,6 +242,7 @@ type SendTextRequest struct { Subject string `json:"subject,omitempty"` SelectedMessageGUID string `json:"selectedMessageGuid,omitempty"` PartIndex int `json:"partIndex,omitempty"` + DDScan bool `json:"ddScan,omitempty"` } type UnsendMessage struct {