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 }