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  }