github.com/lologarithm/mattermost-server@v5.3.2-0.20181002060438-c82a84ed765b+incompatible/model/post.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/json"
     8  	"io"
     9  	"net/http"
    10  	"sort"
    11  	"strings"
    12  	"unicode/utf8"
    13  
    14  	"github.com/mattermost/mattermost-server/utils/markdown"
    15  )
    16  
    17  const (
    18  	POST_SYSTEM_MESSAGE_PREFIX  = "system_"
    19  	POST_DEFAULT                = ""
    20  	POST_SLACK_ATTACHMENT       = "slack_attachment"
    21  	POST_SYSTEM_GENERIC         = "system_generic"
    22  	POST_JOIN_LEAVE             = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead
    23  	POST_JOIN_CHANNEL           = "system_join_channel"
    24  	POST_LEAVE_CHANNEL          = "system_leave_channel"
    25  	POST_JOIN_TEAM              = "system_join_team"
    26  	POST_LEAVE_TEAM             = "system_leave_team"
    27  	POST_AUTO_RESPONDER         = "system_auto_responder"
    28  	POST_ADD_REMOVE             = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead
    29  	POST_ADD_TO_CHANNEL         = "system_add_to_channel"
    30  	POST_REMOVE_FROM_CHANNEL    = "system_remove_from_channel"
    31  	POST_MOVE_CHANNEL           = "system_move_channel"
    32  	POST_ADD_TO_TEAM            = "system_add_to_team"
    33  	POST_REMOVE_FROM_TEAM       = "system_remove_from_team"
    34  	POST_HEADER_CHANGE          = "system_header_change"
    35  	POST_DISPLAYNAME_CHANGE     = "system_displayname_change"
    36  	POST_CONVERT_CHANNEL        = "system_convert_channel"
    37  	POST_PURPOSE_CHANGE         = "system_purpose_change"
    38  	POST_CHANNEL_DELETED        = "system_channel_deleted"
    39  	POST_EPHEMERAL              = "system_ephemeral"
    40  	POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy"
    41  	POST_FILEIDS_MAX_RUNES      = 150
    42  	POST_FILENAMES_MAX_RUNES    = 4000
    43  	POST_HASHTAGS_MAX_RUNES     = 1000
    44  	POST_MESSAGE_MAX_RUNES_V1   = 4000
    45  	POST_MESSAGE_MAX_BYTES_V2   = 65535                         // Maximum size of a TEXT column in MySQL
    46  	POST_MESSAGE_MAX_RUNES_V2   = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation
    47  	POST_PROPS_MAX_RUNES        = 8000
    48  	POST_PROPS_MAX_USER_RUNES   = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications
    49  	POST_CUSTOM_TYPE_PREFIX     = "custom_"
    50  	PROPS_ADD_CHANNEL_MEMBER    = "add_channel_member"
    51  	POST_PROPS_ADDED_USER_ID    = "addedUserId"
    52  	POST_PROPS_DELETE_BY        = "deleteBy"
    53  	POST_ACTION_TYPE_BUTTON     = "button"
    54  	POST_ACTION_TYPE_SELECT     = "select"
    55  )
    56  
    57  type Post struct {
    58  	Id         string `json:"id"`
    59  	CreateAt   int64  `json:"create_at"`
    60  	UpdateAt   int64  `json:"update_at"`
    61  	EditAt     int64  `json:"edit_at"`
    62  	DeleteAt   int64  `json:"delete_at"`
    63  	IsPinned   bool   `json:"is_pinned"`
    64  	UserId     string `json:"user_id"`
    65  	ChannelId  string `json:"channel_id"`
    66  	RootId     string `json:"root_id"`
    67  	ParentId   string `json:"parent_id"`
    68  	OriginalId string `json:"original_id"`
    69  
    70  	Message string `json:"message"`
    71  
    72  	// MessageSource will contain the message as submitted by the user if Message has been modified
    73  	// by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
    74  	// populate edit boxes if present.
    75  	MessageSource string `json:"message_source,omitempty" db:"-"`
    76  
    77  	Type          string          `json:"type"`
    78  	Props         StringInterface `json:"props"`
    79  	Hashtags      string          `json:"hashtags"`
    80  	Filenames     StringArray     `json:"filenames,omitempty"` // Deprecated, do not use this field any more
    81  	FileIds       StringArray     `json:"file_ids,omitempty"`
    82  	PendingPostId string          `json:"pending_post_id" db:"-"`
    83  	HasReactions  bool            `json:"has_reactions,omitempty"`
    84  }
    85  
    86  type PostEphemeral struct {
    87  	UserID string `json:"user_id"`
    88  	Post   *Post  `json:"post"`
    89  }
    90  
    91  type PostPatch struct {
    92  	IsPinned     *bool            `json:"is_pinned"`
    93  	Message      *string          `json:"message"`
    94  	Props        *StringInterface `json:"props"`
    95  	FileIds      *StringArray     `json:"file_ids"`
    96  	HasReactions *bool            `json:"has_reactions"`
    97  }
    98  
    99  type SearchParameter struct {
   100  	Terms                  *string `json:"terms"`
   101  	IsOrSearch             *bool   `json:"is_or_search"`
   102  	TimeZoneOffset         *int    `json:"time_zone_offset"`
   103  	Page                   *int    `json:"page"`
   104  	PerPage                *int    `json:"per_page"`
   105  	IncludeDeletedChannels *bool   `json:"include_deleted_channels"`
   106  }
   107  
   108  func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
   109  	copy := *o
   110  	if copy.Message != nil {
   111  		*copy.Message = RewriteImageURLs(*o.Message, f)
   112  	}
   113  	return &copy
   114  }
   115  
   116  type PostForExport struct {
   117  	Post
   118  	TeamName    string
   119  	ChannelName string
   120  	Username    string
   121  	ReplyCount  int
   122  }
   123  
   124  type ReplyForExport struct {
   125  	Post
   126  	Username string
   127  }
   128  
   129  type PostForIndexing struct {
   130  	Post
   131  	TeamId         string `json:"team_id"`
   132  	ParentCreateAt *int64 `json:"parent_create_at"`
   133  }
   134  
   135  type DoPostActionRequest struct {
   136  	SelectedOption string `json:"selected_option"`
   137  }
   138  
   139  type PostAction struct {
   140  	Id          string                 `json:"id"`
   141  	Name        string                 `json:"name"`
   142  	Type        string                 `json:"type"`
   143  	DataSource  string                 `json:"data_source"`
   144  	Options     []*PostActionOptions   `json:"options"`
   145  	Integration *PostActionIntegration `json:"integration,omitempty"`
   146  }
   147  
   148  type PostActionOptions struct {
   149  	Text  string `json:"text"`
   150  	Value string `json:"value"`
   151  }
   152  
   153  type PostActionIntegration struct {
   154  	URL     string          `json:"url,omitempty"`
   155  	Context StringInterface `json:"context,omitempty"`
   156  }
   157  
   158  type PostActionIntegrationRequest struct {
   159  	UserId     string          `json:"user_id"`
   160  	ChannelId  string          `json:"channel_id"`
   161  	TeamId     string          `json:"team_id"`
   162  	PostId     string          `json:"post_id"`
   163  	Type       string          `json:"type"`
   164  	DataSource string          `json:"data_source"`
   165  	Context    StringInterface `json:"context,omitempty"`
   166  }
   167  
   168  type PostActionIntegrationResponse struct {
   169  	Update        *Post  `json:"update"`
   170  	EphemeralText string `json:"ephemeral_text"`
   171  }
   172  
   173  func (o *Post) ToJson() string {
   174  	copy := *o
   175  	copy.StripActionIntegrations()
   176  	b, _ := json.Marshal(&copy)
   177  	return string(b)
   178  }
   179  
   180  func (o *Post) ToUnsanitizedJson() string {
   181  	b, _ := json.Marshal(o)
   182  	return string(b)
   183  }
   184  
   185  func PostFromJson(data io.Reader) *Post {
   186  	var o *Post
   187  	json.NewDecoder(data).Decode(&o)
   188  	return o
   189  }
   190  
   191  func (o *Post) Etag() string {
   192  	return Etag(o.Id, o.UpdateAt)
   193  }
   194  
   195  func (o *Post) IsValid(maxPostSize int) *AppError {
   196  
   197  	if len(o.Id) != 26 {
   198  		return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
   199  	}
   200  
   201  	if o.CreateAt == 0 {
   202  		return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   203  	}
   204  
   205  	if o.UpdateAt == 0 {
   206  		return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   207  	}
   208  
   209  	if len(o.UserId) != 26 {
   210  		return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
   211  	}
   212  
   213  	if len(o.ChannelId) != 26 {
   214  		return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
   215  	}
   216  
   217  	if !(len(o.RootId) == 26 || len(o.RootId) == 0) {
   218  		return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
   219  	}
   220  
   221  	if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) {
   222  		return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest)
   223  	}
   224  
   225  	if len(o.ParentId) == 26 && len(o.RootId) == 0 {
   226  		return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest)
   227  	}
   228  
   229  	if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) {
   230  		return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
   231  	}
   232  
   233  	if utf8.RuneCountInString(o.Message) > maxPostSize {
   234  		return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   235  	}
   236  
   237  	if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES {
   238  		return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   239  	}
   240  
   241  	switch o.Type {
   242  	case
   243  		POST_DEFAULT,
   244  		POST_JOIN_LEAVE,
   245  		POST_AUTO_RESPONDER,
   246  		POST_ADD_REMOVE,
   247  		POST_JOIN_CHANNEL,
   248  		POST_LEAVE_CHANNEL,
   249  		POST_JOIN_TEAM,
   250  		POST_LEAVE_TEAM,
   251  		POST_ADD_TO_CHANNEL,
   252  		POST_REMOVE_FROM_CHANNEL,
   253  		POST_MOVE_CHANNEL,
   254  		POST_ADD_TO_TEAM,
   255  		POST_REMOVE_FROM_TEAM,
   256  		POST_SLACK_ATTACHMENT,
   257  		POST_HEADER_CHANGE,
   258  		POST_PURPOSE_CHANGE,
   259  		POST_DISPLAYNAME_CHANGE,
   260  		POST_CONVERT_CHANNEL,
   261  		POST_CHANNEL_DELETED,
   262  		POST_CHANGE_CHANNEL_PRIVACY:
   263  	default:
   264  		if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
   265  			return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
   266  		}
   267  	}
   268  
   269  	if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES {
   270  		return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   271  	}
   272  
   273  	if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES {
   274  		return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   275  	}
   276  
   277  	if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES {
   278  		return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   279  	}
   280  
   281  	return nil
   282  }
   283  
   284  func (o *Post) SanitizeProps() {
   285  	membersToSanitize := []string{
   286  		PROPS_ADD_CHANNEL_MEMBER,
   287  	}
   288  
   289  	for _, member := range membersToSanitize {
   290  		if _, ok := o.Props[member]; ok {
   291  			delete(o.Props, member)
   292  		}
   293  	}
   294  }
   295  
   296  func (o *Post) PreSave() {
   297  	if o.Id == "" {
   298  		o.Id = NewId()
   299  	}
   300  
   301  	o.OriginalId = ""
   302  
   303  	if o.CreateAt == 0 {
   304  		o.CreateAt = GetMillis()
   305  	}
   306  
   307  	o.UpdateAt = o.CreateAt
   308  	o.PreCommit()
   309  }
   310  
   311  func (o *Post) PreCommit() {
   312  	if o.Props == nil {
   313  		o.Props = make(map[string]interface{})
   314  	}
   315  
   316  	if o.Filenames == nil {
   317  		o.Filenames = []string{}
   318  	}
   319  
   320  	if o.FileIds == nil {
   321  		o.FileIds = []string{}
   322  	}
   323  
   324  	o.GenerateActionIds()
   325  }
   326  
   327  func (o *Post) MakeNonNil() {
   328  	if o.Props == nil {
   329  		o.Props = make(map[string]interface{})
   330  	}
   331  }
   332  
   333  func (o *Post) AddProp(key string, value interface{}) {
   334  
   335  	o.MakeNonNil()
   336  
   337  	o.Props[key] = value
   338  }
   339  
   340  func (o *Post) IsSystemMessage() bool {
   341  	return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
   342  }
   343  
   344  func (p *Post) Patch(patch *PostPatch) {
   345  	if patch.IsPinned != nil {
   346  		p.IsPinned = *patch.IsPinned
   347  	}
   348  
   349  	if patch.Message != nil {
   350  		p.Message = *patch.Message
   351  	}
   352  
   353  	if patch.Props != nil {
   354  		p.Props = *patch.Props
   355  	}
   356  
   357  	if patch.FileIds != nil {
   358  		p.FileIds = *patch.FileIds
   359  	}
   360  
   361  	if patch.HasReactions != nil {
   362  		p.HasReactions = *patch.HasReactions
   363  	}
   364  }
   365  
   366  func (o *PostPatch) ToJson() string {
   367  	b, err := json.Marshal(o)
   368  	if err != nil {
   369  		return ""
   370  	}
   371  
   372  	return string(b)
   373  }
   374  
   375  func PostPatchFromJson(data io.Reader) *PostPatch {
   376  	decoder := json.NewDecoder(data)
   377  	var post PostPatch
   378  	err := decoder.Decode(&post)
   379  	if err != nil {
   380  		return nil
   381  	}
   382  
   383  	return &post
   384  }
   385  
   386  func (o *SearchParameter) SearchParameterToJson() string {
   387  	b, err := json.Marshal(o)
   388  	if err != nil {
   389  		return ""
   390  	}
   391  
   392  	return string(b)
   393  }
   394  
   395  func SearchParameterFromJson(data io.Reader) *SearchParameter {
   396  	decoder := json.NewDecoder(data)
   397  	var searchParam SearchParameter
   398  	err := decoder.Decode(&searchParam)
   399  	if err != nil {
   400  		return nil
   401  	}
   402  
   403  	return &searchParam
   404  }
   405  
   406  func (o *Post) ChannelMentions() []string {
   407  	return ChannelMentions(o.Message)
   408  }
   409  
   410  func (r *PostActionIntegrationRequest) ToJson() string {
   411  	b, _ := json.Marshal(r)
   412  	return string(b)
   413  }
   414  
   415  func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest {
   416  	var o *PostActionIntegrationRequest
   417  	err := json.NewDecoder(data).Decode(&o)
   418  	if err != nil {
   419  		return nil
   420  	}
   421  	return o
   422  }
   423  
   424  func (r *PostActionIntegrationResponse) ToJson() string {
   425  	b, _ := json.Marshal(r)
   426  	return string(b)
   427  }
   428  
   429  func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse {
   430  	var o *PostActionIntegrationResponse
   431  	err := json.NewDecoder(data).Decode(&o)
   432  	if err != nil {
   433  		return nil
   434  	}
   435  	return o
   436  }
   437  
   438  func (o *Post) Attachments() []*SlackAttachment {
   439  	if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
   440  		return attachments
   441  	}
   442  	var ret []*SlackAttachment
   443  	if attachments, ok := o.Props["attachments"].([]interface{}); ok {
   444  		for _, attachment := range attachments {
   445  			if enc, err := json.Marshal(attachment); err == nil {
   446  				var decoded SlackAttachment
   447  				if json.Unmarshal(enc, &decoded) == nil {
   448  					ret = append(ret, &decoded)
   449  				}
   450  			}
   451  		}
   452  	}
   453  	return ret
   454  }
   455  
   456  func (o *Post) StripActionIntegrations() {
   457  	attachments := o.Attachments()
   458  	if o.Props["attachments"] != nil {
   459  		o.Props["attachments"] = attachments
   460  	}
   461  	for _, attachment := range attachments {
   462  		for _, action := range attachment.Actions {
   463  			action.Integration = nil
   464  		}
   465  	}
   466  }
   467  
   468  func (o *Post) GetAction(id string) *PostAction {
   469  	for _, attachment := range o.Attachments() {
   470  		for _, action := range attachment.Actions {
   471  			if action.Id == id {
   472  				return action
   473  			}
   474  		}
   475  	}
   476  	return nil
   477  }
   478  
   479  func (o *Post) GenerateActionIds() {
   480  	if o.Props["attachments"] != nil {
   481  		o.Props["attachments"] = o.Attachments()
   482  	}
   483  	if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok {
   484  		for _, attachment := range attachments {
   485  			for _, action := range attachment.Actions {
   486  				if action.Id == "" {
   487  					action.Id = NewId()
   488  				}
   489  			}
   490  		}
   491  	}
   492  }
   493  
   494  var markdownDestinationEscaper = strings.NewReplacer(
   495  	`\`, `\\`,
   496  	`<`, `\<`,
   497  	`>`, `\>`,
   498  	`(`, `\(`,
   499  	`)`, `\)`,
   500  )
   501  
   502  // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
   503  // rewritten via RewriteImageURLs.
   504  func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
   505  	copy := *o
   506  	copy.Message = RewriteImageURLs(o.Message, f)
   507  	if copy.MessageSource == "" && copy.Message != o.Message {
   508  		copy.MessageSource = o.Message
   509  	}
   510  	return &copy
   511  }
   512  
   513  func (o *PostEphemeral) ToUnsanitizedJson() string {
   514  	b, _ := json.Marshal(o)
   515  	return string(b)
   516  }
   517  
   518  func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest {
   519  	var o *DoPostActionRequest
   520  	json.NewDecoder(data).Decode(&o)
   521  	return o
   522  }
   523  
   524  // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
   525  // according to the function f. For each image URL, f will be invoked, and the resulting markdown
   526  // will contain the URL returned by that invocation instead.
   527  //
   528  // Image URLs are destination URLs used in inline images or reference definitions that are used
   529  // anywhere in the input markdown as an image.
   530  func RewriteImageURLs(message string, f func(string) string) string {
   531  	if !strings.Contains(message, "![") {
   532  		return message
   533  	}
   534  
   535  	var ranges []markdown.Range
   536  
   537  	markdown.Inspect(message, func(blockOrInline interface{}) bool {
   538  		switch v := blockOrInline.(type) {
   539  		case *markdown.ReferenceImage:
   540  			ranges = append(ranges, v.ReferenceDefinition.RawDestination)
   541  		case *markdown.InlineImage:
   542  			ranges = append(ranges, v.RawDestination)
   543  		default:
   544  			return true
   545  		}
   546  		return true
   547  	})
   548  
   549  	if ranges == nil {
   550  		return message
   551  	}
   552  
   553  	sort.Slice(ranges, func(i, j int) bool {
   554  		return ranges[i].Position < ranges[j].Position
   555  	})
   556  
   557  	copyRanges := make([]markdown.Range, 0, len(ranges))
   558  	urls := make([]string, 0, len(ranges))
   559  	resultLength := len(message)
   560  
   561  	start := 0
   562  	for i, r := range ranges {
   563  		switch {
   564  		case i == 0:
   565  		case r.Position != ranges[i-1].Position:
   566  			start = ranges[i-1].End
   567  		default:
   568  			continue
   569  		}
   570  		original := message[r.Position:r.End]
   571  		replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
   572  		resultLength += len(replacement) - len(original)
   573  		copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
   574  		urls = append(urls, replacement)
   575  	}
   576  
   577  	result := make([]byte, resultLength)
   578  
   579  	offset := 0
   580  	for i, r := range copyRanges {
   581  		offset += copy(result[offset:], message[r.Position:r.End])
   582  		offset += copy(result[offset:], urls[i])
   583  	}
   584  	copy(result[offset:], message[ranges[len(ranges)-1].End:])
   585  
   586  	return string(result)
   587  }