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