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 © 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 }