status-go/server/handlers_linkpreview.go

260 lines
7.4 KiB
Go

package server
import (
"database/sql"
"encoding/json"
"fmt"
"net/http"
"github.com/golang/protobuf/proto"
"go.uber.org/zap"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/protocol/common"
"github.com/status-im/status-go/protocol/protobuf"
)
func getUnfurledLinksFromDB(db *sql.DB, msgID string) ([]*protobuf.UnfurledLink, error) {
var result []byte
err := db.QueryRow(`SELECT unfurled_links FROM user_messages WHERE id = ?`, msgID).Scan(&result)
if err != nil {
return nil, fmt.Errorf("could not find message with message-id '%s': %w", msgID, err)
}
var links []*protobuf.UnfurledLink
err = json.Unmarshal(result, &links)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err)
}
return links, nil
}
func getThumbnailPayload(db *sql.DB, msgID string, thumbnailURL string) ([]byte, error) {
var payload []byte
var links, err = getUnfurledLinksFromDB(db, msgID)
if err != nil {
return nil, err
}
for _, p := range links {
if p.Url == thumbnailURL {
payload = p.ThumbnailPayload
break
}
}
return payload, nil
}
func getFaviconPayload(db *sql.DB, msgID string, faviconURL string) ([]byte, error) {
var payload []byte
var links, err = getUnfurledLinksFromDB(db, msgID)
if err != nil {
return nil, err
}
for _, p := range links {
if p.Url == faviconURL {
payload = p.FaviconPayload
break
}
}
return payload, nil
}
func validateAndReturnImageParams(r *http.Request, w http.ResponseWriter, logger *zap.Logger) ImageParams {
params := r.URL.Query()
parsed := ParseImageParams(logger, params)
if parsed.MessageID == "" {
http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest)
return ImageParams{}
}
if parsed.URL == "" {
http.Error(w, "missing query parameter 'url'", http.StatusBadRequest)
return ImageParams{}
}
return parsed
}
func getMimeTypeAndWriteImage(w http.ResponseWriter, logger *zap.Logger, imagePayload []byte) {
mimeType, err := images.GetMimeType(imagePayload)
if err != nil {
http.Error(w, "mime type not supported", http.StatusNotImplemented)
return
}
w.Header().Set("Content-Type", "image/"+mimeType)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(imagePayload)
if err != nil {
logger.Error("failed to write response", zap.Error(err))
}
}
func checkForFetchImageError(err error, logger *zap.Logger, parsedImageParams ImageParams, w http.ResponseWriter, imageType string) {
if err != nil {
logger.Error("failed to get "+imageType, zap.String("msgID", parsedImageParams.MessageID))
http.Error(w, "failed to get "+imageType, http.StatusInternalServerError)
return
}
}
func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
parsed := validateAndReturnImageParams(r, w, logger)
if parsed.URL != "" {
thumbnail, err := getThumbnailPayload(db, parsed.MessageID, parsed.URL)
checkForFetchImageError(err, logger, parsed, w, "thumbnail")
getMimeTypeAndWriteImage(w, logger, thumbnail)
}
}
}
func handleLinkPreviewFavicon(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
parsed := validateAndReturnImageParams(r, w, logger)
if parsed.URL != "" {
favicon, err := getFaviconPayload(db, parsed.MessageID, parsed.URL)
checkForFetchImageError(err, logger, parsed, w, "favicon")
getMimeTypeAndWriteImage(w, logger, favicon)
}
}
}
func getStatusLinkPreviewImage(p *protobuf.UnfurledStatusLink, imageID common.MediaServerImageID) ([]byte, error) {
switch imageID {
case common.MediaServerContactIcon:
contact := p.GetContact()
if contact == nil {
return nil, fmt.Errorf("this is not a contact link")
}
if contact.Icon == nil {
return nil, fmt.Errorf("contact icon is empty")
}
return contact.Icon.Payload, nil
case common.MediaServerCommunityIcon:
community := p.GetCommunity()
if community == nil {
return nil, fmt.Errorf("this is not a community link")
}
if community.Icon == nil {
return nil, fmt.Errorf("community icon is empty")
}
return community.Icon.Payload, nil
case common.MediaServerCommunityBanner:
community := p.GetCommunity()
if community == nil {
return nil, fmt.Errorf("this is not a community link")
}
if community.Banner == nil {
return nil, fmt.Errorf("community banner is empty")
}
return community.Banner.Payload, nil
case common.MediaServerChannelCommunityIcon:
channel := p.GetChannel()
if channel == nil {
return nil, fmt.Errorf("this is not a community channel link")
}
if channel.Community == nil {
return nil, fmt.Errorf("channel community is empty")
}
if channel.Community.Icon == nil {
return nil, fmt.Errorf("channel community icon is empty")
}
return channel.Community.Icon.Payload, nil
case common.MediaServerChannelCommunityBanner:
channel := p.GetChannel()
if channel == nil {
return nil, fmt.Errorf("this is not a community channel link")
}
if channel.Community == nil {
return nil, fmt.Errorf("channel community is empty")
}
if channel.Community.Banner == nil {
return nil, fmt.Errorf("channel community banner is empty")
}
return channel.Community.Banner.Payload, nil
}
return nil, fmt.Errorf("value not supported")
}
func getStatusLinkPreviewThumbnail(db *sql.DB, messageID string, URL string, imageID common.MediaServerImageID) ([]byte, int, error) {
var messageLinks []byte
err := db.QueryRow(`SELECT unfurled_status_links FROM user_messages WHERE id = ?`, messageID).Scan(&messageLinks)
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("could not find message with message-id '%s': %w", messageID, err)
}
var links protobuf.UnfurledStatusLinks
err = proto.Unmarshal(messageLinks, &links)
if err != nil {
return nil, http.StatusInternalServerError, fmt.Errorf("failed to unmarshal protobuf.UrlPreview: %w", err)
}
for _, p := range links.UnfurledStatusLinks {
if p.Url == URL {
thumbnailPayload, err := getStatusLinkPreviewImage(p, imageID)
if err != nil {
return nil, http.StatusBadRequest, fmt.Errorf("invalid query parameter 'image-id' value: %w", err)
}
return thumbnailPayload, http.StatusOK, nil
}
}
return nil, http.StatusBadRequest, fmt.Errorf("no link preview found for given url")
}
func handleStatusLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
parsed := ParseImageParams(logger, params)
if parsed.MessageID == "" {
http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest)
return
}
if parsed.URL == "" {
http.Error(w, "missing query parameter 'url'", http.StatusBadRequest)
return
}
if parsed.ImageID == "" {
http.Error(w, "missing query parameter 'image-id'", http.StatusBadRequest)
return
}
thumbnail, httpsStatusCode, err := getStatusLinkPreviewThumbnail(db, parsed.MessageID, parsed.URL, common.MediaServerImageID(parsed.ImageID))
if err != nil {
http.Error(w, err.Error(), httpsStatusCode)
return
}
mimeType, err := images.GetMimeType(thumbnail)
if err != nil {
http.Error(w, "mime type not supported", http.StatusNotImplemented)
return
}
w.Header().Set("Content-Type", "image/"+mimeType)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(thumbnail)
if err != nil {
logger.Error("failed to write response", zap.Error(err))
}
}
}