diff --git a/go.mod b/go.mod index dcac32da..3c39f65c 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,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/kylelemons/godebug v1.1.0 // indirect github.com/lib/pq v1.10.9 // indirect diff --git a/go.sum b/go.sum index c18339cf..a725be73 100644 --- a/go.sum +++ b/go.sum @@ -8,6 +8,8 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA= github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU= +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/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= diff --git a/imessage/bluebubbles/api.go b/imessage/bluebubbles/api.go index 3d3d86fa..45ab6ff8 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 f9c12bd4..08a37896 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 {