github.com/demisto/mattermost-server@v4.9.0-rc3+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 "regexp" 11 "sort" 12 "strings" 13 "unicode/utf8" 14 15 "github.com/mattermost/mattermost-server/utils/markdown" 16 ) 17 18 const ( 19 POST_SYSTEM_MESSAGE_PREFIX = "system_" 20 POST_DEFAULT = "" 21 POST_SLACK_ATTACHMENT = "slack_attachment" 22 POST_SYSTEM_GENERIC = "system_generic" 23 POST_JOIN_LEAVE = "system_join_leave" // Deprecated, use POST_JOIN_CHANNEL or POST_LEAVE_CHANNEL instead 24 POST_JOIN_CHANNEL = "system_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_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 ) 52 53 type Post struct { 54 Id string `json:"id"` 55 CreateAt int64 `json:"create_at"` 56 UpdateAt int64 `json:"update_at"` 57 EditAt int64 `json:"edit_at"` 58 DeleteAt int64 `json:"delete_at"` 59 IsPinned bool `json:"is_pinned"` 60 UserId string `json:"user_id"` 61 ChannelId string `json:"channel_id"` 62 RootId string `json:"root_id"` 63 ParentId string `json:"parent_id"` 64 OriginalId string `json:"original_id"` 65 66 Message string `json:"message"` 67 68 // MessageSource will contain the message as submitted by the user if Message has been modified 69 // by Mattermost for presentation (e.g if an image proxy is being used). It should be used to 70 // populate edit boxes if present. 71 MessageSource string `json:"message_source,omitempty" db:"-"` 72 73 Type string `json:"type"` 74 Props StringInterface `json:"props"` 75 Hashtags string `json:"hashtags"` 76 Filenames StringArray `json:"filenames,omitempty"` // Deprecated, do not use this field any more 77 FileIds StringArray `json:"file_ids,omitempty"` 78 PendingPostId string `json:"pending_post_id" db:"-"` 79 HasReactions bool `json:"has_reactions,omitempty"` 80 } 81 82 type PostPatch struct { 83 IsPinned *bool `json:"is_pinned"` 84 Message *string `json:"message"` 85 Props *StringInterface `json:"props"` 86 FileIds *StringArray `json:"file_ids"` 87 HasReactions *bool `json:"has_reactions"` 88 } 89 90 func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch { 91 copy := *o 92 if copy.Message != nil { 93 *copy.Message = RewriteImageURLs(*o.Message, f) 94 } 95 return © 96 } 97 98 type PostForIndexing struct { 99 Post 100 TeamId string `json:"team_id"` 101 ParentCreateAt *int64 `json:"parent_create_at"` 102 } 103 104 type PostAction struct { 105 Id string `json:"id"` 106 Name string `json:"name"` 107 Integration *PostActionIntegration `json:"integration,omitempty"` 108 } 109 110 type PostActionIntegration struct { 111 URL string `json:"url,omitempty"` 112 Context StringInterface `json:"context,omitempty"` 113 } 114 115 type PostActionIntegrationRequest struct { 116 UserId string `json:"user_id"` 117 Context StringInterface `json:"context,omitempty"` 118 } 119 120 type PostActionIntegrationResponse struct { 121 Update *Post `json:"update"` 122 EphemeralText string `json:"ephemeral_text"` 123 } 124 125 func (o *Post) ToJson() string { 126 copy := *o 127 copy.StripActionIntegrations() 128 b, _ := json.Marshal(©) 129 return string(b) 130 } 131 132 func (o *Post) ToUnsanitizedJson() string { 133 b, _ := json.Marshal(o) 134 return string(b) 135 } 136 137 func PostFromJson(data io.Reader) *Post { 138 var o *Post 139 json.NewDecoder(data).Decode(&o) 140 return o 141 } 142 143 func (o *Post) Etag() string { 144 return Etag(o.Id, o.UpdateAt) 145 } 146 147 func (o *Post) IsValid(maxPostSize int) *AppError { 148 149 if len(o.Id) != 26 { 150 return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest) 151 } 152 153 if o.CreateAt == 0 { 154 return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 155 } 156 157 if o.UpdateAt == 0 { 158 return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest) 159 } 160 161 if len(o.UserId) != 26 { 162 return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest) 163 } 164 165 if len(o.ChannelId) != 26 { 166 return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest) 167 } 168 169 if !(len(o.RootId) == 26 || len(o.RootId) == 0) { 170 return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest) 171 } 172 173 if !(len(o.ParentId) == 26 || len(o.ParentId) == 0) { 174 return NewAppError("Post.IsValid", "model.post.is_valid.parent_id.app_error", nil, "", http.StatusBadRequest) 175 } 176 177 if len(o.ParentId) == 26 && len(o.RootId) == 0 { 178 return NewAppError("Post.IsValid", "model.post.is_valid.root_parent.app_error", nil, "", http.StatusBadRequest) 179 } 180 181 if !(len(o.OriginalId) == 26 || len(o.OriginalId) == 0) { 182 return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest) 183 } 184 185 if utf8.RuneCountInString(o.Message) > maxPostSize { 186 return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest) 187 } 188 189 if utf8.RuneCountInString(o.Hashtags) > POST_HASHTAGS_MAX_RUNES { 190 return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest) 191 } 192 193 switch o.Type { 194 case 195 POST_DEFAULT, 196 POST_JOIN_LEAVE, 197 POST_ADD_REMOVE, 198 POST_JOIN_CHANNEL, 199 POST_LEAVE_CHANNEL, 200 POST_JOIN_TEAM, 201 POST_LEAVE_TEAM, 202 POST_ADD_TO_CHANNEL, 203 POST_REMOVE_FROM_CHANNEL, 204 POST_MOVE_CHANNEL, 205 POST_ADD_TO_TEAM, 206 POST_REMOVE_FROM_TEAM, 207 POST_SLACK_ATTACHMENT, 208 POST_HEADER_CHANGE, 209 POST_PURPOSE_CHANGE, 210 POST_DISPLAYNAME_CHANGE, 211 POST_CONVERT_CHANNEL, 212 POST_CHANNEL_DELETED, 213 POST_CHANGE_CHANNEL_PRIVACY: 214 default: 215 if !strings.HasPrefix(o.Type, POST_CUSTOM_TYPE_PREFIX) { 216 return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest) 217 } 218 } 219 220 if utf8.RuneCountInString(ArrayToJson(o.Filenames)) > POST_FILENAMES_MAX_RUNES { 221 return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest) 222 } 223 224 if utf8.RuneCountInString(ArrayToJson(o.FileIds)) > POST_FILEIDS_MAX_RUNES { 225 return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest) 226 } 227 228 if utf8.RuneCountInString(StringInterfaceToJson(o.Props)) > POST_PROPS_MAX_RUNES { 229 return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest) 230 } 231 232 return nil 233 } 234 235 func (o *Post) SanitizeProps() { 236 membersToSanitize := []string{ 237 PROPS_ADD_CHANNEL_MEMBER, 238 } 239 240 for _, member := range membersToSanitize { 241 if _, ok := o.Props[member]; ok { 242 delete(o.Props, member) 243 } 244 } 245 } 246 247 func (o *Post) PreSave() { 248 if o.Id == "" { 249 o.Id = NewId() 250 } 251 252 o.OriginalId = "" 253 254 if o.CreateAt == 0 { 255 o.CreateAt = GetMillis() 256 } 257 258 o.UpdateAt = o.CreateAt 259 o.PreCommit() 260 } 261 262 func (o *Post) PreCommit() { 263 if o.Props == nil { 264 o.Props = make(map[string]interface{}) 265 } 266 267 if o.Filenames == nil { 268 o.Filenames = []string{} 269 } 270 271 if o.FileIds == nil { 272 o.FileIds = []string{} 273 } 274 275 o.GenerateActionIds() 276 } 277 278 func (o *Post) MakeNonNil() { 279 if o.Props == nil { 280 o.Props = make(map[string]interface{}) 281 } 282 } 283 284 func (o *Post) AddProp(key string, value interface{}) { 285 286 o.MakeNonNil() 287 288 o.Props[key] = value 289 } 290 291 func (o *Post) IsSystemMessage() bool { 292 return len(o.Type) >= len(POST_SYSTEM_MESSAGE_PREFIX) && o.Type[:len(POST_SYSTEM_MESSAGE_PREFIX)] == POST_SYSTEM_MESSAGE_PREFIX 293 } 294 295 func (p *Post) Patch(patch *PostPatch) { 296 if patch.IsPinned != nil { 297 p.IsPinned = *patch.IsPinned 298 } 299 300 if patch.Message != nil { 301 p.Message = *patch.Message 302 } 303 304 if patch.Props != nil { 305 p.Props = *patch.Props 306 } 307 308 if patch.FileIds != nil { 309 p.FileIds = *patch.FileIds 310 } 311 312 if patch.HasReactions != nil { 313 p.HasReactions = *patch.HasReactions 314 } 315 } 316 317 func (o *PostPatch) ToJson() string { 318 b, err := json.Marshal(o) 319 if err != nil { 320 return "" 321 } 322 323 return string(b) 324 } 325 326 func PostPatchFromJson(data io.Reader) *PostPatch { 327 decoder := json.NewDecoder(data) 328 var post PostPatch 329 err := decoder.Decode(&post) 330 if err != nil { 331 return nil 332 } 333 334 return &post 335 } 336 337 var channelMentionRegexp = regexp.MustCompile(`\B~[a-zA-Z0-9\-_]+`) 338 339 func (o *Post) ChannelMentions() (names []string) { 340 if strings.Contains(o.Message, "~") { 341 alreadyMentioned := make(map[string]bool) 342 for _, match := range channelMentionRegexp.FindAllString(o.Message, -1) { 343 name := match[1:] 344 if !alreadyMentioned[name] { 345 names = append(names, name) 346 alreadyMentioned[name] = true 347 } 348 } 349 } 350 return 351 } 352 353 func (r *PostActionIntegrationRequest) ToJson() string { 354 b, _ := json.Marshal(r) 355 return string(b) 356 } 357 358 func (o *Post) Attachments() []*SlackAttachment { 359 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 360 return attachments 361 } 362 var ret []*SlackAttachment 363 if attachments, ok := o.Props["attachments"].([]interface{}); ok { 364 for _, attachment := range attachments { 365 if enc, err := json.Marshal(attachment); err == nil { 366 var decoded SlackAttachment 367 if json.Unmarshal(enc, &decoded) == nil { 368 ret = append(ret, &decoded) 369 } 370 } 371 } 372 } 373 return ret 374 } 375 376 func (o *Post) StripActionIntegrations() { 377 attachments := o.Attachments() 378 if o.Props["attachments"] != nil { 379 o.Props["attachments"] = attachments 380 } 381 for _, attachment := range attachments { 382 for _, action := range attachment.Actions { 383 action.Integration = nil 384 } 385 } 386 } 387 388 func (o *Post) GetAction(id string) *PostAction { 389 for _, attachment := range o.Attachments() { 390 for _, action := range attachment.Actions { 391 if action.Id == id { 392 return action 393 } 394 } 395 } 396 return nil 397 } 398 399 func (o *Post) GenerateActionIds() { 400 if o.Props["attachments"] != nil { 401 o.Props["attachments"] = o.Attachments() 402 } 403 if attachments, ok := o.Props["attachments"].([]*SlackAttachment); ok { 404 for _, attachment := range attachments { 405 for _, action := range attachment.Actions { 406 if action.Id == "" { 407 action.Id = NewId() 408 } 409 } 410 } 411 } 412 } 413 414 var markdownDestinationEscaper = strings.NewReplacer( 415 `\`, `\\`, 416 `<`, `\<`, 417 `>`, `\>`, 418 `(`, `\(`, 419 `)`, `\)`, 420 ) 421 422 // WithRewrittenImageURLs returns a new shallow copy of the post where the message has been 423 // rewritten via RewriteImageURLs. 424 func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post { 425 copy := *o 426 copy.Message = RewriteImageURLs(o.Message, f) 427 if copy.MessageSource == "" && copy.Message != o.Message { 428 copy.MessageSource = o.Message 429 } 430 return © 431 } 432 433 // RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced 434 // according to the function f. For each image URL, f will be invoked, and the resulting markdown 435 // will contain the URL returned by that invocation instead. 436 // 437 // Image URLs are destination URLs used in inline images or reference definitions that are used 438 // anywhere in the input markdown as an image. 439 func RewriteImageURLs(message string, f func(string) string) string { 440 if !strings.Contains(message, "![") { 441 return message 442 } 443 444 var ranges []markdown.Range 445 446 markdown.Inspect(message, func(blockOrInline interface{}) bool { 447 switch v := blockOrInline.(type) { 448 case *markdown.ReferenceImage: 449 ranges = append(ranges, v.ReferenceDefinition.RawDestination) 450 case *markdown.InlineImage: 451 ranges = append(ranges, v.RawDestination) 452 default: 453 return true 454 } 455 return true 456 }) 457 458 if ranges == nil { 459 return message 460 } 461 462 sort.Slice(ranges, func(i, j int) bool { 463 return ranges[i].Position < ranges[j].Position 464 }) 465 466 copyRanges := make([]markdown.Range, 0, len(ranges)) 467 urls := make([]string, 0, len(ranges)) 468 resultLength := len(message) 469 470 start := 0 471 for i, r := range ranges { 472 switch { 473 case i == 0: 474 case r.Position != ranges[i-1].Position: 475 start = ranges[i-1].End 476 default: 477 continue 478 } 479 original := message[r.Position:r.End] 480 replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original))) 481 resultLength += len(replacement) - len(original) 482 copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position}) 483 urls = append(urls, replacement) 484 } 485 486 result := make([]byte, resultLength) 487 488 offset := 0 489 for i, r := range copyRanges { 490 offset += copy(result[offset:], message[r.Position:r.End]) 491 offset += copy(result[offset:], urls[i]) 492 } 493 copy(result[offset:], message[ranges[len(ranges)-1].End:]) 494 495 return string(result) 496 }