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

Support for Sending & Displaying Rich Links from BlueBubbles #210

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 14 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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
114 changes: 110 additions & 4 deletions imessage/bluebubbles/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"math"
"math/rand"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1705,14 +1708,117 @@ 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) convertBBChatToiMessageChat(bbChat Chat) (*imessage.ChatInfo, error) {
members := make([]string, len(bbChat.Participants))

Expand Down Expand Up @@ -1880,7 +1986,7 @@ func (bb *blueBubbles) Capabilities() imessage.ConnectorCapabilities {
MessageStatusCheckpoints: false,
DeliveredStatus: bb.usingPrivateAPI,
ContactChatMerging: false,
RichLinks: false,
RichLinks: bb.usingPrivateAPI,
ChatBridgeResult: false,
}
}
1 change: 1 addition & 0 deletions imessage/bluebubbles/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down