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