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