github.com/masterhung0112/hk_server/v5@v5.0.0-20220302090640-ec71aef15e1c/model/post.go (about)

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