1// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2// See LICENSE.txt for license information.
3
4package model
5
6import (
7	"encoding/json"
8	"errors"
9	"net/http"
10	"regexp"
11	"sort"
12	"strings"
13	"sync"
14	"unicode/utf8"
15
16	"github.com/mattermost/mattermost-server/v6/shared/markdown"
17)
18
19const (
20	PostSystemMessagePrefix        = "system_"
21	PostTypeDefault                = ""
22	PostTypeSlackAttachment        = "slack_attachment"
23	PostTypeSystemGeneric          = "system_generic"
24	PostTypeJoinLeave              = "system_join_leave" // Deprecated, use PostJoinChannel or PostLeaveChannel instead
25	PostTypeJoinChannel            = "system_join_channel"
26	PostTypeGuestJoinChannel       = "system_guest_join_channel"
27	PostTypeLeaveChannel           = "system_leave_channel"
28	PostTypeJoinTeam               = "system_join_team"
29	PostTypeLeaveTeam              = "system_leave_team"
30	PostTypeAutoResponder          = "system_auto_responder"
31	PostTypeAddRemove              = "system_add_remove" // Deprecated, use PostAddToChannel or PostRemoveFromChannel instead
32	PostTypeAddToChannel           = "system_add_to_channel"
33	PostTypeAddGuestToChannel      = "system_add_guest_to_chan"
34	PostTypeRemoveFromChannel      = "system_remove_from_channel"
35	PostTypeMoveChannel            = "system_move_channel"
36	PostTypeAddToTeam              = "system_add_to_team"
37	PostTypeRemoveFromTeam         = "system_remove_from_team"
38	PostTypeHeaderChange           = "system_header_change"
39	PostTypeDisplaynameChange      = "system_displayname_change"
40	PostTypeConvertChannel         = "system_convert_channel"
41	PostTypePurposeChange          = "system_purpose_change"
42	PostTypeChannelDeleted         = "system_channel_deleted"
43	PostTypeChannelRestored        = "system_channel_restored"
44	PostTypeEphemeral              = "system_ephemeral"
45	PostTypeChangeChannelPrivacy   = "system_change_chan_privacy"
46	PostTypeAddBotTeamsChannels    = "add_bot_teams_channels"
47	PostTypeSystemWarnMetricStatus = "warn_metric_status"
48	PostTypeMe                     = "me"
49	PostCustomTypePrefix           = "custom_"
50
51	PostFileidsMaxRunes   = 300
52	PostFilenamesMaxRunes = 4000
53	PostHashtagsMaxRunes  = 1000
54	PostMessageMaxRunesV1 = 4000
55	PostMessageMaxBytesV2 = 65535                     // Maximum size of a TEXT column in MySQL
56	PostMessageMaxRunesV2 = PostMessageMaxBytesV2 / 4 // Assume a worst-case representation
57	PostPropsMaxRunes     = 800000
58	PostPropsMaxUserRunes = PostPropsMaxRunes - 40000 // Leave some room for system / pre-save modifications
59
60	PropsAddChannelMember = "add_channel_member"
61
62	PostPropsAddedUserId       = "addedUserId"
63	PostPropsDeleteBy          = "deleteBy"
64	PostPropsOverrideIconURL   = "override_icon_url"
65	PostPropsOverrideIconEmoji = "override_icon_emoji"
66
67	PostPropsMentionHighlightDisabled = "mentionHighlightDisabled"
68	PostPropsGroupHighlightDisabled   = "disable_group_highlight"
69
70	PostPropsPreviewedPost = "previewed_post"
71)
72
73type Post struct {
74	Id         string `json:"id"`
75	CreateAt   int64  `json:"create_at"`
76	UpdateAt   int64  `json:"update_at"`
77	EditAt     int64  `json:"edit_at"`
78	DeleteAt   int64  `json:"delete_at"`
79	IsPinned   bool   `json:"is_pinned"`
80	UserId     string `json:"user_id"`
81	ChannelId  string `json:"channel_id"`
82	RootId     string `json:"root_id"`
83	OriginalId string `json:"original_id"`
84
85	Message string `json:"message"`
86	// MessageSource will contain the message as submitted by the user if Message has been modified
87	// by Mattermost for presentation (e.g if an image proxy is being used). It should be used to
88	// populate edit boxes if present.
89	MessageSource string `json:"message_source,omitempty" db:"-"`
90
91	Type          string          `json:"type"`
92	propsMu       sync.RWMutex    `db:"-"`       // Unexported mutex used to guard Post.Props.
93	Props         StringInterface `json:"props"` // Deprecated: use GetProps()
94	Hashtags      string          `json:"hashtags"`
95	Filenames     StringArray     `json:"-"` // Deprecated, do not use this field any more
96	FileIds       StringArray     `json:"file_ids,omitempty"`
97	PendingPostId string          `json:"pending_post_id" db:"-"`
98	HasReactions  bool            `json:"has_reactions,omitempty"`
99	RemoteId      *string         `json:"remote_id,omitempty"`
100
101	// Transient data populated before sending a post to the client
102	ReplyCount   int64         `json:"reply_count" db:"-"`
103	LastReplyAt  int64         `json:"last_reply_at" db:"-"`
104	Participants []*User       `json:"participants" db:"-"`
105	IsFollowing  *bool         `json:"is_following,omitempty" db:"-"` // for root posts in collapsed thread mode indicates if the current user is following this thread
106	Metadata     *PostMetadata `json:"metadata,omitempty" db:"-"`
107}
108
109type PostEphemeral struct {
110	UserID string `json:"user_id"`
111	Post   *Post  `json:"post"`
112}
113
114type PostPatch struct {
115	IsPinned     *bool            `json:"is_pinned"`
116	Message      *string          `json:"message"`
117	Props        *StringInterface `json:"props"`
118	FileIds      *StringArray     `json:"file_ids"`
119	HasReactions *bool            `json:"has_reactions"`
120}
121
122type SearchParameter struct {
123	Terms                  *string `json:"terms"`
124	IsOrSearch             *bool   `json:"is_or_search"`
125	TimeZoneOffset         *int    `json:"time_zone_offset"`
126	Page                   *int    `json:"page"`
127	PerPage                *int    `json:"per_page"`
128	IncludeDeletedChannels *bool   `json:"include_deleted_channels"`
129}
130
131type AnalyticsPostCountsOptions struct {
132	TeamId        string
133	BotsOnly      bool
134	YesterdayOnly bool
135}
136
137func (o *PostPatch) WithRewrittenImageURLs(f func(string) string) *PostPatch {
138	copy := *o
139	if copy.Message != nil {
140		*copy.Message = RewriteImageURLs(*o.Message, f)
141	}
142	return &copy
143}
144
145type PostForExport struct {
146	Post
147	TeamName    string
148	ChannelName string
149	Username    string
150	ReplyCount  int
151}
152
153type DirectPostForExport struct {
154	Post
155	User           string
156	ChannelMembers *[]string
157}
158
159type ReplyForExport struct {
160	Post
161	Username string
162}
163
164type PostForIndexing struct {
165	Post
166	TeamId         string `json:"team_id"`
167	ParentCreateAt *int64 `json:"parent_create_at"`
168}
169
170type FileForIndexing struct {
171	FileInfo
172	ChannelId string `json:"channel_id"`
173	Content   string `json:"content"`
174}
175
176// ShallowCopy is an utility function to shallow copy a Post to the given
177// destination without touching the internal RWMutex.
178func (o *Post) ShallowCopy(dst *Post) error {
179	if dst == nil {
180		return errors.New("dst cannot be nil")
181	}
182	o.propsMu.RLock()
183	defer o.propsMu.RUnlock()
184	dst.propsMu.Lock()
185	defer dst.propsMu.Unlock()
186	dst.Id = o.Id
187	dst.CreateAt = o.CreateAt
188	dst.UpdateAt = o.UpdateAt
189	dst.EditAt = o.EditAt
190	dst.DeleteAt = o.DeleteAt
191	dst.IsPinned = o.IsPinned
192	dst.UserId = o.UserId
193	dst.ChannelId = o.ChannelId
194	dst.RootId = o.RootId
195	dst.OriginalId = o.OriginalId
196	dst.Message = o.Message
197	dst.MessageSource = o.MessageSource
198	dst.Type = o.Type
199	dst.Props = o.Props
200	dst.Hashtags = o.Hashtags
201	dst.Filenames = o.Filenames
202	dst.FileIds = o.FileIds
203	dst.PendingPostId = o.PendingPostId
204	dst.HasReactions = o.HasReactions
205	dst.ReplyCount = o.ReplyCount
206	dst.Participants = o.Participants
207	dst.LastReplyAt = o.LastReplyAt
208	dst.Metadata = o.Metadata
209	if o.IsFollowing != nil {
210		dst.IsFollowing = NewBool(*o.IsFollowing)
211	}
212	dst.RemoteId = o.RemoteId
213	return nil
214}
215
216// Clone shallowly copies the post and returns the copy.
217func (o *Post) Clone() *Post {
218	copy := &Post{}
219	o.ShallowCopy(copy)
220	return copy
221}
222
223func (o *Post) ToJSON() (string, error) {
224	copy := o.Clone()
225	copy.StripActionIntegrations()
226	b, err := json.Marshal(copy)
227	return string(b), err
228}
229
230type GetPostsSinceOptions struct {
231	UserId                   string
232	ChannelId                string
233	Time                     int64
234	SkipFetchThreads         bool
235	CollapsedThreads         bool
236	CollapsedThreadsExtended bool
237	SortAscending            bool
238}
239
240type GetPostsSinceForSyncCursor struct {
241	LastPostUpdateAt int64
242	LastPostId       string
243}
244
245type GetPostsSinceForSyncOptions struct {
246	ChannelId       string
247	ExcludeRemoteId string
248	IncludeDeleted  bool
249}
250
251type GetPostsOptions struct {
252	UserId                   string
253	ChannelId                string
254	PostId                   string
255	Page                     int
256	PerPage                  int
257	SkipFetchThreads         bool
258	CollapsedThreads         bool
259	CollapsedThreadsExtended bool
260}
261
262func (o *Post) Etag() string {
263	return Etag(o.Id, o.UpdateAt)
264}
265
266func (o *Post) IsValid(maxPostSize int) *AppError {
267	if !IsValidId(o.Id) {
268		return NewAppError("Post.IsValid", "model.post.is_valid.id.app_error", nil, "", http.StatusBadRequest)
269	}
270
271	if o.CreateAt == 0 {
272		return NewAppError("Post.IsValid", "model.post.is_valid.create_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
273	}
274
275	if o.UpdateAt == 0 {
276		return NewAppError("Post.IsValid", "model.post.is_valid.update_at.app_error", nil, "id="+o.Id, http.StatusBadRequest)
277	}
278
279	if !IsValidId(o.UserId) {
280		return NewAppError("Post.IsValid", "model.post.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
281	}
282
283	if !IsValidId(o.ChannelId) {
284		return NewAppError("Post.IsValid", "model.post.is_valid.channel_id.app_error", nil, "", http.StatusBadRequest)
285	}
286
287	if !(IsValidId(o.RootId) || o.RootId == "") {
288		return NewAppError("Post.IsValid", "model.post.is_valid.root_id.app_error", nil, "", http.StatusBadRequest)
289	}
290
291	if !(len(o.OriginalId) == 26 || o.OriginalId == "") {
292		return NewAppError("Post.IsValid", "model.post.is_valid.original_id.app_error", nil, "", http.StatusBadRequest)
293	}
294
295	if utf8.RuneCountInString(o.Message) > maxPostSize {
296		return NewAppError("Post.IsValid", "model.post.is_valid.msg.app_error", nil, "id="+o.Id, http.StatusBadRequest)
297	}
298
299	if utf8.RuneCountInString(o.Hashtags) > PostHashtagsMaxRunes {
300		return NewAppError("Post.IsValid", "model.post.is_valid.hashtags.app_error", nil, "id="+o.Id, http.StatusBadRequest)
301	}
302
303	switch o.Type {
304	case
305		PostTypeDefault,
306		PostTypeSystemGeneric,
307		PostTypeJoinLeave,
308		PostTypeAutoResponder,
309		PostTypeAddRemove,
310		PostTypeJoinChannel,
311		PostTypeGuestJoinChannel,
312		PostTypeLeaveChannel,
313		PostTypeJoinTeam,
314		PostTypeLeaveTeam,
315		PostTypeAddToChannel,
316		PostTypeAddGuestToChannel,
317		PostTypeRemoveFromChannel,
318		PostTypeMoveChannel,
319		PostTypeAddToTeam,
320		PostTypeRemoveFromTeam,
321		PostTypeSlackAttachment,
322		PostTypeHeaderChange,
323		PostTypePurposeChange,
324		PostTypeDisplaynameChange,
325		PostTypeConvertChannel,
326		PostTypeChannelDeleted,
327		PostTypeChannelRestored,
328		PostTypeChangeChannelPrivacy,
329		PostTypeAddBotTeamsChannels,
330		PostTypeSystemWarnMetricStatus,
331		PostTypeMe:
332	default:
333		if !strings.HasPrefix(o.Type, PostCustomTypePrefix) {
334			return NewAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type, http.StatusBadRequest)
335		}
336	}
337
338	if utf8.RuneCountInString(ArrayToJSON(o.Filenames)) > PostFilenamesMaxRunes {
339		return NewAppError("Post.IsValid", "model.post.is_valid.filenames.app_error", nil, "id="+o.Id, http.StatusBadRequest)
340	}
341
342	if utf8.RuneCountInString(ArrayToJSON(o.FileIds)) > PostFileidsMaxRunes {
343		return NewAppError("Post.IsValid", "model.post.is_valid.file_ids.app_error", nil, "id="+o.Id, http.StatusBadRequest)
344	}
345
346	if utf8.RuneCountInString(StringInterfaceToJSON(o.GetProps())) > PostPropsMaxRunes {
347		return NewAppError("Post.IsValid", "model.post.is_valid.props.app_error", nil, "id="+o.Id, http.StatusBadRequest)
348	}
349
350	return nil
351}
352
353func (o *Post) SanitizeProps() {
354	if o == nil {
355		return
356	}
357	membersToSanitize := []string{
358		PropsAddChannelMember,
359	}
360
361	for _, member := range membersToSanitize {
362		if _, ok := o.GetProps()[member]; ok {
363			o.DelProp(member)
364		}
365	}
366	for _, p := range o.Participants {
367		p.Sanitize(map[string]bool{})
368	}
369}
370
371func (o *Post) PreSave() {
372	if o.Id == "" {
373		o.Id = NewId()
374	}
375
376	o.OriginalId = ""
377
378	if o.CreateAt == 0 {
379		o.CreateAt = GetMillis()
380	}
381
382	o.UpdateAt = o.CreateAt
383	o.PreCommit()
384}
385
386func (o *Post) PreCommit() {
387	if o.GetProps() == nil {
388		o.SetProps(make(map[string]interface{}))
389	}
390
391	if o.Filenames == nil {
392		o.Filenames = []string{}
393	}
394
395	if o.FileIds == nil {
396		o.FileIds = []string{}
397	}
398
399	o.GenerateActionIds()
400
401	// There's a rare bug where the client sends up duplicate FileIds so protect against that
402	o.FileIds = RemoveDuplicateStrings(o.FileIds)
403}
404
405func (o *Post) MakeNonNil() {
406	if o.GetProps() == nil {
407		o.SetProps(make(map[string]interface{}))
408	}
409}
410
411func (o *Post) DelProp(key string) {
412	o.propsMu.Lock()
413	defer o.propsMu.Unlock()
414	propsCopy := make(map[string]interface{}, len(o.Props)-1)
415	for k, v := range o.Props {
416		propsCopy[k] = v
417	}
418	delete(propsCopy, key)
419	o.Props = propsCopy
420}
421
422func (o *Post) AddProp(key string, value interface{}) {
423	o.propsMu.Lock()
424	defer o.propsMu.Unlock()
425	propsCopy := make(map[string]interface{}, len(o.Props)+1)
426	for k, v := range o.Props {
427		propsCopy[k] = v
428	}
429	propsCopy[key] = value
430	o.Props = propsCopy
431}
432
433func (o *Post) GetProps() StringInterface {
434	o.propsMu.RLock()
435	defer o.propsMu.RUnlock()
436	return o.Props
437}
438
439func (o *Post) SetProps(props StringInterface) {
440	o.propsMu.Lock()
441	defer o.propsMu.Unlock()
442	o.Props = props
443}
444
445func (o *Post) GetProp(key string) interface{} {
446	o.propsMu.RLock()
447	defer o.propsMu.RUnlock()
448	return o.Props[key]
449}
450
451func (o *Post) IsSystemMessage() bool {
452	return len(o.Type) >= len(PostSystemMessagePrefix) && o.Type[:len(PostSystemMessagePrefix)] == PostSystemMessagePrefix
453}
454
455// IsRemote returns true if the post originated on a remote cluster.
456func (o *Post) IsRemote() bool {
457	return o.RemoteId != nil && *o.RemoteId != ""
458}
459
460// GetRemoteID safely returns the remoteID or empty string if not remote.
461func (o *Post) GetRemoteID() string {
462	if o.RemoteId != nil {
463		return *o.RemoteId
464	}
465	return ""
466}
467
468func (o *Post) IsJoinLeaveMessage() bool {
469	return o.Type == PostTypeJoinLeave ||
470		o.Type == PostTypeAddRemove ||
471		o.Type == PostTypeJoinChannel ||
472		o.Type == PostTypeLeaveChannel ||
473		o.Type == PostTypeJoinTeam ||
474		o.Type == PostTypeLeaveTeam ||
475		o.Type == PostTypeAddToChannel ||
476		o.Type == PostTypeRemoveFromChannel ||
477		o.Type == PostTypeAddToTeam ||
478		o.Type == PostTypeRemoveFromTeam
479}
480
481func (o *Post) Patch(patch *PostPatch) {
482	if patch.IsPinned != nil {
483		o.IsPinned = *patch.IsPinned
484	}
485
486	if patch.Message != nil {
487		o.Message = *patch.Message
488	}
489
490	if patch.Props != nil {
491		newProps := *patch.Props
492		o.SetProps(newProps)
493	}
494
495	if patch.FileIds != nil {
496		o.FileIds = *patch.FileIds
497	}
498
499	if patch.HasReactions != nil {
500		o.HasReactions = *patch.HasReactions
501	}
502}
503
504func (o *Post) ChannelMentions() []string {
505	return ChannelMentions(o.Message)
506}
507
508// DisableMentionHighlights disables a posts mention highlighting and returns the first channel mention that was present in the message.
509func (o *Post) DisableMentionHighlights() string {
510	mention, hasMentions := findAtChannelMention(o.Message)
511	if hasMentions {
512		o.AddProp(PostPropsMentionHighlightDisabled, true)
513	}
514	return mention
515}
516
517// DisableMentionHighlights disables mention highlighting for a post patch if required.
518func (o *PostPatch) DisableMentionHighlights() {
519	if o.Message == nil {
520		return
521	}
522	if _, hasMentions := findAtChannelMention(*o.Message); hasMentions {
523		if o.Props == nil {
524			o.Props = &StringInterface{}
525		}
526		(*o.Props)[PostPropsMentionHighlightDisabled] = true
527	}
528}
529
530func findAtChannelMention(message string) (mention string, found bool) {
531	re := regexp.MustCompile(`(?i)\B@(channel|all|here)\b`)
532	matched := re.FindStringSubmatch(message)
533	if found = (len(matched) > 0); found {
534		mention = strings.ToLower(matched[0])
535	}
536	return
537}
538
539func (o *Post) Attachments() []*SlackAttachment {
540	if attachments, ok := o.GetProp("attachments").([]*SlackAttachment); ok {
541		return attachments
542	}
543	var ret []*SlackAttachment
544	if attachments, ok := o.GetProp("attachments").([]interface{}); ok {
545		for _, attachment := range attachments {
546			if enc, err := json.Marshal(attachment); err == nil {
547				var decoded SlackAttachment
548				if json.Unmarshal(enc, &decoded) == nil {
549					// Ignoring nil actions
550					i := 0
551					for _, action := range decoded.Actions {
552						if action != nil {
553							decoded.Actions[i] = action
554							i++
555						}
556					}
557					decoded.Actions = decoded.Actions[:i]
558
559					// Ignoring nil fields
560					i = 0
561					for _, field := range decoded.Fields {
562						if field != nil {
563							decoded.Fields[i] = field
564							i++
565						}
566					}
567					decoded.Fields = decoded.Fields[:i]
568					ret = append(ret, &decoded)
569				}
570			}
571		}
572	}
573	return ret
574}
575
576func (o *Post) AttachmentsEqual(input *Post) bool {
577	attachments := o.Attachments()
578	inputAttachments := input.Attachments()
579
580	if len(attachments) != len(inputAttachments) {
581		return false
582	}
583
584	for i := range attachments {
585		if !attachments[i].Equals(inputAttachments[i]) {
586			return false
587		}
588	}
589
590	return true
591}
592
593var markdownDestinationEscaper = strings.NewReplacer(
594	`\`, `\\`,
595	`<`, `\<`,
596	`>`, `\>`,
597	`(`, `\(`,
598	`)`, `\)`,
599)
600
601// WithRewrittenImageURLs returns a new shallow copy of the post where the message has been
602// rewritten via RewriteImageURLs.
603func (o *Post) WithRewrittenImageURLs(f func(string) string) *Post {
604	copy := o.Clone()
605	copy.Message = RewriteImageURLs(o.Message, f)
606	if copy.MessageSource == "" && copy.Message != o.Message {
607		copy.MessageSource = o.Message
608	}
609	return copy
610}
611
612// RewriteImageURLs takes a message and returns a copy that has all of the image URLs replaced
613// according to the function f. For each image URL, f will be invoked, and the resulting markdown
614// will contain the URL returned by that invocation instead.
615//
616// Image URLs are destination URLs used in inline images or reference definitions that are used
617// anywhere in the input markdown as an image.
618func RewriteImageURLs(message string, f func(string) string) string {
619	if !strings.Contains(message, "![") {
620		return message
621	}
622
623	var ranges []markdown.Range
624
625	markdown.Inspect(message, func(blockOrInline interface{}) bool {
626		switch v := blockOrInline.(type) {
627		case *markdown.ReferenceImage:
628			ranges = append(ranges, v.ReferenceDefinition.RawDestination)
629		case *markdown.InlineImage:
630			ranges = append(ranges, v.RawDestination)
631		default:
632			return true
633		}
634		return true
635	})
636
637	if ranges == nil {
638		return message
639	}
640
641	sort.Slice(ranges, func(i, j int) bool {
642		return ranges[i].Position < ranges[j].Position
643	})
644
645	copyRanges := make([]markdown.Range, 0, len(ranges))
646	urls := make([]string, 0, len(ranges))
647	resultLength := len(message)
648
649	start := 0
650	for i, r := range ranges {
651		switch {
652		case i == 0:
653		case r.Position != ranges[i-1].Position:
654			start = ranges[i-1].End
655		default:
656			continue
657		}
658		original := message[r.Position:r.End]
659		replacement := markdownDestinationEscaper.Replace(f(markdown.Unescape(original)))
660		resultLength += len(replacement) - len(original)
661		copyRanges = append(copyRanges, markdown.Range{Position: start, End: r.Position})
662		urls = append(urls, replacement)
663	}
664
665	result := make([]byte, resultLength)
666
667	offset := 0
668	for i, r := range copyRanges {
669		offset += copy(result[offset:], message[r.Position:r.End])
670		offset += copy(result[offset:], urls[i])
671	}
672	copy(result[offset:], message[ranges[len(ranges)-1].End:])
673
674	return string(result)
675}
676
677func (o *Post) IsFromOAuthBot() bool {
678	props := o.GetProps()
679	return props["from_webhook"] == "true" && props["override_username"] != ""
680}
681
682func (o *Post) ToNilIfInvalid() *Post {
683	if o.Id == "" {
684		return nil
685	}
686	return o
687}
688
689func (o *Post) RemovePreviewPost() {
690	if o.Metadata == nil || o.Metadata.Embeds == nil {
691		return
692	}
693	n := 0
694	for _, embed := range o.Metadata.Embeds {
695		if embed.Type != PostEmbedPermalink {
696			o.Metadata.Embeds[n] = embed
697			n++
698		}
699	}
700	o.Metadata.Embeds = o.Metadata.Embeds[:n]
701}
702
703func (o *Post) GetPreviewPost() *PreviewPost {
704	for _, embed := range o.Metadata.Embeds {
705		if embed.Type == PostEmbedPermalink {
706			if previewPost, ok := embed.Data.(*PreviewPost); ok {
707				return previewPost
708			}
709		}
710	}
711	return nil
712}
713
714func (o *Post) GetPreviewedPostProp() string {
715	if val, ok := o.GetProp(PostPropsPreviewedPost).(string); ok {
716		return val
717	}
718	return ""
719}
720