github.com/lologarithm/mattermost-server@v5.3.2-0.20181002060438-c82a84ed765b+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 Page *int `json:"page"` 104 PerPage *int `json:"per_page"` 105 IncludeDeletedChannels *bool `json:"include_deleted_channels"` 106 } 107 108 func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { 109 copy := *o 110 if copy.Message != nil { 111 *copy.Message = RewriteImageURLs(*o.Message, f) 112 } 113 return © 114 } 115 116 type PostForExport struct { 117 Post 118 TeamName string 119 ChannelName string 120 Username string 121 ReplyCount int 122 } 123 124 type ReplyForExport struct { 125 Post 126 Username string 127 } 128 129 type PostForIndexing struct { 130 Post 131 TeamId string `json:"team_id"` 132 ParentCreateAt *int64 `json:"parent_create_at"` 133 } 134 135 type DoPostActionRequest struct { 136 SelectedOption string `json:"selected_option"` 137 } 138 139 type PostAction struct { 140 Id string `json:"id"` 141 Name string `json:"name"` 142 Type string `json:"type"` 143 DataSource string `json:"data_source"` 144 Options []*PostActionOptions `json:"options"` 145 Integration *PostActionIntegration `json:"integration,omitempty"` 146 } 147 148 type PostActionOptions struct { 149 Text string `json:"text"` 150 Value string `json:"value"` 151 } 152 153 type PostActionIntegration struct { 154 URL string `json:"url,omitempty"` 155 Context StringInterface `json:"context,omitempty"` 156 } 157 158 type PostActionIntegrationRequest struct { 159 UserId string `json:"user_id"` 160 ChannelId string `json:"channel_id"` 161 TeamId string `json:"team_id"` 162 PostId string `json:"post_id"` 163 Type string `json:"type"` 164 DataSource string `json:"data_source"` 165 Context StringInterface `json:"context,omitempty"` 166 } 167 168 type PostActionIntegrationResponse struct { 169 Update *Post `json:"update"` 170 EphemeralText string `json:"ephemeral_text"` 171 } 172 173 func (o *Post) ToJson() string { 174 copy := *o 175 copy.StripActionIntegrations() 176 b, _ := json.Marshal(©) 177 return string(b) 178 } 179 180 func (o *Post) ToUnsanitizedJson() string { 181 b, _ := json.Marshal(o) 182 return string(b) 183 } 184 185 func PostFromJson(data io.Reader) *Post { 186 var o *Post 187 json.NewDecoder(data).Decode(&o) 188 return o 189 } 190 191 func (o *Post) Etag() string { 192 return Etag(o.Id, o.UpdateAt) 193 } 194 195 func (o *Post) IsValid(maxPostSize int) *AppError { 196 197 if len(o.Id) != 26 { 198 return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) 199 } 200 201 if o.CreateAt == 0 { 202 return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 203 } 204 205 if o.UpdateAt == 0 { 206 return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 207 } 208 209 if len(o.UserId) != 26 { 210 return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) 211 } 212 213 if len(o.ChannelId) != 26 { 214 return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) 215 } 216 217 if !(len(o.RootId) == 26 || len(o.RootId) == 0) { 218 return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) 219 } 220 221 if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { 222 return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) 223 } 224 225 if len(o.ParentId) == 26 && len(o.RootId) == 0 { 226 return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) 227 } 228 229 if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { 230 return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) 231 } 232 233 if utf8.RuneCountInString(o.Message) > maxPostSize { 234 return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) 235 } 236 237 if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { 238 return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) 239 } 240 241 switch o.Type { 242 case 243 POST_DEFAULT, 244 POST_JOIN_LEAVE, 245 POST_AUTO_RESPONDER, 246 POST_ADD_REMOVE, 247 POST_JOIN_CHANNEL, 248 POST_LEAVE_CHANNEL, 249 POST_JOIN_TEAM, 250 POST_LEAVE_TEAM, 251 POST_ADD_TO_CHANNEL, 252 POST_REMOVE_FROM_CHANNEL, 253 POST_MOVE_CHANNEL, 254 POST_ADD_TO_TEAM, 255 POST_REMOVE_FROM_TEAM, 256 POST_SLACK_ATTACHMENT, 257 POST_HEADER_CHANGE, 258 POST_PURPOSE_CHANGE, 259 POST_DISPLAYNAME_CHANGE, 260 POST_CONVERT_CHANNEL, 261 POST_CHANNEL_DELETED, 262 POST_CHANGE_CHANNEL_PRIVACY: 263 default: 264 if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { 265 return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) 266 } 267 } 268 269 if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { 270 return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) 271 } 272 273 if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { 274 return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) 275 } 276 277 if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { 278 return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) 279 } 280 281 return nil 282 } 283 284 func (o *Post) SanitizeProps() { 285 membersToSanitize := []string{ 286 PROPS_ADD_CHANNEL_MEMBER, 287 } 288 289 for _, member := range membersToSanitize { 290 if _, ok := o.Props[member]; ok { 291 delete(o.Props, member) 292 } 293 } 294 } 295 296 func (o *Post) PreSave() { 297 if o.Id == "" { 298 o.Id = NewId() 299 } 300 301 o.OriginalId = "" 302 303 if o.CreateAt == 0 { 304 o.CreateAt = GetMillis() 305 } 306 307 o.UpdateAt = o.CreateAt 308 o.PreCommit() 309 } 310 311 func (o *Post) PreCommit() { 312 if o.Props == nil { 313 o.Props = make(map[string]interface{}) 314 } 315 316 if o.Filenames == nil { 317 o.Filenames = []string{} 318 } 319 320 if o.FileIds == nil { 321 o.FileIds = []string{} 322 } 323 324 o.GenerateActionIds() 325 } 326 327 func (o *Post) MakeNonNil() { 328 if o.Props == nil { 329 o.Props = make(map[string]interface{}) 330 } 331 } 332 333 func (o *Post) AddProp(key string, value interface{}) { 334 335 o.MakeNonNil() 336 337 o.Props[key] = value 338 } 339 340 func (o *Post) IsSystemMessage() bool { 341 return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX 342 } 343 344 func (p *Post) Patch(patch *PostPatch) { 345 if patch.IsPinned != nil { 346 p.IsPinned = *patch.IsPinned 347 } 348 349 if patch.Message != nil { 350 p.Message = *patch.Message 351 } 352 353 if patch.Props != nil { 354 p.Props = *patch.Props 355 } 356 357 if patch.FileIds != nil { 358 p.FileIds = *patch.FileIds 359 } 360 361 if patch.HasReactions != nil { 362 p.HasReactions = *patch.HasReactions 363 } 364 } 365 366 func (o *PostPatch) ToJson() string { 367 b, err := json.Marshal(o) 368 if err != nil { 369 return "" 370 } 371 372 return string(b) 373 } 374 375 func PostPatchFromJson(data io.Reader) *PostPatch { 376 decoder := json.NewDecoder(data) 377 var post PostPatch 378 err := decoder.Decode(&post) 379 if err != nil { 380 return nil 381 } 382 383 return &post 384 } 385 386 func (o *SearchParameter) SearchParameterToJson() string { 387 b, err := json.Marshal(o) 388 if err != nil { 389 return "" 390 } 391 392 return string(b) 393 } 394 395 func SearchParameterFromJson(data io.Reader) *SearchParameter { 396 decoder := json.NewDecoder(data) 397 var searchParam SearchParameter 398 err := decoder.Decode(&searchParam) 399 if err != nil { 400 return nil 401 } 402 403 return &searchParam 404 } 405 406 func (o *Post) ChannelMentions() []string { 407 return ChannelMentions(o.Message) 408 } 409 410 func (r *PostActionIntegrationRequest) ToJson() string { 411 b, _ := json.Marshal(r) 412 return string(b) 413 } 414 415 func PostActionIntegrationRequesteFromJson(data io.Reader) *PostActionIntegrationRequest { 416 var o *PostActionIntegrationRequest 417 err := json.NewDecoder(data).Decode(&o) 418 if err != nil { 419 return nil 420 } 421 return o 422 } 423 424 func (r *PostActionIntegrationResponse) ToJson() string { 425 b, _ := json.Marshal(r) 426 return string(b) 427 } 428 429 func PostActionIntegrationResponseFromJson(data io.Reader) *PostActionIntegrationResponse { 430 var o *PostActionIntegrationResponse 431 err := json.NewDecoder(data).Decode(&o) 432 if err != nil { 433 return nil 434 } 435 return o 436 } 437 438 func (o *Post) Attachments() []*SlackAttachment { 439 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 440 return attachments 441 } 442 var ret []*SlackAttachment 443 if attachments, ok := o.Props["attachments"].([]interface{}); ok { 444 for _, attachment := range attachments { 445 if enc, err := json.Marshal(attachment); err == nil { 446 var decoded SlackAttachment 447 if json.Unmarshal(enc, &decoded) == nil { 448 ret = append(ret, &decoded) 449 } 450 } 451 } 452 } 453 return ret 454 } 455 456 func (o *Post) StripActionIntegrations() { 457 attachments := o.Attachments() 458 if o.Props["attachments"] != nil { 459 o.Props["attachments"] = attachments 460 } 461 for _, attachment := range attachments { 462 for _, action := range attachment.Actions { 463 action.Integration = nil 464 } 465 } 466 } 467 468 func (o *Post) GetAction(id string) *PostAction { 469 for _, attachment := range o.Attachments() { 470 for _, action := range attachment.Actions { 471 if action.Id == id { 472 return action 473 } 474 } 475 } 476 return nil 477 } 478 479 func (o *Post) GenerateActionIds() { 480 if o.Props["attachments"] != nil { 481 o.Props["attachments"] = o.Attachments() 482 } 483 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 484 for _, attachment := range attachments { 485 for _, action := range attachment.Actions { 486 if action.Id == "" { 487 action.Id = NewId() 488 } 489 } 490 } 491 } 492 } 493 494 var markdownDestinationEscaper = strings.NewReplacer( 495 `\`, `\\`, 496 `<`, `\<`, 497 `>`, `\>`, 498 `(`, `\(`, 499 `)`, `\)`, 500 ) 501 502 // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been 503 // rewritten via RewriteImageURLs. 504 func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { 505 copy := *o 506 copy.Message = RewriteImageURLs(o.Message, f) 507 if copy.MessageSource == "" && copy.Message != o.Message { 508 copy.MessageSource = o.Message 509 } 510 return © 511 } 512 513 func (o *PostEphemeral) ToUnsanitizedJson() string { 514 b, _ := json.Marshal(o) 515 return string(b) 516 } 517 518 func DoPostActionRequestFromJson(data io.Reader) *DoPostActionRequest { 519 var o *DoPostActionRequest 520 json.NewDecoder(data).Decode(&o) 521 return o 522 } 523 524 // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced 525 // according to the function f. For each image URL, f will be invoked, and the resulting markdown 526 // will contain the URL returned by that invocation instead. 527 // 528 // Image URLs are destination URLs used in inline images or reference definitions that are used 529 // anywhere in the input markdown as an image. 530 func RewriteImageURLs(message string, f func(string) string) string { 531 if !strings.Contains(message, "![") { 532 return message 533 } 534 535 var ranges []markdown.Range 536 537 markdown.Inspect(message, func(blockOrInline interface{}) bool { 538 switch v := blockOrInline.(type) { 539 case *markdown.ReferenceImage: 540 ranges = append(ranges, v.ReferenceDefinition.RawDestination) 541 case *markdown.InlineImage: 542 ranges = append(ranges, v.RawDestination) 543 default: 544 return true 545 } 546 return true 547 }) 548 549 if ranges == nil { 550 return message 551 } 552 553 sort.Slice(ranges, func(i, j int) bool { 554 return ranges[i].Position < ranges[j].Position 555 }) 556 557 copyRanges := make([]markdown.Range, 0, len(ranges)) 558 urls := make([]string, 0, len(ranges)) 559 resultLength := len(message) 560 561 start := 0 562 for i, r := range ranges { 563 switch { 564 case i == 0: 565 case r.Position != ranges[i-1].Position: 566 start = ranges[i-1].End 567 default: 568 continue 569 } 570 original := message[r.Position:r.End] 571 replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) 572 resultLength += len(replacement) - len(original) 573 copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) 574 urls = append(urls, replacement) 575 } 576 577 result := make([]byte, resultLength) 578 579 offset := 0 580 for i, r := range copyRanges { 581 offset += copy(result[offset:], message[r.Position:r.End]) 582 offset += copy(result[offset:], urls[i]) 583 } 584 copy(result[offset:], message[ranges[len(ranges)-1].End:]) 585 586 return string(result) 587 }