github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/model/link_metadata.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package model
     5  
     6  import (
     7  	"encoding/binary"
     8  	"encoding/json"
     9  	"fmt"
    10  	"hash/fnv"
    11  	"net/http"
    12  	"time"
    13  	"unicode/utf8"
    14  
    15  	"github.com/dyatlov/go-opengraph/opengraph"
    16  )
    17  
    18  const (
    19  	LINK_METADATA_TYPE_IMAGE     LinkMetadataType = "image"
    20  	LINK_METADATA_TYPE_NONE      LinkMetadataType = "none"
    21  	LINK_METADATA_TYPE_OPENGRAPH LinkMetadataType = "opengraph"
    22  	MAX_IMAGES                   int              = 5
    23  )
    24  
    25  type LinkMetadataType string
    26  
    27  // LinkMetadata stores arbitrary data about a link posted in a message. This includes dimensions of linked images
    28  // and OpenGraph metadata.
    29  type LinkMetadata struct {
    30  	// Hash is a value computed from the URL and Timestamp for use as a primary key in the database.
    31  	Hash int64
    32  
    33  	URL       string
    34  	Timestamp int64
    35  	Type      LinkMetadataType
    36  
    37  	// Data is the actual metadata for the link. It should contain data of one of the following types:
    38  	// - *model.PostImage if the linked content is an image
    39  	// - *opengraph.OpenGraph if the linked content is an HTML document
    40  	// - nil if the linked content has no metadata
    41  	Data interface{}
    42  }
    43  
    44  // truncateText ensure string is 300 chars, truncate and add ellipsis
    45  // if it was bigger.
    46  func truncateText(original string) string {
    47  	if utf8.RuneCountInString(original) > 300 {
    48  		return fmt.Sprintf("%.300s[...]", original)
    49  	}
    50  	return original
    51  }
    52  
    53  func firstNImages(images []*opengraph.Image, maxImages int) []*opengraph.Image {
    54  	if maxImages < 0 { // dont break stuff, if it's weird, go for sane defaults
    55  		maxImages = MAX_IMAGES
    56  	}
    57  	numImages := len(images)
    58  	if numImages > maxImages {
    59  		return images[0:maxImages]
    60  	}
    61  	return images
    62  }
    63  
    64  // TruncateOpenGraph ensure OpenGraph metadata doesn't grow too big by
    65  // shortening strings, trimming fields and reducing the number of
    66  // images.
    67  func TruncateOpenGraph(ogdata *opengraph.OpenGraph) *opengraph.OpenGraph {
    68  	if ogdata != nil {
    69  		empty := &opengraph.OpenGraph{}
    70  		ogdata.Title = truncateText(ogdata.Title)
    71  		ogdata.Description = truncateText(ogdata.Description)
    72  		ogdata.SiteName = truncateText(ogdata.SiteName)
    73  		ogdata.Article = empty.Article
    74  		ogdata.Book = empty.Book
    75  		ogdata.Profile = empty.Profile
    76  		ogdata.Determiner = empty.Determiner
    77  		ogdata.Locale = empty.Locale
    78  		ogdata.LocalesAlternate = empty.LocalesAlternate
    79  		ogdata.Images = firstNImages(ogdata.Images, MAX_IMAGES)
    80  		ogdata.Audios = empty.Audios
    81  		ogdata.Videos = empty.Videos
    82  	}
    83  	return ogdata
    84  }
    85  
    86  func (o *LinkMetadata) PreSave() {
    87  	o.Hash = GenerateLinkMetadataHash(o.URL, o.Timestamp)
    88  }
    89  
    90  func (o *LinkMetadata) IsValid() *AppError {
    91  	if o.URL == "" {
    92  		return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.url.app_error", nil, "", http.StatusBadRequest)
    93  	}
    94  
    95  	if o.Timestamp == 0 || !isRoundedToNearestHour(o.Timestamp) {
    96  		return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.timestamp.app_error", nil, "", http.StatusBadRequest)
    97  	}
    98  
    99  	switch o.Type {
   100  	case LINK_METADATA_TYPE_IMAGE:
   101  		if o.Data == nil {
   102  			return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest)
   103  		}
   104  
   105  		if _, ok := o.Data.(*PostImage); !ok {
   106  			return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
   107  		}
   108  	case LINK_METADATA_TYPE_NONE:
   109  		if o.Data != nil {
   110  			return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
   111  		}
   112  	case LINK_METADATA_TYPE_OPENGRAPH:
   113  		if o.Data == nil {
   114  			return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest)
   115  		}
   116  
   117  		if _, ok := o.Data.(*opengraph.OpenGraph); !ok {
   118  			return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest)
   119  		}
   120  	default:
   121  		return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.type.app_error", nil, "", http.StatusBadRequest)
   122  	}
   123  
   124  	return nil
   125  }
   126  
   127  // DeserializeDataToConcreteType converts o.Data from JSON into properly structured data. This is intended to be used
   128  // after getting a LinkMetadata object that has been stored in the database.
   129  func (o *LinkMetadata) DeserializeDataToConcreteType() error {
   130  	var b []byte
   131  	switch t := o.Data.(type) {
   132  	case []byte:
   133  		// MySQL uses a byte slice for JSON
   134  		b = t
   135  	case string:
   136  		// Postgres uses a string for JSON
   137  		b = []byte(t)
   138  	}
   139  
   140  	if b == nil {
   141  		// Data doesn't need to be fixed
   142  		return nil
   143  	}
   144  
   145  	var data interface{}
   146  	var err error
   147  
   148  	switch o.Type {
   149  	case LINK_METADATA_TYPE_IMAGE:
   150  		image := &PostImage{}
   151  
   152  		err = json.Unmarshal(b, &image)
   153  
   154  		data = image
   155  	case LINK_METADATA_TYPE_OPENGRAPH:
   156  		og := &opengraph.OpenGraph{}
   157  
   158  		json.Unmarshal(b, &og)
   159  
   160  		data = og
   161  	}
   162  
   163  	if err != nil {
   164  		return err
   165  	}
   166  
   167  	o.Data = data
   168  
   169  	return nil
   170  }
   171  
   172  // FloorToNearestHour takes a timestamp (in milliseconds) and returns it rounded to the previous hour in UTC.
   173  func FloorToNearestHour(ms int64) int64 {
   174  	t := time.Unix(0, ms*int64(1000*1000)).UTC()
   175  
   176  	return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, time.UTC).UnixNano() / int64(time.Millisecond)
   177  }
   178  
   179  // isRoundedToNearestHour returns true if the given timestamp (in milliseconds) has been rounded to the nearest hour in UTC.
   180  func isRoundedToNearestHour(ms int64) bool {
   181  	return FloorToNearestHour(ms) == ms
   182  }
   183  
   184  // GenerateLinkMetadataHash generates a unique hash for a given URL and timestamp for use as a database key.
   185  func GenerateLinkMetadataHash(url string, timestamp int64) int64 {
   186  	hash := fnv.New32()
   187  
   188  	// Note that we ignore write errors here because the Hash interface says that its Write will never return an error
   189  	binary.Write(hash, binary.LittleEndian, timestamp)
   190  	hash.Write([]byte(url))
   191  
   192  	return int64(hash.Sum32())
   193  }