github.com/vnforks/kid/v5@v5.22.1-0.20200408055009-b89d99c65676/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/vnforks/kid/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_CLASS or POST_LEAVE_CLASS instead
    26  	POST_JOIN_CLASS               = "system_join_class"
    27  	POST_GUEST_JOIN_CLASS         = "system_guest_join_class"
    28  	POST_LEAVE_CLASS              = "system_leave_class"
    29  	POST_JOIN_BRANCH              = "system_join_branch"
    30  	POST_LEAVE_BRANCH             = "system_leave_branch"
    31  	POST_AUTO_RESPONDER           = "system_auto_responder"
    32  	POST_ADD_REMOVE               = "system_add_remove" // Deprecated, use POST_ADD_TO_CLASS or POST_REMOVE_FROM_CLASS instead
    33  	POST_ADD_TO_CLASS             = "system_add_to_class"
    34  	POST_ADD_GUEST_TO_CLASS       = "system_add_guest_to_chan"
    35  	POST_REMOVE_FROM_CLASS        = "system_remove_from_class"
    36  	POST_MOVE_CLASS               = "system_move_class"
    37  	POST_ADD_TO_BRANCH            = "system_add_to_branch"
    38  	POST_REMOVE_FROM_BRANCH       = "system_remove_from_branch"
    39  	POST_HEADER_CHANGE            = "system_header_change"
    40  	POST_DISPLAYNAME_CHANGE       = "system_displayname_change"
    41  	POST_CONVERT_CLASS            = "system_convert_class"
    42  	POST_PURPOSE_CHANGE           = "system_purpose_change"
    43  	POST_CLASS_DELETED            = "system_class_deleted"
    44  	POST_CLASS_RESTORED           = "system_class_restored"
    45  	POST_EPHEMERAL                = "system_ephemeral"
    46  	POST_CHANGE_CLASS_PRIVACY     = "system_change_chan_privacy"
    47  	POST_ADD_BOT_BRANCHES_CLASSES = "add_bot_branches_classes"
    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_CLASS_MEMBER        = "add_class_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  )
    67  
    68  type Post struct {
    69  	Id       string `json:"id"`
    70  	CreateAt int64  `json:"create_at"`
    71  	UpdateAt int64  `json:"update_at"`
    72  	EditAt   int64  `json:"edit_at"`
    73  	DeleteAt int64  `json:"delete_at"`
    74  	UserId   string `json:"user_id"`
    75  	ClassId  string `json:"class_id"`
    76  
    77  	Message string `json:"message"`
    78  
    79  	Type          string          `json:"type"`
    80  	propsMu       sync.RWMutex    `db:"-"`       // Unexported mutex used to guard Post.Props.
    81  	Props         StringInterface `json:"props"` // Deprecated: use GetProps()
    82  	Hashtags      string          `json:"hashtags"`
    83  	Filenames     StringArray     `json:"filenames,omitempty"` // Deprecated, do not use this field any more
    84  	FileIds       StringArray     `json:"file_ids,omitempty"`
    85  	PendingPostId string          `json:"pending_post_id" db:"-"`
    86  	HasReactions  bool            `json:"has_reactions,omitempty"`
    87  
    88  	// Transient data populated before sending a post to the client
    89  	ReplyCount int64         `json:"reply_count" db:"-"`
    90  	Metadata   *PostMetadata `json:"metadata,omitempty" db:"-"`
    91  }
    92  
    93  type PostEphemeral struct {
    94  	UserID string `json:"user_id"`
    95  	Post   *Post  `json:"post"`
    96  }
    97  
    98  type PostPatch struct {
    99  	Message      *string          `json:"message"`
   100  	Props        *StringInterface `json:"props"`
   101  	FileIds      *StringArray     `json:"file_ids"`
   102  	HasReactions *bool            `json:"has_reactions"`
   103  }
   104  
   105  type SearchParameter struct {
   106  	Terms                 *string `json:"terms"`
   107  	IsOrSearch            *bool   `json:"is_or_search"`
   108  	TimeZoneOffset        *int    `json:"time_zone_offset"`
   109  	Page                  *int    `json:"page"`
   110  	PerPage               *int    `json:"per_page"`
   111  	IncludeDeletedClasses *bool   `json:"include_deleted_classes"`
   112  }
   113  
   114  type AnalyticsPostCountsOptions struct {
   115  	BranchId      string
   116  	YesterdayOnly bool
   117  }
   118  
   119  func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
   120  	copy := *o
   121  	if copy.Message != nil {
   122  		*copy.Message = RewriteImageURLs(*o.Message, f)
   123  	}
   124  	return &copy
   125  }
   126  
   127  type PostForExport struct {
   128  	Post
   129  	BranchName string
   130  	ClassName  string
   131  	Username   string
   132  	ReplyCount int
   133  }
   134  
   135  type DirectPostForExport struct {
   136  	Post
   137  	User         string
   138  	ClassMembers *[]string
   139  }
   140  
   141  type ReplyForExport struct {
   142  	Post
   143  	Username string
   144  }
   145  
   146  type PostForIndexing struct {
   147  	Post
   148  	BranchId       string `json:"branch_id"`
   149  	ParentCreateAt *int64 `json:"parent_create_at"`
   150  }
   151  
   152  // ShallowCopy is an utility function to shallow copy a Post to the given
   153  // destination without touching the internal RWMutex.
   154  func (o *Post) ShallowCopy(dst *Post) error {
   155  	if dst == nil {
   156  		return errors.New("dst cannot be nil")
   157  	}
   158  	o.propsMu.RLock()
   159  	defer o.propsMu.RUnlock()
   160  	dst.propsMu.Lock()
   161  	defer dst.propsMu.Unlock()
   162  	dst.Id = o.Id
   163  	dst.CreateAt = o.CreateAt
   164  	dst.UpdateAt = o.UpdateAt
   165  	dst.EditAt = o.EditAt
   166  	dst.DeleteAt = o.DeleteAt
   167  	dst.UserId = o.UserId
   168  	dst.ClassId = o.ClassId
   169  	dst.Message = o.Message
   170  	dst.Type = o.Type
   171  	dst.Props = o.Props
   172  	dst.Hashtags = o.Hashtags
   173  	dst.Filenames = o.Filenames
   174  	dst.FileIds = o.FileIds
   175  	dst.PendingPostId = o.PendingPostId
   176  	dst.HasReactions = o.HasReactions
   177  	dst.ReplyCount = o.ReplyCount
   178  	dst.Metadata = o.Metadata
   179  	return nil
   180  }
   181  
   182  // Clone shallowly copies the post and returns the copy.
   183  func (o *Post) Clone() *Post {
   184  	copy := &Post{}
   185  	o.ShallowCopy(copy)
   186  	return copy
   187  }
   188  
   189  func (o *Post) ToJson() string {
   190  	copy := o.Clone()
   191  	b, _ := json.Marshal(copy)
   192  	return string(b)
   193  }
   194  
   195  func (o *Post) ToUnsanitizedJson() string {
   196  	b, _ := json.Marshal(o)
   197  	return string(b)
   198  }
   199  
   200  type GetPostsSinceOptions struct {
   201  	ClassId          string
   202  	Time             int64
   203  	SkipFetchThreads bool
   204  }
   205  
   206  type GetPostsOptions struct {
   207  	ClassId          string
   208  	PostId           string
   209  	Page             int
   210  	PerPage          int
   211  	SkipFetchThreads bool
   212  }
   213  
   214  func PostFromJson(data io.Reader) *Post {
   215  	var o *Post
   216  	json.NewDecoder(data).Decode(&o)
   217  	return o
   218  }
   219  
   220  func (o *Post) Etag() string {
   221  	return Etag(o.Id, o.UpdateAt)
   222  }
   223  
   224  func (o *Post) IsValid(maxPostSize int) *AppError {
   225  	if len(o.Id) != 26 {
   226  		return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
   227  	}
   228  
   229  	if o.CreateAt == 0 {
   230  		return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   231  	}
   232  
   233  	if o.UpdateAt == 0 {
   234  		return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   235  	}
   236  
   237  	if len(o.UserId) != 26 {
   238  		return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
   239  	}
   240  
   241  	if len(o.ClassId) != 26 {
   242  		return NewAppError("Post.IsValid", "model.post.is_valid.class_id.app_error", nil, "", http.StatusBadRequest)
   243  	}
   244  
   245  	if utf8.RuneCountInString(o.Message) > maxPostSize {
   246  		return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   247  	}
   248  
   249  	if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES {
   250  		return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   251  	}
   252  
   253  	switch o.Type {
   254  	case
   255  		POST_DEFAULT,
   256  		POST_SYSTEM_GENERIC,
   257  		POST_JOIN_LEAVE,
   258  		POST_AUTO_RESPONDER,
   259  		POST_ADD_REMOVE,
   260  		POST_JOIN_CLASS,
   261  		POST_GUEST_JOIN_CLASS,
   262  		POST_LEAVE_CLASS,
   263  		POST_JOIN_BRANCH,
   264  		POST_LEAVE_BRANCH,
   265  		POST_ADD_TO_CLASS,
   266  		POST_ADD_GUEST_TO_CLASS,
   267  		POST_REMOVE_FROM_CLASS,
   268  		POST_MOVE_CLASS,
   269  		POST_ADD_TO_BRANCH,
   270  		POST_REMOVE_FROM_BRANCH,
   271  		POST_SLACK_ATTACHMENT,
   272  		POST_HEADER_CHANGE,
   273  		POST_PURPOSE_CHANGE,
   274  		POST_DISPLAYNAME_CHANGE,
   275  		POST_CONVERT_CLASS,
   276  		POST_CLASS_DELETED,
   277  		POST_CLASS_RESTORED,
   278  		POST_CHANGE_CLASS_PRIVACY,
   279  		POST_ME,
   280  		POST_ADD_BOT_BRANCHES_CLASSES:
   281  	default:
   282  		if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) {
   283  			return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
   284  		}
   285  	}
   286  
   287  	if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES {
   288  		return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   289  	}
   290  
   291  	if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES {
   292  		return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   293  	}
   294  
   295  	if utf8.RuneCountInString(StringInterfaceToJson(o.GetProps())) > POST_PROPS_MAX_RUNES {
   296  		return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
   297  	}
   298  
   299  	return nil
   300  }
   301  
   302  func (o *Post) SanitizeProps() {
   303  	membersToSanitize := []string{
   304  		PROPS_ADD_CLASS_MEMBER,
   305  	}
   306  
   307  	for _, member := range membersToSanitize {
   308  		if _, ok := o.GetProps()[member]; ok {
   309  			o.DelProp(member)
   310  		}
   311  	}
   312  }
   313  
   314  func (o *Post) PreSave() {
   315  	if o.Id == "" {
   316  		o.Id = NewId()
   317  	}
   318  
   319  	if o.CreateAt == 0 {
   320  		o.CreateAt = GetMillis()
   321  	}
   322  
   323  	o.UpdateAt = o.CreateAt
   324  	o.PreCommit()
   325  }
   326  
   327  func (o *Post) PreCommit() {
   328  	if o.GetProps() == nil {
   329  		o.SetProps(make(map[string]interface{}))
   330  	}
   331  
   332  	if o.Filenames == nil {
   333  		o.Filenames = []string{}
   334  	}
   335  
   336  	if o.FileIds == nil {
   337  		o.FileIds = []string{}
   338  	}
   339  
   340  	// There's a rare bug where the client sends up duplicate FileIds so protect against that
   341  	o.FileIds = RemoveDuplicateStrings(o.FileIds)
   342  }
   343  
   344  func (o *Post) MakeNonNil() {
   345  	if o.GetProps() == nil {
   346  		o.SetProps(make(map[string]interface{}))
   347  	}
   348  }
   349  
   350  func (o *Post) DelProp(key string) {
   351  	o.propsMu.Lock()
   352  	defer o.propsMu.Unlock()
   353  	propsCopy := make(map[string]interface{}, len(o.Props)-1)
   354  	for k, v := range o.Props {
   355  		propsCopy[k] = v
   356  	}
   357  	delete(propsCopy, key)
   358  	o.Props = propsCopy
   359  }
   360  
   361  func (o *Post) AddProp(key string, value interface{}) {
   362  	o.propsMu.Lock()
   363  	defer o.propsMu.Unlock()
   364  	propsCopy := make(map[string]interface{}, len(o.Props)+1)
   365  	for k, v := range o.Props {
   366  		propsCopy[k] = v
   367  	}
   368  	propsCopy[key] = value
   369  	o.Props = propsCopy
   370  }
   371  
   372  func (o *Post) GetProps() StringInterface {
   373  	o.propsMu.RLock()
   374  	defer o.propsMu.RUnlock()
   375  	return o.Props
   376  }
   377  
   378  func (o *Post) SetProps(props StringInterface) {
   379  	o.propsMu.Lock()
   380  	defer o.propsMu.Unlock()
   381  	o.Props = props
   382  }
   383  
   384  func (o *Post) GetProp(key string) interface{} {
   385  	o.propsMu.RLock()
   386  	defer o.propsMu.RUnlock()
   387  	return o.Props[key]
   388  }
   389  
   390  func (o *Post) IsSystemMessage() bool {
   391  	return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX
   392  }
   393  
   394  func (o *Post) IsJoinLeaveMessage() bool {
   395  	return o.Type == POST_JOIN_LEAVE ||
   396  		o.Type == POST_ADD_REMOVE ||
   397  		o.Type == POST_JOIN_CLASS ||
   398  		o.Type == POST_LEAVE_CLASS ||
   399  		o.Type == POST_JOIN_BRANCH ||
   400  		o.Type == POST_LEAVE_BRANCH ||
   401  		o.Type == POST_ADD_TO_CLASS ||
   402  		o.Type == POST_REMOVE_FROM_CLASS ||
   403  		o.Type == POST_ADD_TO_BRANCH ||
   404  		o.Type == POST_REMOVE_FROM_BRANCH
   405  }
   406  
   407  func (o *Post) Patch(patch *PostPatch) {
   408  
   409  	if patch.Message != nil {
   410  		o.Message = *patch.Message
   411  	}
   412  
   413  	if patch.Props != nil {
   414  		newProps := *patch.Props
   415  		o.SetProps(newProps)
   416  	}
   417  
   418  	if patch.FileIds != nil {
   419  		o.FileIds = *patch.FileIds
   420  	}
   421  
   422  	if patch.HasReactions != nil {
   423  		o.HasReactions = *patch.HasReactions
   424  	}
   425  }
   426  
   427  func (o *PostPatch) ToJson() string {
   428  	b, err := json.Marshal(o)
   429  	if err != nil {
   430  		return ""
   431  	}
   432  
   433  	return string(b)
   434  }
   435  
   436  func PostPatchFromJson(data io.Reader) *PostPatch {
   437  	decoder := json.NewDecoder(data)
   438  	var post PostPatch
   439  	err := decoder.Decode(&post)
   440  	if err != nil {
   441  		return nil
   442  	}
   443  
   444  	return &post
   445  }
   446  
   447  func (o *SearchParameter) SearchParameterToJson() string {
   448  	b, err := json.Marshal(o)
   449  	if err != nil {
   450  		return ""
   451  	}
   452  
   453  	return string(b)
   454  }
   455  
   456  func SearchParameterFromJson(data io.Reader) *SearchParameter {
   457  	decoder := json.NewDecoder(data)
   458  	var searchParam SearchParameter
   459  	err := decoder.Decode(&searchParam)
   460  	if err != nil {
   461  		return nil
   462  	}
   463  
   464  	return &searchParam
   465  }
   466  
   467  // DisableMentionHighlights disables a posts mention highlighting and returns the first class mention that was present in the message.
   468  func (o *Post) DisableMentionHighlights() string {
   469  	mention, hasMentions := findAtClassMention(o.Message)
   470  	if hasMentions {
   471  		o.AddProp(POST_PROPS_MENTION_HIGHLIGHT_DISABLED, true)
   472  	}
   473  	return mention
   474  }
   475  
   476  // DisableMentionHighlights disables mention highlighting for a post patch if required.
   477  func (o *PostPatch) DisableMentionHighlights() {
   478  	if _, hasMentions := findAtClassMention(*o.Message); hasMentions {
   479  		if o.Props == nil {
   480  			o.Props = &StringInterface{}
   481  		}
   482  		(*o.Props)[POST_PROPS_MENTION_HIGHLIGHT_DISABLED] = true
   483  	}
   484  }
   485  
   486  func findAtClassMention(message string) (mention string, found bool) {
   487  	re := regexp.MustCompile(`(?i)\B@(class|all|here)\b`)
   488  	matched := re.FindStringSubmatch(message)
   489  	if found = (len(matched) > 0); found {
   490  		mention = strings.ToLower(matched[0])
   491  	}
   492  	return
   493  }
   494  
   495  func (o *Post) AttachmentsEqual(input *Post) bool {
   496  	attachments := o.Attachments()
   497  	inputAttachments := input.Attachments()
   498  
   499  	if len(attachments) != len(inputAttachments) {
   500  		return false
   501  	}
   502  
   503  	for i := range attachments {
   504  		if !attachments[i].Equals(inputAttachments[i]) {
   505  			return false
   506  		}
   507  	}
   508  
   509  	return true
   510  }
   511  
   512  func (o *Post) Attachments() []*SlackAttachment {
   513  	if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
   514  		return attachments
   515  	}
   516  	var ret []*SlackAttachment
   517  	if attachments, ok := o.GetProp("attachments").([]interface{}); ok {
   518  		for _, attachment := range attachments {
   519  			if enc, err := json.Marshal(attachment); err == nil {
   520  				var decoded SlackAttachment
   521  				if json.Unmarshal(enc, &decoded) == nil {
   522  					ret = append(ret, &decoded)
   523  				}
   524  			}
   525  		}
   526  	}
   527  	return ret
   528  }
   529  
   530  var markdownDestinationEscaper = strings.NewReplacer(
   531  	`\`, `\\`,
   532  	`<`, `\<`,
   533  	`>`, `\>`,
   534  	`(`, `\(`,
   535  	`)`, `\)`,
   536  )
   537  
   538  // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
   539  // rewritten via RewriteImageURLs.
   540  func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
   541  	copy := o.Clone()
   542  	copy.Message = RewriteImageURLs(o.Message, f)
   543  	return copy
   544  }
   545  
   546  func (o *PostEphemeral) ToUnsanitizedJson() string {
   547  	b, _ := json.Marshal(o)
   548  	return string(b)
   549  }
   550  
   551  // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
   552  // according to the function f. For each image URL, f will be invoked, and the resulting markdown
   553  // will contain the URL returned by that invocation instead.
   554  //
   555  // Image URLs are destination URLs used in inline images or reference definitions that are used
   556  // anywhere in the input markdown as an image.
   557  func RewriteImageURLs(message string, f func(string) string) string {
   558  	if !strings.Contains(message, "![") {
   559  		return message
   560  	}
   561  
   562  	var ranges []markdown.Range
   563  
   564  	markdown.Inspect(message, func(blockOrInline interface{}) bool {
   565  		switch v := blockOrInline.(type) {
   566  		case *markdown.ReferenceImage:
   567  			ranges = append(ranges, v.ReferenceDefinition.RawDestination)
   568  		case *markdown.InlineImage:
   569  			ranges = append(ranges, v.RawDestination)
   570  		default:
   571  			return true
   572  		}
   573  		return true
   574  	})
   575  
   576  	if ranges == nil {
   577  		return message
   578  	}
   579  
   580  	sort.Slice(ranges, func(i, j int) bool {
   581  		return ranges[i].Position < ranges[j].Position
   582  	})
   583  
   584  	copyRanges := make([]markdown.Range, 0, len(ranges))
   585  	urls := make([]string, 0, len(ranges))
   586  	resultLength := len(message)
   587  
   588  	start := 0
   589  	for i, r := range ranges {
   590  		switch {
   591  		case i == 0:
   592  		case r.Position != ranges[i-1].Position:
   593  			start = ranges[i-1].End
   594  		default:
   595  			continue
   596  		}
   597  		original := message[r.Position:r.End]
   598  		replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
   599  		resultLength += len(replacement) - len(original)
   600  		copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
   601  		urls = append(urls, replacement)
   602  	}
   603  
   604  	result := make([]byte, resultLength)
   605  
   606  	offset := 0
   607  	for i, r := range copyRanges {
   608  		offset += copy(result[offset:], message[r.Position:r.End])
   609  		offset += copy(result[offset:], urls[i])
   610  	}
   611  	copy(result[offset:], message[ranges[len(ranges)-1].End:])
   612  
   613  	return string(result)
   614  }