github.com/status-im/status-go@v1.1.0/server/handlers_linkpreview.go (about) 1 package server 2 3 import ( 4 "database/sql" 5 "encoding/json" 6 "fmt" 7 "net/http" 8 9 "github.com/golang/protobuf/proto" 10 "go.uber.org/zap" 11 12 "github.com/status-im/status-go/images" 13 "github.com/status-im/status-go/protocol/common" 14 "github.com/status-im/status-go/protocol/protobuf" 15 ) 16 17 func getUnfurledLinksFromDB(db *sql.DB, msgID string) ([]*protobuf.UnfurledLink, error) { 18 var result []byte 19 err := db.QueryRow(`SELECT unfurled_links FROM user_messages WHERE id = ?`, msgID).Scan(&result) 20 if err != nil { 21 return nil, fmt.Errorf("could not find message with message-id '%s': %w", msgID, err) 22 } 23 24 var links []*protobuf.UnfurledLink 25 err = json.Unmarshal(result, &links) 26 if err != nil { 27 return nil, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err) 28 } 29 return links, nil 30 } 31 32 func getThumbnailPayload(db *sql.DB, msgID string, thumbnailURL string) ([]byte, error) { 33 var payload []byte 34 35 var links, err = getUnfurledLinksFromDB(db, msgID) 36 if err != nil { 37 return nil, err 38 } 39 40 for _, p := range links { 41 if p.Url == thumbnailURL { 42 payload = p.ThumbnailPayload 43 break 44 } 45 } 46 47 return payload, nil 48 } 49 50 func getFaviconPayload(db *sql.DB, msgID string, faviconURL string) ([]byte, error) { 51 var payload []byte 52 53 var links, err = getUnfurledLinksFromDB(db, msgID) 54 if err != nil { 55 return nil, err 56 } 57 58 for _, p := range links { 59 if p.Url == faviconURL { 60 payload = p.FaviconPayload 61 break 62 } 63 } 64 65 return payload, nil 66 } 67 68 func validateAndReturnImageParams(r *http.Request, w http.ResponseWriter, logger *zap.Logger) ImageParams { 69 params := r.URL.Query() 70 parsed := ParseImageParams(logger, params) 71 72 if parsed.MessageID == "" { 73 http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest) 74 return ImageParams{} 75 } 76 77 if parsed.URL == "" { 78 http.Error(w, "missing query parameter 'url'", http.StatusBadRequest) 79 return ImageParams{} 80 } 81 return parsed 82 } 83 84 func getMimeTypeAndWriteImage(w http.ResponseWriter, logger *zap.Logger, imagePayload []byte) { 85 mimeType, err := images.GetMimeType(imagePayload) 86 if err != nil { 87 http.Error(w, "mime type not supported", http.StatusNotImplemented) 88 return 89 } 90 91 w.Header().Set("Content-Type", "image/"+mimeType) 92 w.Header().Set("Cache-Control", "no-store") 93 94 _, err = w.Write(imagePayload) 95 if err != nil { 96 logger.Error("failed to write response", zap.Error(err)) 97 } 98 } 99 100 func checkForFetchImageError(err error, logger *zap.Logger, parsedImageParams ImageParams, w http.ResponseWriter, imageType string) { 101 if err != nil { 102 logger.Error("failed to get "+imageType, zap.String("msgID", parsedImageParams.MessageID)) 103 http.Error(w, "failed to get "+imageType, http.StatusInternalServerError) 104 return 105 } 106 } 107 108 func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc { 109 return func(w http.ResponseWriter, r *http.Request) { 110 parsed := validateAndReturnImageParams(r, w, logger) 111 if parsed.URL != "" { 112 thumbnail, err := getThumbnailPayload(db, parsed.MessageID, parsed.URL) 113 checkForFetchImageError(err, logger, parsed, w, "thumbnail") 114 getMimeTypeAndWriteImage(w, logger, thumbnail) 115 } 116 } 117 } 118 119 func handleLinkPreviewFavicon(db *sql.DB, logger *zap.Logger) http.HandlerFunc { 120 return func(w http.ResponseWriter, r *http.Request) { 121 parsed := validateAndReturnImageParams(r, w, logger) 122 if parsed.URL != "" { 123 favicon, err := getFaviconPayload(db, parsed.MessageID, parsed.URL) 124 checkForFetchImageError(err, logger, parsed, w, "favicon") 125 getMimeTypeAndWriteImage(w, logger, favicon) 126 } 127 } 128 } 129 130 func getStatusLinkPreviewImage(p *protobuf.UnfurledStatusLink, imageID common.MediaServerImageID) ([]byte, error) { 131 132 switch imageID { 133 case common.MediaServerContactIcon: 134 contact := p.GetContact() 135 if contact == nil { 136 return nil, fmt.Errorf("this is not a contact link") 137 } 138 if contact.Icon == nil { 139 return nil, fmt.Errorf("contact icon is empty") 140 } 141 return contact.Icon.Payload, nil 142 143 case common.MediaServerCommunityIcon: 144 community := p.GetCommunity() 145 if community == nil { 146 return nil, fmt.Errorf("this is not a community link") 147 } 148 if community.Icon == nil { 149 return nil, fmt.Errorf("community icon is empty") 150 } 151 return community.Icon.Payload, nil 152 153 case common.MediaServerCommunityBanner: 154 community := p.GetCommunity() 155 if community == nil { 156 return nil, fmt.Errorf("this is not a community link") 157 } 158 if community.Banner == nil { 159 return nil, fmt.Errorf("community banner is empty") 160 } 161 return community.Banner.Payload, nil 162 163 case common.MediaServerChannelCommunityIcon: 164 channel := p.GetChannel() 165 if channel == nil { 166 return nil, fmt.Errorf("this is not a community channel link") 167 } 168 if channel.Community == nil { 169 return nil, fmt.Errorf("channel community is empty") 170 } 171 if channel.Community.Icon == nil { 172 return nil, fmt.Errorf("channel community icon is empty") 173 } 174 return channel.Community.Icon.Payload, nil 175 176 case common.MediaServerChannelCommunityBanner: 177 channel := p.GetChannel() 178 if channel == nil { 179 return nil, fmt.Errorf("this is not a community channel link") 180 } 181 if channel.Community == nil { 182 return nil, fmt.Errorf("channel community is empty") 183 } 184 if channel.Community.Banner == nil { 185 return nil, fmt.Errorf("channel community banner is empty") 186 } 187 return channel.Community.Banner.Payload, nil 188 } 189 190 return nil, fmt.Errorf("value not supported") 191 } 192 193 func getStatusLinkPreviewThumbnail(db *sql.DB, messageID string, URL string, imageID common.MediaServerImageID) ([]byte, int, error) { 194 var messageLinks []byte 195 err := db.QueryRow(`SELECT unfurled_status_links FROM user_messages WHERE id = ?`, messageID).Scan(&messageLinks) 196 if err != nil { 197 return nil, http.StatusBadRequest, fmt.Errorf("could not find message with message-id '%s': %w", messageID, err) 198 } 199 200 var links protobuf.UnfurledStatusLinks 201 err = proto.Unmarshal(messageLinks, &links) 202 if err != nil { 203 return nil, http.StatusInternalServerError, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err) 204 } 205 206 for _, p := range links.UnfurledStatusLinks { 207 if p.Url == URL { 208 thumbnailPayload, err := getStatusLinkPreviewImage(p, imageID) 209 if err != nil { 210 return nil, http.StatusBadRequest, fmt.Errorf("invalid query parameter 'image-id' value: %w", err) 211 } 212 return thumbnailPayload, http.StatusOK, nil 213 } 214 } 215 216 return nil, http.StatusBadRequest, fmt.Errorf("no link preview found for given url") 217 } 218 219 func handleStatusLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc { 220 return func(w http.ResponseWriter, r *http.Request) { 221 params := r.URL.Query() 222 parsed := ParseImageParams(logger, params) 223 224 if parsed.MessageID == "" { 225 http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest) 226 return 227 } 228 229 if parsed.URL == "" { 230 http.Error(w, "missing query parameter 'url'", http.StatusBadRequest) 231 return 232 } 233 234 if parsed.ImageID == "" { 235 http.Error(w, "missing query parameter 'image-id'", http.StatusBadRequest) 236 return 237 } 238 239 thumbnail, httpsStatusCode, err := getStatusLinkPreviewThumbnail(db, parsed.MessageID, parsed.URL, common.MediaServerImageID(parsed.ImageID)) 240 if err != nil { 241 http.Error(w, err.Error(), httpsStatusCode) 242 return 243 } 244 245 mimeType, err := images.GetMimeType(thumbnail) 246 if err != nil { 247 http.Error(w, "mime type not supported", http.StatusNotImplemented) 248 return 249 } 250 251 w.Header().Set("Content-Type", "image/"+mimeType) 252 w.Header().Set("Cache-Control", "no-store") 253 254 _, err = w.Write(thumbnail) 255 if err != nil { 256 logger.Error("failed to write response", zap.Error(err)) 257 } 258 } 259 }