github.com/levb/mattermost-server@v5.3.1+incompatible/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 "io" 9 "net/http" 10 "sort" 11 "strings" 12 "unicode/utf8" 13 14 "github.com/mattermost/mattermost-server/utils/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_LEAVE_CHANNEL = "system_leave_channel" 25 POST_JOIN_TEAM = "system_join_team" 26 POST_LEAVE_TEAM = "system_leave_team" 27 POST_AUTO_RESPONDER = "system_auto_responder" 28 POST_ADD_REMOVE = "system_add_remove" // Deprecated, use POST_ADD_TO_CHANNEL or POST_REMOVE_FROM_CHANNEL instead 29 POST_ADD_TO_CHANNEL = "system_add_to_channel" 30 POST_REMOVE_FROM_CHANNEL = "system_remove_from_channel" 31 POST_MOVE_CHANNEL = "system_move_channel" 32 POST_ADD_TO_TEAM = "system_add_to_team" 33 POST_REMOVE_FROM_TEAM = "system_remove_from_team" 34 POST_HEADER_CHANGE = "system_header_change" 35 POST_DISPLAYNAME_CHANGE = "system_displayname_change" 36 POST_CONVERT_CHANNEL = "system_convert_channel" 37 POST_PURPOSE_CHANGE = "system_purpose_change" 38 POST_CHANNEL_DELETED = "system_channel_deleted" 39 POST_EPHEMERAL = "system_ephemeral" 40 POST_CHANGE_CHANNEL_PRIVACY = "system_change_chan_privacy" 41 POST_FILEIDS_MAX_RUNES = 150 42 POST_FILENAMES_MAX_RUNES = 4000 43 POST_HASHTAGS_MAX_RUNES = 1000 44 POST_MESSAGE_MAX_RUNES_V1 = 4000 45 POST_MESSAGE_MAX_BYTES_V2 = 65535 // Maximum size of a TEXT column in MySQL 46 POST_MESSAGE_MAX_RUNES_V2 = POST_MESSAGE_MAX_BYTES_V2 / 4 // Assume a worst-case representation 47 POST_PROPS_MAX_RUNES = 8000 48 POST_PROPS_MAX_USER_RUNES = POST_PROPS_MAX_RUNES - 400 // Leave some room for system / pre-save modifications 49 POST_CUSTOM_TYPE_PREFIX = "custom_" 50 PROPS_ADD_CHANNEL_MEMBER = "add_channel_member" 51 POST_PROPS_ADDED_USER_ID = "addedUserId" 52 POST_PROPS_DELETE_BY = "deleteBy" 53 POST_ACTION_TYPE_BUTTON = "button" 54 POST_ACTION_TYPE_SELECT = "select" 55 ) 56 57 type Post struct { 58 Id string `json:"id"` 59 CreateAt int64 `json:"create_at"` 60 UpdateAt int64 `json:"update_at"` 61 EditAt int64 `json:"edit_at"` 62 DeleteAt int64 `json:"delete_at"` 63 IsPinned bool `json:"is_pinned"` 64 UserId string `json:"user_id"` 65 ChannelId string `json:"channel_id"` 66 RootId string `json:"root_id"` 67 ParentId string `json:"parent_id"` 68 OriginalId string `json:"original_id"` 69 70 Message string `json:"message"` 71 72 // MessageSource will contain the message as submitted by the user if Message has been modified 73 // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to 74 // populate edit boxes if present. 75 MessageSource string `json:"message_source,omitempty" db:"-"` 76 77 Type string `json:"type"` 78 Props StringInterface `json:"props"` 79 Hashtags string `json:"hashtags"` 80 Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more 81 FileIds StringArray `json:"file_ids,omitempty"` 82 PendingPostId string `json:"pending_post_id" db:"-"` 83 HasReactions bool `json:"has_reactions,omitempty"` 84 } 85 86 type PostEphemeral struct { 87 UserID string `json:"user_id"` 88 Post *Post `json:"post"` 89 } 90 91 type PostPatch struct { 92 IsPinned *bool `json:"is_pinned"` 93 Message *string `json:"message"` 94 Props *StringInterface `json:"props"` 95 FileIds *StringArray `json:"file_ids"` 96 HasReactions *bool `json:"has_reactions"` 97 } 98 99 type SearchParameter struct { 100 Terms *string `json:"terms"` 101 IsOrSearch *bool `json:"is_or_search"` 102 TimeZoneOffset *int `json:"time_zone_offset"` 103 } 104 105 func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { 106 copy := *o 107 if copy.Message != nil { 108 *copy.Message = RewriteImageURLs(*o.Message, f) 109 } 110 return © 111 } 112 113 type PostForIndexing struct { 114 Post 115 TeamId string `json:"team_id"` 116 ParentCreateAt *int64 `json:"parent_create_at"` 117 } 118 119 type DoPostActionRequest struct { 120 SelectedOption string `json:"selected_option"` 121 } 122 123 type PostAction struct { 124 Id string `json:"id"` 125 Name string `json:"name"` 126 Type string `json:"type"` 127 DataSource string `json:"data_source"` 128 Options []*PostActionOptions `json:"options"` 129 Integration *PostActionIntegration `json:"integration,omitempty"` 130 } 131 132 type PostActionOptions struct { 133 Text string `json:"text"` 134 Value string `json:"value"` 135 } 136 137 type PostActionIntegration struct { 138 URL string `json:"url,omitempty"` 139 Context StringInterface `json:"context,omitempty"` 140 } 141 142 type PostActionIntegrationRequest struct { 143 UserId string `json:"user_id"` 144 PostId string `json:"post_id"` 145 Type string `json:"type"` 146 DataSource string `json:"data_source"` 147 Context StringInterface `json:"context,omitempty"` 148 } 149 150 type PostActionIntegrationResponse struct { 151 Update *Post `json:"update"` 152 EphemeralText string `json:"ephemeral_text"` 153 } 154 155 func (o *Post) ToJson() string { 156 copy := *o 157 copy.StripActionIntegrations() 158 b, _ := json.Marshal(©) 159 return string(b) 160 } 161 162 func (o *Post) ToUnsanitizedJson() string { 163 b, _ := json.Marshal(o) 164 return string(b) 165 } 166 167 func PostFromJson(data io.Reader) *Post { 168 var o *Post 169 json.NewDecoder(data).Decode(&o) 170 return o 171 } 172 173 func (o *Post) Etag() string { 174 return Etag(o.Id, o.UpdateAt) 175 } 176 177 func (o *Post) IsValid(maxPostSize int) *AppError { 178 179 if len(o.Id) != 26 { 180 return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) 181 } 182 183 if o.CreateAt == 0 { 184 return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 185 } 186 187 if o.UpdateAt == 0 { 188 return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 189 } 190 191 if len(o.UserId) != 26 { 192 return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) 193 } 194 195 if len(o.ChannelId) != 26 { 196 return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) 197 } 198 199 if !(len(o.RootId) == 26 || len(o.RootId) == 0) { 200 return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) 201 } 202 203 if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { 204 return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) 205 } 206 207 if len(o.ParentId) == 26 && len(o.RootId) == 0 { 208 return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) 209 } 210 211 if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { 212 return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) 213 } 214 215 if utf8.RuneCountInString(o.Message) > maxPostSize { 216 return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) 217 } 218 219 if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { 220 return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) 221 } 222 223 switch o.Type { 224 case 225 POST_DEFAULT, 226 POST_JOIN_LEAVE, 227 POST_AUTO_RESPONDER, 228 POST_ADD_REMOVE, 229 POST_JOIN_CHANNEL, 230 POST_LEAVE_CHANNEL, 231 POST_JOIN_TEAM, 232 POST_LEAVE_TEAM, 233 POST_ADD_TO_CHANNEL, 234 POST_REMOVE_FROM_CHANNEL, 235 POST_MOVE_CHANNEL, 236 POST_ADD_TO_TEAM, 237 POST_REMOVE_FROM_TEAM, 238 POST_SLACK_ATTACHMENT, 239 POST_HEADER_CHANGE, 240 POST_PURPOSE_CHANGE, 241 POST_DISPLAYNAME_CHANGE, 242 POST_CONVERT_CHANNEL, 243 POST_CHANNEL_DELETED, 244 POST_CHANGE_CHANNEL_PRIVACY: 245 default: 246 if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { 247 return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) 248 } 249 } 250 251 if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { 252 return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) 253 } 254 255 if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { 256 return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) 257 } 258 259 if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { 260 return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) 261 } 262 263 return nil 264 } 265 266 func (o *Post) SanitizeProps() { 267 membersToSanitize := []string{ 268 PROPS_ADD_CHANNEL_MEMBER, 269 } 270 271 for _, member := range membersToSanitize { 272 if _, ok := o.Props[member]; ok { 273 delete(o.Props, member) 274 } 275 } 276 } 277 278 func (o *Post) PreSave() { 279 if o.Id == "" { 280 o.Id = NewId() 281 } 282 283 o.OriginalId = "" 284 285 if o.CreateAt == 0 { 286 o.CreateAt = GetMillis() 287 } 288 289 o.UpdateAt = o.CreateAt 290 o.PreCommit() 291 } 292 293 func (o *Post) PreCommit() { 294 if o.Props == nil { 295 o.Props = make(map[string]interface{}) 296 } 297 298 if o.Filenames == nil { 299 o.Filenames = []string{} 300 } 301 302 if o.FileIds == nil { 303 o.FileIds = []string{} 304 } 305 306 o.GenerateActionIds() 307 } 308 309 func (o *Post) MakeNonNil() { 310 if o.Props == nil { 311 o.Props = make(map[string]interface{}) 312 } 313 } 314 315 func (o *Post) AddProp(key string, value interface{}) { 316 317 o.MakeNonNil() 318 319 o.Props[key] = value 320 } 321 322 func (o *Post) IsSystemMessage() bool { 323 return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX 324 } 325 326 func (p *Post) Patch(patch *PostPatch) { 327 if patch.IsPinned != nil { 328 p.IsPinned = *patch.IsPinned 329 } 330 331 if patch.Message != nil { 332 p.Message = *patch.Message 333 } 334 335 if patch.Props != nil { 336 p.Props = *patch.Props 337 } 338 339 if patch.FileIds != nil { 340 p.FileIds = *patch.FileIds 341 } 342 343 if patch.HasReactions != nil { 344 p.HasReactions = *patch.HasReactions 345 } 346 } 347 348 func (o *PostPatch) ToJson() string { 349 b, err := json.Marshal(o) 350 if err != nil { 351 return "" 352 } 353 354 return string(b) 355 } 356 357 func PostPatchFromJson(data io.Reader) *PostPatch { 358 decoder := json.NewDecoder(data) 359 var post PostPatch 360 err := decoder.Decode(&post) 361 if err != nil { 362 return nil 363 } 364 365 return &post 366 } 367 368 func (o *SearchParameter) SearchParameterToJson() string { 369 b, err := json.Marshal(o) 370 if err != nil { 371 return "" 372 } 373 374 return string(b) 375 } 376 377 func SearchParameterFromJson(data io.Reader) *SearchParameter { 378 decoder := json.NewDecoder(data) 379 var searchParam SearchParameter 380 err := decoder.Decode(&searchParam) 381 if err != nil { 382 return nil 383 } 384 385 return &searchParam 386 } 387 388 func (o *Post) ChannelMentions() []string { 389 return ChannelMentions(o.Message) 390 } 391 392 func (r *PostActionIntegrationRequest) ToJson() string { 393 b, _ := json.Marshal(r) 394 return string(b) 395 } 396 397 func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest { 398 var o *PostActionIntegrationRequest 399 err := json.NewDecoder(data).Decode(&o) 400 if err != nil { 401 return nil 402 } 403 return o 404 } 405 406 func (r *PostActionIntegrationResponse) ToJson() string { 407 b, _ := json.Marshal(r) 408 return string(b) 409 } 410 411 func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { 412 var o *PostActionIntegrationResponse 413 err := json.NewDecoder(data).Decode(&o) 414 if err != nil { 415 return nil 416 } 417 return o 418 } 419 420 func (o *Post) Attachments() []*SlackAttachment { 421 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 422 return attachments 423 } 424 var ret []*SlackAttachment 425 if attachments, ok := o.Props["attachments"].([]interface{}); ok { 426 for _, attachment := range attachments { 427 if enc, err := json.Marshal(attachment); err == nil { 428 var decoded SlackAttachment 429 if json.Unmarshal(enc, &decoded) == nil { 430 ret = append(ret, &decoded) 431 } 432 } 433 } 434 } 435 return ret 436 } 437 438 func (o *Post) StripActionIntegrations() { 439 attachments := o.Attachments() 440 if o.Props["attachments"] != nil { 441 o.Props["attachments"] = attachments 442 } 443 for _, attachment := range attachments { 444 for _, action := range attachment.Actions { 445 action.Integration = nil 446 } 447 } 448 } 449 450 func (o *Post) GetAction(id string) *PostAction { 451 for _, attachment := range o.Attachments() { 452 for _, action := range attachment.Actions { 453 if action.Id == id { 454 return action 455 } 456 } 457 } 458 return nil 459 } 460 461 func (o *Post) GenerateActionIds() { 462 if o.Props["attachments"] != nil { 463 o.Props["attachments"] = o.Attachments() 464 } 465 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 466 for _, attachment := range attachments { 467 for _, action := range attachment.Actions { 468 if action.Id == "" { 469 action.Id = NewId() 470 } 471 } 472 } 473 } 474 } 475 476 var markdownDestinationEscaper = strings.NewReplacer( 477 `\`, `\\`, 478 `<`, `\<`, 479 `>`, `\>`, 480 `(`, `\(`, 481 `)`, `\)`, 482 ) 483 484 // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been 485 // rewritten via RewriteImageURLs. 486 func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { 487 copy := *o 488 copy.Message = RewriteImageURLs(o.Message, f) 489 if copy.MessageSource == "" && copy.Message != o.Message { 490 copy.MessageSource = o.Message 491 } 492 return © 493 } 494 495 func (o *PostEphemeral) ToUnsanitizedJson() string { 496 b, _ := json.Marshal(o) 497 return string(b) 498 } 499 500 func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest { 501 var o *DoPostActionRequest 502 json.NewDecoder(data).Decode(&o) 503 return o 504 } 505 506 // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced 507 // according to the function f. For each image URL, f will be invoked, and the resulting markdown 508 // will contain the URL returned by that invocation instead. 509 // 510 // Image URLs are destination URLs used in inline images or reference definitions that are used 511 // anywhere in the input markdown as an image. 512 func RewriteImageURLs(message string, f func(string) string) string { 513 if !strings.Contains(message, "![") { 514 return message 515 } 516 517 var ranges []markdown.Range 518 519 markdown.Inspect(message, func(blockOrInline interface{}) bool { 520 switch v := blockOrInline.(type) { 521 case *markdown.ReferenceImage: 522 ranges = append(ranges, v.ReferenceDefinition.RawDestination) 523 case *markdown.InlineImage: 524 ranges = append(ranges, v.RawDestination) 525 default: 526 return true 527 } 528 return true 529 }) 530 531 if ranges == nil { 532 return message 533 } 534 535 sort.Slice(ranges, func(i, j int) bool { 536 return ranges[i].Position < ranges[j].Position 537 }) 538 539 copyRanges := make([]markdown.Range, 0, len(ranges)) 540 urls := make([]string, 0, len(ranges)) 541 resultLength := len(message) 542 543 start := 0 544 for i, r := range ranges { 545 switch { 546 case i == 0: 547 case r.Position != ranges[i-1].Position: 548 start = ranges[i-1].End 549 default: 550 continue 551 } 552 original := message[r.Position:r.End] 553 replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) 554 resultLength += len(replacement) - len(original) 555 copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) 556 urls = append(urls, replacement) 557 } 558 559 result := make([]byte, resultLength) 560 561 offset := 0 562 for i, r := range copyRanges { 563 offset += copy(result[offset:], message[r.Position:r.End]) 564 offset += copy(result[offset:], urls[i]) 565 } 566 copy(result[offset:], message[ranges[len(ranges)-1].End:]) 567 568 return string(result) 569 }