github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/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  	Metadata   *PostMetadata `json:"metadata,omitempty" db:"-"`
   103  }
   104  
   105  type PostEphemeral struct {
   106  	UserID string `json:"user_id"`
   107  	Post   *Post  `json:"post"`
   108  }
   109  
   110  type PostPatch struct {
   111  	IsPinned     *bool            `json:"is_pinned"`
   112  	Message      *string          `json:"message"`
   113  	Props        *StringInterface `json:"props"`
   114  	FileIds      *StringArray     `json:"file_ids"`
   115  	HasReactions *bool            `json:"has_reactions"`
   116  }
   117  
   118  type SearchParameter struct {
   119  	Terms                  *string `json:"terms"`
   120  	IsOrSearch             *bool   `json:"is_or_search"`
   121  	TimeZoneOffset         *int    `json:"time_zone_offset"`
   122  	Page                   *int    `json:"page"`
   123  	PerPage                *int    `json:"per_page"`
   124  	IncludeDeletedChannels *bool   `json:"include_deleted_channels"`
   125  }
   126  
   127  type AnalyticsPostCountsOptions struct {
   128  	TeamId        string
   129  	BotsOnly      bool
   130  	YesterdayOnly bool
   131  }
   132  
   133  func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
   134  	copy := *o
   135  	if copy.Message != nil {
   136  		*copy.Message = RewriteImageURLs(*o.Message, f)
   137  	}
   138  	return &copy
   139  }
   140  
   141  type PostForExport struct {
   142  	Post
   143  	TeamName    string
   144  	ChannelName string
   145  	Username    string
   146  	ReplyCount  int
   147  }
   148  
   149  type DirectPostForExport struct {
   150  	Post
   151  	User           string
   152  	ChannelMembers *[]string
   153  }
   154  
   155  type ReplyForExport struct {
   156  	Post
   157  	Username string
   158  }
   159  
   160  type PostForIndexing struct {
   161  	Post
   162  	TeamId         string `json:"team_id"`
   163  	ParentCreateAt *int64 `json:"parent_create_at"`
   164  }
   165  
   166  // ShallowCopy is an utility function to shallow copy a Post to the given
   167  // destination without touching the internal RWMutex.
   168  func (o *Post) ShallowCopy(dst *Post) error {
   169  	if dst == nil {
   170  		return errors.New("dst cannot be nil")
   171  	}
   172  	o.propsMu.RLock()
   173  	defer o.propsMu.RUnlock()
   174  	dst.propsMu.Lock()
   175  	defer dst.propsMu.Unlock()
   176  	dst.Id = o.Id
   177  	dst.CreateAt = o.CreateAt
   178  	dst.UpdateAt = o.UpdateAt
   179  	dst.EditAt = o.EditAt
   180  	dst.DeleteAt = o.DeleteAt
   181  	dst.IsPinned = o.IsPinned
   182  	dst.UserId = o.UserId
   183  	dst.ChannelId = o.ChannelId
   184  	dst.RootId = o.RootId
   185  	dst.ParentId = o.ParentId
   186  	dst.OriginalId = o.OriginalId
   187  	dst.Message = o.Message
   188  	dst.MessageSource = o.MessageSource
   189  	dst.Type = o.Type
   190  	dst.Props = o.Props
   191  	dst.Hashtags = o.Hashtags
   192  	dst.Filenames = o.Filenames
   193  	dst.FileIds = o.FileIds
   194  	dst.PendingPostId = o.PendingPostId
   195  	dst.HasReactions = o.HasReactions
   196  	dst.ReplyCount = o.ReplyCount
   197  	dst.Metadata = o.Metadata
   198  	return nil
   199  }
   200  
   201  // Clone shallowly copies the post and returns the copy.
   202  func (o *Post) Clone() *Post {
   203  	copy := &Post{}
   204  	o.ShallowCopy(copy)
   205  	return copy
   206  }
   207  
   208  func (o *Post) ToJson() string {
   209  	copy := o.Clone()
   210  	copy.StripActionIntegrations()
   211  	b, _ := json.Marshal(copy)
   212  	return string(b)
   213  }
   214  
   215  func (o *Post) ToUnsanitizedJson() string {
   216  	b, _ := json.Marshal(o)
   217  	return string(b)
   218  }
   219  
   220  type GetPostsSinceOptions struct {
   221  	ChannelId        string
   222  	Time             int64
   223  	SkipFetchThreads bool
   224  }
   225  
   226  type GetPostsOptions struct {
   227  	ChannelId        string
   228  	PostId           string
   229  	Page             int
   230  	PerPage          int
   231  	SkipFetchThreads bool
   232  }
   233  
   234  func PostFromJson(data io.Reader) *Post {
   235  	var o *Post
   236  	json.NewDecoder(data).Decode(&o)
   237  	return o
   238  }
   239  
   240  func (o *Post) Etag() string {
   241  	return Etag(o.Id, o.UpdateAt)
   242  }
   243  
   244  func (o *Post) IsValid(maxPostSize int) *AppError {
   245  	if !IsValidId(o.Id) {
   246  		return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
   247  	}
   248  
   249  	if o.CreateAt == 0 {
   250  		return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   251  	}
   252  
   253  	if o.UpdateAt == 0 {
   254  		return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   255  	}
   256  
   257  	if !IsValidId(o.UserId) {
   258  		return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
   259  	}
   260  
   261  	if !IsValidId(o.ChannelId) {
   262  		return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
   263  	}
   264  
   265  	if !(IsValidId(o.RootId) || len(o.RootId) == 0) {
   266  		return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
   267  	}
   268  
   269  	if !(IsValidId(o.ParentId) || len(o.ParentId) == 0) {
   270  		return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest)
   271  	}
   272  
   273  	if len(o.ParentId) == 26 && len(o.RootId) == 0 {
   274  		return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest)
   275  	}
   276  
   277  	if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) {
   278  		return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
   279  	}
   280  
   281  	if utf8.RuneCountInString(o.Message) > maxPostSize {
   282  		return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   283  	}
   284  
   285  	if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES {
   286  		return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   287  	}
   288  
   289  	switch o.Type {
   290  	case
   291  		POST_DEFAULT,
   292  		POST_SYSTEM_GENERIC,
   293  		POST_JOIN_LEAVE,
   294  		POST_AUTO_RESPONDER,
   295  		POST_ADD_REMOVE,
   296  		POST_JOIN_CHANNEL,
   297  		POST_GUEST_JOIN_CHANNEL,
   298  		POST_LEAVE_CHANNEL,
   299  		POST_JOIN_TEAM,
   300  		POST_LEAVE_TEAM,
   301  		POST_ADD_TO_CHANNEL,
   302  		POST_ADD_GUEST_TO_CHANNEL,
   303  		POST_REMOVE_FROM_CHANNEL,
   304  		POST_MOVE_CHANNEL,
   305  		POST_ADD_TO_TEAM,
   306  		POST_REMOVE_FROM_TEAM,
   307  		POST_SLACK_ATTACHMENT,
   308  		POST_HEADER_CHANGE,
   309  		POST_PURPOSE_CHANGE,
   310  		POST_DISPLAYNAME_CHANGE,
   311  		POST_CONVERT_CHANNEL,
   312  		POST_CHANNEL_DELETED,
   313  		POST_CHANNEL_RESTORED,
   314  		POST_CHANGE_CHANNEL_PRIVACY,
   315  		POST_ME,
   316  		POST_ADD_BOT_TEAMS_CHANNELS,
   317  		POST_SYSTEM_WARN_METRIC_STATUS:
   318  	default:
   319  		if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
   320  			return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
   321  		}
   322  	}
   323  
   324  	if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES {
   325  		return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   326  	}
   327  
   328  	if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES {
   329  		return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   330  	}
   331  
   332  	if utf8.RuneCountInString(StringInterfaceToJson(o.GetProps())) > POST_PROPS_MAX_RUNES {
   333  		return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   334  	}
   335  
   336  	return nil
   337  }
   338  
   339  func (o *Post) SanitizeProps() {
   340  	membersToSanitize := []string{
   341  		PROPS_ADD_CHANNEL_MEMBER,
   342  	}
   343  
   344  	for _, member := range membersToSanitize {
   345  		if _, ok := o.GetProps()[member]; ok {
   346  			o.DelProp(member)
   347  		}
   348  	}
   349  }
   350  
   351  func (o *Post) PreSave() {
   352  	if o.Id == "" {
   353  		o.Id = NewId()
   354  	}
   355  
   356  	o.OriginalId = ""
   357  
   358  	if o.CreateAt == 0 {
   359  		o.CreateAt = GetMillis()
   360  	}
   361  
   362  	o.UpdateAt = o.CreateAt
   363  	o.PreCommit()
   364  }
   365  
   366  func (o *Post) PreCommit() {
   367  	if o.GetProps() == nil {
   368  		o.SetProps(make(map[string]interface{}))
   369  	}
   370  
   371  	if o.Filenames == nil {
   372  		o.Filenames = []string{}
   373  	}
   374  
   375  	if o.FileIds == nil {
   376  		o.FileIds = []string{}
   377  	}
   378  
   379  	o.GenerateActionIds()
   380  
   381  	// There's a rare bug where the client sends up duplicate FileIds so protect against that
   382  	o.FileIds = RemoveDuplicateStrings(o.FileIds)
   383  }
   384  
   385  func (o *Post) MakeNonNil() {
   386  	if o.GetProps() == nil {
   387  		o.SetProps(make(map[string]interface{}))
   388  	}
   389  }
   390  
   391  func (o *Post) DelProp(key string) {
   392  	o.propsMu.Lock()
   393  	defer o.propsMu.Unlock()
   394  	propsCopy := make(map[string]interface{}, len(o.Props)-1)
   395  	for k, v := range o.Props {
   396  		propsCopy[k] = v
   397  	}
   398  	delete(propsCopy, key)
   399  	o.Props = propsCopy
   400  }
   401  
   402  func (o *Post) AddProp(key string, value interface{}) {
   403  	o.propsMu.Lock()
   404  	defer o.propsMu.Unlock()
   405  	propsCopy := make(map[string]interface{}, len(o.Props)+1)
   406  	for k, v := range o.Props {
   407  		propsCopy[k] = v
   408  	}
   409  	propsCopy[key] = value
   410  	o.Props = propsCopy
   411  }
   412  
   413  func (o *Post) GetProps() StringInterface {
   414  	o.propsMu.RLock()
   415  	defer o.propsMu.RUnlock()
   416  	return o.Props
   417  }
   418  
   419  func (o *Post) SetProps(props StringInterface) {
   420  	o.propsMu.Lock()
   421  	defer o.propsMu.Unlock()
   422  	o.Props = props
   423  }
   424  
   425  func (o *Post) GetProp(key string) interface{} {
   426  	o.propsMu.RLock()
   427  	defer o.propsMu.RUnlock()
   428  	return o.Props[key]
   429  }
   430  
   431  func (o *Post) IsSystemMessage() bool {
   432  	return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
   433  }
   434  
   435  func (o *Post) IsJoinLeaveMessage() bool {
   436  	return o.Type == POST_JOIN_LEAVE ||
   437  		o.Type == POST_ADD_REMOVE ||
   438  		o.Type == POST_JOIN_CHANNEL ||
   439  		o.Type == POST_LEAVE_CHANNEL ||
   440  		o.Type == POST_JOIN_TEAM ||
   441  		o.Type == POST_LEAVE_TEAM ||
   442  		o.Type == POST_ADD_TO_CHANNEL ||
   443  		o.Type == POST_REMOVE_FROM_CHANNEL ||
   444  		o.Type == POST_ADD_TO_TEAM ||
   445  		o.Type == POST_REMOVE_FROM_TEAM
   446  }
   447  
   448  func (o *Post) Patch(patch *PostPatch) {
   449  	if patch.IsPinned != nil {
   450  		o.IsPinned = *patch.IsPinned
   451  	}
   452  
   453  	if patch.Message != nil {
   454  		o.Message = *patch.Message
   455  	}
   456  
   457  	if patch.Props != nil {
   458  		newProps := *patch.Props
   459  		o.SetProps(newProps)
   460  	}
   461  
   462  	if patch.FileIds != nil {
   463  		o.FileIds = *patch.FileIds
   464  	}
   465  
   466  	if patch.HasReactions != nil {
   467  		o.HasReactions = *patch.HasReactions
   468  	}
   469  }
   470  
   471  func (o *PostPatch) ToJson() string {
   472  	b, err := json.Marshal(o)
   473  	if err != nil {
   474  		return ""
   475  	}
   476  
   477  	return string(b)
   478  }
   479  
   480  func PostPatchFromJson(data io.Reader) *PostPatch {
   481  	decoder := json.NewDecoder(data)
   482  	var post PostPatch
   483  	err := decoder.Decode(&post)
   484  	if err != nil {
   485  		return nil
   486  	}
   487  
   488  	return &post
   489  }
   490  
   491  func (o *SearchParameter) SearchParameterToJson() string {
   492  	b, err := json.Marshal(o)
   493  	if err != nil {
   494  		return ""
   495  	}
   496  
   497  	return string(b)
   498  }
   499  
   500  func SearchParameterFromJson(data io.Reader) *SearchParameter {
   501  	decoder := json.NewDecoder(data)
   502  	var searchParam SearchParameter
   503  	err := decoder.Decode(&searchParam)
   504  	if err != nil {
   505  		return nil
   506  	}
   507  
   508  	return &searchParam
   509  }
   510  
   511  func (o *Post) ChannelMentions() []string {
   512  	return ChannelMentions(o.Message)
   513  }
   514  
   515  // DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message.
   516  func (o *Post) DisableMentionHighlights() string {
   517  	mention, hasMentions := findAtChannelMention(o.Message)
   518  	if hasMentions {
   519  		o.AddProp(POST_PROPS_MENTION_HIGHLIGHT_DISABLED, true)
   520  	}
   521  	return mention
   522  }
   523  
   524  // DisableMentionHighlights disables mention highlighting for a post patch if required.
   525  func (o *PostPatch) DisableMentionHighlights() {
   526  	if o.Message == nil {
   527  		return
   528  	}
   529  	if _, hasMentions := findAtChannelMention(*o.Message); hasMentions {
   530  		if o.Props == nil {
   531  			o.Props = &StringInterface{}
   532  		}
   533  		(*o.Props)[POST_PROPS_MENTION_HIGHLIGHT_DISABLED] = true
   534  	}
   535  }
   536  
   537  func findAtChannelMention(message string) (mention string, found bool) {
   538  	re := regexp.MustCompile(`(?i)\B@(channel|all|here)\b`)
   539  	matched := re.FindStringSubmatch(message)
   540  	if found = (len(matched) > 0); found {
   541  		mention = strings.ToLower(matched[0])
   542  	}
   543  	return
   544  }
   545  
   546  func (o *Post) Attachments() []*SlackAttachment {
   547  	if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
   548  		return attachments
   549  	}
   550  	var ret []*SlackAttachment
   551  	if attachments, ok := o.GetProp("attachments").([]interface{}); ok {
   552  		for _, attachment := range attachments {
   553  			if enc, err := json.Marshal(attachment); err == nil {
   554  				var decoded SlackAttachment
   555  				if json.Unmarshal(enc, &decoded) == nil {
   556  					ret = append(ret, &decoded)
   557  				}
   558  			}
   559  		}
   560  	}
   561  	return ret
   562  }
   563  
   564  func (o *Post) AttachmentsEqual(input *Post) bool {
   565  	attachments := o.Attachments()
   566  	inputAttachments := input.Attachments()
   567  
   568  	if len(attachments) != len(inputAttachments) {
   569  		return false
   570  	}
   571  
   572  	for i := range attachments {
   573  		if !attachments[i].Equals(inputAttachments[i]) {
   574  			return false
   575  		}
   576  	}
   577  
   578  	return true
   579  }
   580  
   581  var markdownDestinationEscaper = strings.NewReplacer(
   582  	`\`, `\\`,
   583  	`<`, `\<`,
   584  	`>`, `\>`,
   585  	`(`, `\(`,
   586  	`)`, `\)`,
   587  )
   588  
   589  // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
   590  // rewritten via RewriteImageURLs.
   591  func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
   592  	copy := o.Clone()
   593  	copy.Message = RewriteImageURLs(o.Message, f)
   594  	if copy.MessageSource == "" && copy.Message != o.Message {
   595  		copy.MessageSource = o.Message
   596  	}
   597  	return copy
   598  }
   599  
   600  func (o *PostEphemeral) ToUnsanitizedJson() string {
   601  	b, _ := json.Marshal(o)
   602  	return string(b)
   603  }
   604  
   605  // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
   606  // according to the function f. For each image URL, f will be invoked, and the resulting markdown
   607  // will contain the URL returned by that invocation instead.
   608  //
   609  // Image URLs are destination URLs used in inline images or reference definitions that are used
   610  // anywhere in the input markdown as an image.
   611  func RewriteImageURLs(message string, f func(string) string) string {
   612  	if !strings.Contains(message, "![") {
   613  		return message
   614  	}
   615  
   616  	var ranges []markdown.Range
   617  
   618  	markdown.Inspect(message, func(blockOrInline interface{}) bool {
   619  		switch v := blockOrInline.(type) {
   620  		case *markdown.ReferenceImage:
   621  			ranges = append(ranges, v.ReferenceDefinition.RawDestination)
   622  		case *markdown.InlineImage:
   623  			ranges = append(ranges, v.RawDestination)
   624  		default:
   625  			return true
   626  		}
   627  		return true
   628  	})
   629  
   630  	if ranges == nil {
   631  		return message
   632  	}
   633  
   634  	sort.Slice(ranges, func(i, j int) bool {
   635  		return ranges[i].Position < ranges[j].Position
   636  	})
   637  
   638  	copyRanges := make([]markdown.Range, 0, len(ranges))
   639  	urls := make([]string, 0, len(ranges))
   640  	resultLength := len(message)
   641  
   642  	start := 0
   643  	for i, r := range ranges {
   644  		switch {
   645  		case i == 0:
   646  		case r.Position != ranges[i-1].Position:
   647  			start = ranges[i-1].End
   648  		default:
   649  			continue
   650  		}
   651  		original := message[r.Position:r.End]
   652  		replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
   653  		resultLength += len(replacement) - len(original)
   654  		copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
   655  		urls = append(urls, replacement)
   656  	}
   657  
   658  	result := make([]byte, resultLength)
   659  
   660  	offset := 0
   661  	for i, r := range copyRanges {
   662  		offset += copy(result[offset:], message[r.Position:r.End])
   663  		offset += copy(result[offset:], urls[i])
   664  	}
   665  	copy(result[offset:], message[ranges[len(ranges)-1].End:])
   666  
   667  	return string(result)
   668  }