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