github.com/vnforks/kid@v5.11.1+incompatible/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 "hash/fnv" 10 "net/http" 11 "time" 12 13 "github.com/dyatlov/go-opengraph/opengraph" 14 ) 15 16 const ( 17 LINK_METADATA_TYPE_IMAGE LinkMetadataType = "image" 18 LINK_METADATA_TYPE_NONE LinkMetadataType = "none" 19 LINK_METADATA_TYPE_OPENGRAPH LinkMetadataType = "opengraph" 20 ) 21 22 type LinkMetadataType string 23 24 // LinkMetadata stores arbitrary data about a link posted in a message. This includes dimensions of linked images 25 // and OpenGraph metadata. 26 type LinkMetadata struct { 27 // Hash is a value computed from the URL and Timestamp for use as a primary key in the database. 28 Hash int64 29 30 URL string 31 Timestamp int64 32 Type LinkMetadataType 33 34 // Data is the actual metadata for the link. It should contain data of one of the following types: 35 // - *model.PostImage if the linked content is an image 36 // - *opengraph.OpenGraph if the linked content is an HTML document 37 // - nil if the linked content has no metadata 38 Data interface{} 39 } 40 41 func (o *LinkMetadata) PreSave() { 42 o.Hash = GenerateLinkMetadataHash(o.URL, o.Timestamp) 43 } 44 45 func (o *LinkMetadata) IsValid() *AppError { 46 if o.URL == "" { 47 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.url.app_error", nil, "", http.StatusBadRequest) 48 } 49 50 if o.Timestamp == 0 || !isRoundedToNearestHour(o.Timestamp) { 51 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.timestamp.app_error", nil, "", http.StatusBadRequest) 52 } 53 54 switch o.Type { 55 case LINK_METADATA_TYPE_IMAGE: 56 if o.Data == nil { 57 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest) 58 } 59 60 if _, ok := o.Data.(*PostImage); !ok { 61 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) 62 } 63 case LINK_METADATA_TYPE_NONE: 64 if o.Data != nil { 65 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) 66 } 67 case LINK_METADATA_TYPE_OPENGRAPH: 68 if o.Data == nil { 69 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data.app_error", nil, "", http.StatusBadRequest) 70 } 71 72 if _, ok := o.Data.(*opengraph.OpenGraph); !ok { 73 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.data_type.app_error", nil, "", http.StatusBadRequest) 74 } 75 default: 76 return NewAppError("LinkMetadata.IsValid", "model.link_metadata.is_valid.type.app_error", nil, "", http.StatusBadRequest) 77 } 78 79 return nil 80 } 81 82 // DeserializeDataToConcreteType converts o.Data from JSON into properly structured data. This is intended to be used 83 // after getting a LinkMetadata object that has been stored in the database. 84 func (o *LinkMetadata) DeserializeDataToConcreteType() error { 85 var b []byte 86 switch t := o.Data.(type) { 87 case []byte: 88 // MySQL uses a byte slice for JSON 89 b = t 90 case string: 91 // Postgres uses a string for JSON 92 b = []byte(t) 93 } 94 95 if b == nil { 96 // Data doesn't need to be fixed 97 return nil 98 } 99 100 var data interface{} 101 var err error 102 103 switch o.Type { 104 case LINK_METADATA_TYPE_IMAGE: 105 image := &PostImage{} 106 107 err = json.Unmarshal(b, &image) 108 109 data = image 110 case LINK_METADATA_TYPE_OPENGRAPH: 111 og := &opengraph.OpenGraph{} 112 113 json.Unmarshal(b, &og) 114 115 data = og 116 } 117 118 if err != nil { 119 return err 120 } 121 122 o.Data = data 123 124 return nil 125 } 126 127 // FloorToNearestHour takes a timestamp (in milliseconds) and returns it rounded to the previous hour in UTC. 128 func FloorToNearestHour(ms int64) int64 { 129 t := time.Unix(0, ms*int64(1000*1000)) 130 131 return time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), 0, 0, 0, t.Location()).UnixNano() / int64(time.Millisecond) 132 } 133 134 // isRoundedToNearestHour returns true if the given timestamp (in milliseconds) has been rounded to the nearest hour in UTC. 135 func isRoundedToNearestHour(ms int64) bool { 136 return FloorToNearestHour(ms) == ms 137 } 138 139 // GenerateLinkMetadataHash generates a unique hash for a given URL and timestamp for use as a database key. 140 func GenerateLinkMetadataHash(url string, timestamp int64) int64 { 141 hash := fnv.New32() 142 143 // Note that we ignore write errors here because the Hash interface says that its Write will never return an error 144 binary.Write(hash, binary.LittleEndian, timestamp) 145 hash.Write([]byte(url)) 146 147 return int64(hash.Sum32()) 148 }