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  }