2023-10-13 12:25:34 +00:00
|
|
|
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"
|
|
|
|
)
|
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
func getUnfurledLinksFromDB(db *sql.DB, msgID string) ([]*protobuf.UnfurledLink, error) {
|
2023-10-13 12:25:34 +00:00
|
|
|
var result []byte
|
|
|
|
err := db.QueryRow(`SELECT unfurled_links FROM user_messages WHERE id = ?`, msgID).Scan(&result)
|
|
|
|
if err != nil {
|
2024-03-12 20:47:51 +00:00
|
|
|
return nil, fmt.Errorf("could not find message with message-id '%s': %w", msgID, err)
|
2023-10-13 12:25:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
var links []*protobuf.UnfurledLink
|
|
|
|
err = json.Unmarshal(result, &links)
|
|
|
|
if err != nil {
|
2024-03-12 20:47:51 +00:00
|
|
|
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
|
2023-10-13 12:25:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for _, p := range links {
|
|
|
|
if p.Url == thumbnailURL {
|
|
|
|
payload = p.ThumbnailPayload
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return payload, nil
|
|
|
|
}
|
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
func getFaviconPayload(db *sql.DB, msgID string, faviconURL string) ([]byte, error) {
|
|
|
|
var payload []byte
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
var links, err = getUnfurledLinksFromDB(db, msgID)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
for _, p := range links {
|
|
|
|
if p.Url == faviconURL {
|
|
|
|
payload = p.FaviconPayload
|
|
|
|
break
|
2023-10-13 12:25:34 +00:00
|
|
|
}
|
2024-03-12 20:47:51 +00:00
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
return payload, nil
|
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
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))
|
|
|
|
}
|
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
2023-10-13 12:25:34 +00:00
|
|
|
|
2024-03-12 20:47:51 +00:00
|
|
|
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)
|
2023-10-13 12:25:34 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-01-18 18:54:54 +00:00
|
|
|
w.Header().Set("Content-Type", "image/"+mimeType)
|
2023-10-13 12:25:34 +00:00
|
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
|
|
|
|
|
|
_, err = w.Write(thumbnail)
|
|
|
|
if err != nil {
|
|
|
|
logger.Error("failed to write response", zap.Error(err))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|