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