github.com/haalcala/mattermost-server-change-repo/v5@v5.33.2/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  	"errors"
     9  	"io"
    10  	"net/http"
    11  	"regexp"
    12  	"sort"
    13  	"strings"
    14  	"sync"
    15  	"unicode/utf8"
    16  
    17  	"github.com/mattermost/mattermost-server/v5/utils/markdown"
    18  )
    19  
    20  const (
    21  	POST_SYSTEM_MESSAGE_PREFIX  = "system_"
    22  	POST_DEFAULT                = ""
    23  	POST_SLACK_ATTACHMENT       = "slack_attachment"
    24  	POST_SYSTEM_GENERIC         = "system_generic"
    25  	POST_JOIN_LEAVE             = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead
    26  	POST_JOIN_CHANNEL           = "system_join_channel"
    27  	POST_GUEST_JOIN_CHANNEL     = "system_guest_join_channel"
    28  	POST_LEAVE_CHANNEL          = "system_leave_channel"
    29  	POST_JOIN_TEAM              = "system_join_team"
    30  	POST_LEAVE_TEAM             = "system_leave_team"
    31  	POST_AUTO_RESPONDER         = "system_auto_responder"
    32  	POST_ADD_REMOVE             = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead
    33  	POST_ADD_TO_CHANNEL         = "system_add_to_channel"
    34  	POST_ADD_GUEST_TO_CHANNEL   = "system_add_guest_to_chan"
    35  	POST_REMOVE_FROM_CHANNEL    = "system_remove_from_channel"
    36  	POST_MOVE_CHANNEL           = "system_move_channel"
    37  	POST_ADD_TO_TEAM            = "system_add_to_team"
    38  	POST_REMOVE_FROM_TEAM       = "system_remove_from_team"
    39  	POST_HEADER_CHANGE          = "system_header_change"
    40  	POST_DISPLAYNAME_CHANGE     = "system_displayname_change"
    41  	POST_CONVERT_CHANNEL        = "system_convert_channel"
    42  	POST_PURPOSE_CHANGE         = "system_purpose_change"
    43  	POST_CHANNEL_DELETED        = "system_channel_deleted"
    44  	POST_CHANNEL_RESTORED       = "system_channel_restored"
    45  	POST_EPHEMERAL              = "system_ephemeral"
    46  	POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy"
    47  	POST_ADD_BOT_TEAMS_CHANNELS = "add_bot_teams_channels"
    48  	POST_FILEIDS_MAX_RUNES      = 150
    49  	POST_FILENAMES_MAX_RUNES    = 4000
    50  	POST_HASHTAGS_MAX_RUNES     = 1000
    51  	POST_MESSAGE_MAX_RUNES_V1   = 4000
    52  	POST_MESSAGE_MAX_BYTES_V2   = 65535                         // Maximum size of a TEXT column in MySQL
    53  	POST_MESSAGE_MAX_RUNES_V2   = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation
    54  	POST_PROPS_MAX_RUNES        = 8000
    55  	POST_PROPS_MAX_USER_RUNES   = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications
    56  	POST_CUSTOM_TYPE_PREFIX     = "custom_"
    57  	POST_ME                     = "me"
    58  	PROPS_ADD_CHANNEL_MEMBER    = "add_channel_member"
    59  
    60  	POST_PROPS_ADDED_USER_ID       = "addedUserId"
    61  	POST_PROPS_DELETE_BY           = "deleteBy"
    62  	POST_PROPS_OVERRIDE_ICON_URL   = "override_icon_url"
    63  	POST_PROPS_OVERRIDE_ICON_EMOJI = "override_icon_emoji"
    64  
    65  	POST_PROPS_MENTION_HIGHLIGHT_DISABLED = "mentionHighlightDisabled"
    66  	POST_PROPS_GROUP_HIGHLIGHT_DISABLED   = "disable_group_highlight"
    67  	POST_SYSTEM_WARN_METRIC_STATUS        = "warn_metric_status"
    68  )
    69  
    70  var AT_MENTION_PATTEN = regexp.MustCompile(`\B@`)
    71  
    72  type Post struct {
    73  	Id         string `json:"id"`
    74  	CreateAt   int64  `json:"create_at"`
    75  	UpdateAt   int64  `json:"update_at"`
    76  	EditAt     int64  `json:"edit_at"`
    77  	DeleteAt   int64  `json:"delete_at"`
    78  	IsPinned   bool   `json:"is_pinned"`
    79  	UserId     string `json:"user_id"`
    80  	ChannelId  string `json:"channel_id"`
    81  	RootId     string `json:"root_id"`
    82  	ParentId   string `json:"parent_id"`
    83  	OriginalId string `json:"original_id"`
    84  
    85  	Message string `json:"message"`
    86  	// MessageSource will contain the message as submitted by the user if Message has been modified
    87  	// by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
    88  	// populate edit boxes if present.
    89  	MessageSource string `json:"message_source,omitempty" db:"-"`
    90  
    91  	Type          string          `json:"type"`
    92  	propsMu       sync.RWMutex    `db:"-"`       // Unexported mutex used to guard Post.Props.
    93  	Props         StringInterface `json:"props"` // Deprecated: use GetProps()
    94  	Hashtags      string          `json:"hashtags"`
    95  	Filenames     StringArray     `json:"filenames,omitempty"` // Deprecated, do not use this field any more
    96  	FileIds       StringArray     `json:"file_ids,omitempty"`
    97  	PendingPostId string          `json:"pending_post_id" db:"-"`
    98  	HasReactions  bool            `json:"has_reactions,omitempty"`
    99  
   100  	// Transient data populated before sending a post to the client
   101  	ReplyCount   int64         `json:"reply_count" db:"-"`
   102  	LastReplyAt  int64         `json:"last_reply_at" db:"-"`
   103  	Participants []*User       `json:"participants" db:"-"`
   104  	Metadata     *PostMetadata `json:"metadata,omitempty" db:"-"`
   105  }
   106  
   107  type PostEphemeral struct {
   108  	UserID string `json:"user_id"`
   109  	Post   *Post  `json:"post"`
   110  }
   111  
   112  type PostPatch struct {
   113  	IsPinned     *bool            `json:"is_pinned"`
   114  	Message      *string          `json:"message"`
   115  	Props        *StringInterface `json:"props"`
   116  	FileIds      *StringArray     `json:"file_ids"`
   117  	HasReactions *bool            `json:"has_reactions"`
   118  }
   119  
   120  type SearchParameter struct {
   121  	Terms                  *string `json:"terms"`
   122  	IsOrSearch             *bool   `json:"is_or_search"`
   123  	TimeZoneOffset         *int    `json:"time_zone_offset"`
   124  	Page                   *int    `json:"page"`
   125  	PerPage                *int    `json:"per_page"`
   126  	IncludeDeletedChannels *bool   `json:"include_deleted_channels"`
   127  }
   128  
   129  type AnalyticsPostCountsOptions struct {
   130  	TeamId        string
   131  	BotsOnly      bool
   132  	YesterdayOnly bool
   133  }
   134  
   135  func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
   136  	copy := *o
   137  	if copy.Message != nil {
   138  		*copy.Message = RewriteImageURLs(*o.Message, f)
   139  	}
   140  	return &copy
   141  }
   142  
   143  type PostForExport struct {
   144  	Post
   145  	TeamName    string
   146  	ChannelName string
   147  	Username    string
   148  	ReplyCount  int
   149  }
   150  
   151  type DirectPostForExport struct {
   152  	Post
   153  	User           string
   154  	ChannelMembers *[]string
   155  }
   156  
   157  type ReplyForExport struct {
   158  	Post
   159  	Username string
   160  }
   161  
   162  type PostForIndexing struct {
   163  	Post
   164  	TeamId         string `json:"team_id"`
   165  	ParentCreateAt *int64 `json:"parent_create_at"`
   166  }
   167  
   168  type FileForIndexing struct {
   169  	FileInfo
   170  	ChannelId string `json:"channel_id"`
   171  	Content   string `json:"content"`
   172  }
   173  
   174  // ShallowCopy is an utility function to shallow copy a Post to the given
   175  // destination without touching the internal RWMutex.
   176  func (o *Post) ShallowCopy(dst *Post) error {
   177  	if dst == nil {
   178  		return errors.New("dst cannot be nil")
   179  	}
   180  	o.propsMu.RLock()
   181  	defer o.propsMu.RUnlock()
   182  	dst.propsMu.Lock()
   183  	defer dst.propsMu.Unlock()
   184  	dst.Id = o.Id
   185  	dst.CreateAt = o.CreateAt
   186  	dst.UpdateAt = o.UpdateAt
   187  	dst.EditAt = o.EditAt
   188  	dst.DeleteAt = o.DeleteAt
   189  	dst.IsPinned = o.IsPinned
   190  	dst.UserId = o.UserId
   191  	dst.ChannelId = o.ChannelId
   192  	dst.RootId = o.RootId
   193  	dst.ParentId = o.ParentId
   194  	dst.OriginalId = o.OriginalId
   195  	dst.Message = o.Message
   196  	dst.MessageSource = o.MessageSource
   197  	dst.Type = o.Type
   198  	dst.Props = o.Props
   199  	dst.Hashtags = o.Hashtags
   200  	dst.Filenames = o.Filenames
   201  	dst.FileIds = o.FileIds
   202  	dst.PendingPostId = o.PendingPostId
   203  	dst.HasReactions = o.HasReactions
   204  	dst.ReplyCount = o.ReplyCount
   205  	dst.Participants = o.Participants
   206  	dst.LastReplyAt = o.LastReplyAt
   207  	dst.Metadata = o.Metadata
   208  	return nil
   209  }
   210  
   211  // Clone shallowly copies the post and returns the copy.
   212  func (o *Post) Clone() *Post {
   213  	copy := &Post{}
   214  	o.ShallowCopy(copy)
   215  	return copy
   216  }
   217  
   218  func (o *Post) ToJson() string {
   219  	copy := o.Clone()
   220  	copy.StripActionIntegrations()
   221  	b, _ := json.Marshal(copy)
   222  	return string(b)
   223  }
   224  
   225  func (o *Post) ToUnsanitizedJson() string {
   226  	b, _ := json.Marshal(o)
   227  	return string(b)
   228  }
   229  
   230  type GetPostsSinceOptions struct {
   231  	ChannelId                string
   232  	Time                     int64
   233  	SkipFetchThreads         bool
   234  	CollapsedThreads         bool
   235  	CollapsedThreadsExtended bool
   236  }
   237  
   238  type GetPostsOptions struct {
   239  	ChannelId                string
   240  	PostId                   string
   241  	Page                     int
   242  	PerPage                  int
   243  	SkipFetchThreads         bool
   244  	CollapsedThreads         bool
   245  	CollapsedThreadsExtended bool
   246  }
   247  
   248  func PostFromJson(data io.Reader) *Post {
   249  	var o *Post
   250  	json.NewDecoder(data).Decode(&o)
   251  	return o
   252  }
   253  
   254  func (o *Post) Etag() string {
   255  	return Etag(o.Id, o.UpdateAt)
   256  }
   257  
   258  func (o *Post) IsValid(maxPostSize int) *AppError {
   259  	if !IsValidId(o.Id) {
   260  		return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
   261  	}
   262  
   263  	if o.CreateAt == 0 {
   264  		return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   265  	}
   266  
   267  	if o.UpdateAt == 0 {
   268  		return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   269  	}
   270  
   271  	if !IsValidId(o.UserId) {
   272  		return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
   273  	}
   274  
   275  	if !IsValidId(o.ChannelId) {
   276  		return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
   277  	}
   278  
   279  	if !(IsValidId(o.RootId) || o.RootId == "") {
   280  		return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
   281  	}
   282  
   283  	if !(IsValidId(o.ParentId) || o.ParentId == "") {
   284  		return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest)
   285  	}
   286  
   287  	if len(o.ParentId) == 26 && o.RootId == "" {
   288  		return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest)
   289  	}
   290  
   291  	if !(len(o.OriginalId) == 26 || o.OriginalId == "") {
   292  		return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
   293  	}
   294  
   295  	if utf8.RuneCountInString(o.Message) > maxPostSize {
   296  		return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   297  	}
   298  
   299  	if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES {
   300  		return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   301  	}
   302  
   303  	switch o.Type {
   304  	case
   305  		POST_DEFAULT,
   306  		POST_SYSTEM_GENERIC,
   307  		POST_JOIN_LEAVE,
   308  		POST_AUTO_RESPONDER,
   309  		POST_ADD_REMOVE,
   310  		POST_JOIN_CHANNEL,
   311  		POST_GUEST_JOIN_CHANNEL,
   312  		POST_LEAVE_CHANNEL,
   313  		POST_JOIN_TEAM,
   314  		POST_LEAVE_TEAM,
   315  		POST_ADD_TO_CHANNEL,
   316  		POST_ADD_GUEST_TO_CHANNEL,
   317  		POST_REMOVE_FROM_CHANNEL,
   318  		POST_MOVE_CHANNEL,
   319  		POST_ADD_TO_TEAM,
   320  		POST_REMOVE_FROM_TEAM,
   321  		POST_SLACK_ATTACHMENT,
   322  		POST_HEADER_CHANGE,
   323  		POST_PURPOSE_CHANGE,
   324  		POST_DISPLAYNAME_CHANGE,
   325  		POST_CONVERT_CHANNEL,
   326  		POST_CHANNEL_DELETED,
   327  		POST_CHANNEL_RESTORED,
   328  		POST_CHANGE_CHANNEL_PRIVACY,
   329  		POST_ME,
   330  		POST_ADD_BOT_TEAMS_CHANNELS,
   331  		POST_SYSTEM_WARN_METRIC_STATUS:
   332  	default:
   333  		if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
   334  			return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
   335  		}
   336  	}
   337  
   338  	if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES {
   339  		return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   340  	}
   341  
   342  	if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES {
   343  		return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   344  	}
   345  
   346  	if utf8.RuneCountInString(StringInterfaceToJson(o.GetProps())) > POST_PROPS_MAX_RUNES {
   347  		return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   348  	}
   349  
   350  	return nil
   351  }
   352  
   353  func (o *Post) SanitizeProps() {
   354  	membersToSanitize := []string{
   355  		PROPS_ADD_CHANNEL_MEMBER,
   356  	}
   357  
   358  	for _, member := range membersToSanitize {
   359  		if _, ok := o.GetProps()[member]; ok {
   360  			o.DelProp(member)
   361  		}
   362  	}
   363  	for _, p := range o.Participants {
   364  		p.Sanitize(map[string]bool{})
   365  	}
   366  }
   367  
   368  func (o *Post) PreSave() {
   369  	if o.Id == "" {
   370  		o.Id = NewId()
   371  	}
   372  
   373  	o.OriginalId = ""
   374  
   375  	if o.CreateAt == 0 {
   376  		o.CreateAt = GetMillis()
   377  	}
   378  
   379  	o.UpdateAt = o.CreateAt
   380  	o.PreCommit()
   381  }
   382  
   383  func (o *Post) PreCommit() {
   384  	if o.GetProps() == nil {
   385  		o.SetProps(make(map[string]interface{}))
   386  	}
   387  
   388  	if o.Filenames == nil {
   389  		o.Filenames = []string{}
   390  	}
   391  
   392  	if o.FileIds == nil {
   393  		o.FileIds = []string{}
   394  	}
   395  
   396  	o.GenerateActionIds()
   397  
   398  	// There's a rare bug where the client sends up duplicate FileIds so protect against that
   399  	o.FileIds = RemoveDuplicateStrings(o.FileIds)
   400  }
   401  
   402  func (o *Post) MakeNonNil() {
   403  	if o.GetProps() == nil {
   404  		o.SetProps(make(map[string]interface{}))
   405  	}
   406  }
   407  
   408  func (o *Post) DelProp(key string) {
   409  	o.propsMu.Lock()
   410  	defer o.propsMu.Unlock()
   411  	propsCopy := make(map[string]interface{}, len(o.Props)-1)
   412  	for k, v := range o.Props {
   413  		propsCopy[k] = v
   414  	}
   415  	delete(propsCopy, key)
   416  	o.Props = propsCopy
   417  }
   418  
   419  func (o *Post) AddProp(key string, value interface{}) {
   420  	o.propsMu.Lock()
   421  	defer o.propsMu.Unlock()
   422  	propsCopy := make(map[string]interface{}, len(o.Props)+1)
   423  	for k, v := range o.Props {
   424  		propsCopy[k] = v
   425  	}
   426  	propsCopy[key] = value
   427  	o.Props = propsCopy
   428  }
   429  
   430  func (o *Post) GetProps() StringInterface {
   431  	o.propsMu.RLock()
   432  	defer o.propsMu.RUnlock()
   433  	return o.Props
   434  }
   435  
   436  func (o *Post) SetProps(props StringInterface) {
   437  	o.propsMu.Lock()
   438  	defer o.propsMu.Unlock()
   439  	o.Props = props
   440  }
   441  
   442  func (o *Post) GetProp(key string) interface{} {
   443  	o.propsMu.RLock()
   444  	defer o.propsMu.RUnlock()
   445  	return o.Props[key]
   446  }
   447  
   448  func (o *Post) IsSystemMessage() bool {
   449  	return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
   450  }
   451  
   452  func (o *Post) IsJoinLeaveMessage() bool {
   453  	return o.Type == POST_JOIN_LEAVE ||
   454  		o.Type == POST_ADD_REMOVE ||
   455  		o.Type == POST_JOIN_CHANNEL ||
   456  		o.Type == POST_LEAVE_CHANNEL ||
   457  		o.Type == POST_JOIN_TEAM ||
   458  		o.Type == POST_LEAVE_TEAM ||
   459  		o.Type == POST_ADD_TO_CHANNEL ||
   460  		o.Type == POST_REMOVE_FROM_CHANNEL ||
   461  		o.Type == POST_ADD_TO_TEAM ||
   462  		o.Type == POST_REMOVE_FROM_TEAM
   463  }
   464  
   465  func (o *Post) Patch(patch *PostPatch) {
   466  	if patch.IsPinned != nil {
   467  		o.IsPinned = *patch.IsPinned
   468  	}
   469  
   470  	if patch.Message != nil {
   471  		o.Message = *patch.Message
   472  	}
   473  
   474  	if patch.Props != nil {
   475  		newProps := *patch.Props
   476  		o.SetProps(newProps)
   477  	}
   478  
   479  	if patch.FileIds != nil {
   480  		o.FileIds = *patch.FileIds
   481  	}
   482  
   483  	if patch.HasReactions != nil {
   484  		o.HasReactions = *patch.HasReactions
   485  	}
   486  }
   487  
   488  func (o *PostPatch) ToJson() string {
   489  	b, err := json.Marshal(o)
   490  	if err != nil {
   491  		return ""
   492  	}
   493  
   494  	return string(b)
   495  }
   496  
   497  func PostPatchFromJson(data io.Reader) *PostPatch {
   498  	decoder := json.NewDecoder(data)
   499  	var post PostPatch
   500  	err := decoder.Decode(&post)
   501  	if err != nil {
   502  		return nil
   503  	}
   504  
   505  	return &post
   506  }
   507  
   508  func (o *SearchParameter) SearchParameterToJson() string {
   509  	b, err := json.Marshal(o)
   510  	if err != nil {
   511  		return ""
   512  	}
   513  
   514  	return string(b)
   515  }
   516  
   517  func SearchParameterFromJson(data io.Reader) (*SearchParameter, error) {
   518  	decoder := json.NewDecoder(data)
   519  	var searchParam SearchParameter
   520  	if err := decoder.Decode(&searchParam); err != nil {
   521  		return nil, err
   522  	}
   523  
   524  	return &searchParam, nil
   525  }
   526  
   527  func (o *Post) ChannelMentions() []string {
   528  	return ChannelMentions(o.Message)
   529  }
   530  
   531  // DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message.
   532  func (o *Post) DisableMentionHighlights() string {
   533  	mention, hasMentions := findAtChannelMention(o.Message)
   534  	if hasMentions {
   535  		o.AddProp(POST_PROPS_MENTION_HIGHLIGHT_DISABLED, true)
   536  	}
   537  	return mention
   538  }
   539  
   540  // DisableMentionHighlights disables mention highlighting for a post patch if required.
   541  func (o *PostPatch) DisableMentionHighlights() {
   542  	if o.Message == nil {
   543  		return
   544  	}
   545  	if _, hasMentions := findAtChannelMention(*o.Message); hasMentions {
   546  		if o.Props == nil {
   547  			o.Props = &StringInterface{}
   548  		}
   549  		(*o.Props)[POST_PROPS_MENTION_HIGHLIGHT_DISABLED] = true
   550  	}
   551  }
   552  
   553  func findAtChannelMention(message string) (mention string, found bool) {
   554  	re := regexp.MustCompile(`(?i)\B@(channel|all|here)\b`)
   555  	matched := re.FindStringSubmatch(message)
   556  	if found = (len(matched) > 0); found {
   557  		mention = strings.ToLower(matched[0])
   558  	}
   559  	return
   560  }
   561  
   562  func (o *Post) Attachments() []*SlackAttachment {
   563  	if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
   564  		return attachments
   565  	}
   566  	var ret []*SlackAttachment
   567  	if attachments, ok := o.GetProp("attachments").([]interface{}); ok {
   568  		for _, attachment := range attachments {
   569  			if enc, err := json.Marshal(attachment); err == nil {
   570  				var decoded SlackAttachment
   571  				if json.Unmarshal(enc, &decoded) == nil {
   572  					i := 0
   573  					for _, action := range decoded.Actions {
   574  						if action != nil {
   575  							decoded.Actions[i] = action
   576  							i++
   577  						}
   578  					}
   579  					decoded.Actions = decoded.Actions[:i]
   580  					ret = append(ret, &decoded)
   581  				}
   582  			}
   583  		}
   584  	}
   585  	return ret
   586  }
   587  
   588  func (o *Post) AttachmentsEqual(input *Post) bool {
   589  	attachments := o.Attachments()
   590  	inputAttachments := input.Attachments()
   591  
   592  	if len(attachments) != len(inputAttachments) {
   593  		return false
   594  	}
   595  
   596  	for i := range attachments {
   597  		if !attachments[i].Equals(inputAttachments[i]) {
   598  			return false
   599  		}
   600  	}
   601  
   602  	return true
   603  }
   604  
   605  var markdownDestinationEscaper = strings.NewReplacer(
   606  	`\`, `\\`,
   607  	`<`, `\<`,
   608  	`>`, `\>`,
   609  	`(`, `\(`,
   610  	`)`, `\)`,
   611  )
   612  
   613  // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
   614  // rewritten via RewriteImageURLs.
   615  func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
   616  	copy := o.Clone()
   617  	copy.Message = RewriteImageURLs(o.Message, f)
   618  	if copy.MessageSource == "" && copy.Message != o.Message {
   619  		copy.MessageSource = o.Message
   620  	}
   621  	return copy
   622  }
   623  
   624  func (o *PostEphemeral) ToUnsanitizedJson() string {
   625  	b, _ := json.Marshal(o)
   626  	return string(b)
   627  }
   628  
   629  // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
   630  // according to the function f. For each image URL, f will be invoked, and the resulting markdown
   631  // will contain the URL returned by that invocation instead.
   632  //
   633  // Image URLs are destination URLs used in inline images or reference definitions that are used
   634  // anywhere in the input markdown as an image.
   635  func RewriteImageURLs(message string, f func(string) string) string {
   636  	if !strings.Contains(message, "![") {
   637  		return message
   638  	}
   639  
   640  	var ranges []markdown.Range
   641  
   642  	markdown.Inspect(message, func(blockOrInline interface{}) bool {
   643  		switch v := blockOrInline.(type) {
   644  		case *markdown.ReferenceImage:
   645  			ranges = append(ranges, v.ReferenceDefinition.RawDestination)
   646  		case *markdown.InlineImage:
   647  			ranges = append(ranges, v.RawDestination)
   648  		default:
   649  			return true
   650  		}
   651  		return true
   652  	})
   653  
   654  	if ranges == nil {
   655  		return message
   656  	}
   657  
   658  	sort.Slice(ranges, func(i, j int) bool {
   659  		return ranges[i].Position < ranges[j].Position
   660  	})
   661  
   662  	copyRanges := make([]markdown.Range, 0, len(ranges))
   663  	urls := make([]string, 0, len(ranges))
   664  	resultLength := len(message)
   665  
   666  	start := 0
   667  	for i, r := range ranges {
   668  		switch {
   669  		case i == 0:
   670  		case r.Position != ranges[i-1].Position:
   671  			start = ranges[i-1].End
   672  		default:
   673  			continue
   674  		}
   675  		original := message[r.Position:r.End]
   676  		replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
   677  		resultLength += len(replacement) - len(original)
   678  		copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
   679  		urls = append(urls, replacement)
   680  	}
   681  
   682  	result := make([]byte, resultLength)
   683  
   684  	offset := 0
   685  	for i, r := range copyRanges {
   686  		offset += copy(result[offset:], message[r.Position:r.End])
   687  		offset += copy(result[offset:], urls[i])
   688  	}
   689  	copy(result[offset:], message[ranges[len(ranges)-1].End:])
   690  
   691  	return string(result)
   692  }
   693  
   694  func (o *Post) IsFromOAuthBot() bool {
   695  	props := o.GetProps()
   696  	return props["from_webhook"] == "true" && props["override_username"] != ""
   697  }