status.go (15289B)
1 // GoToSocial 2 // Copyright (C) GoToSocial Authors admin@gotosocial.org 3 // SPDX-License-Identifier: AGPL-3.0-or-later 4 // 5 // This program is free software: you can redistribute it and/or modify 6 // it under the terms of the GNU Affero General Public License as published by 7 // the Free Software Foundation, either version 3 of the License, or 8 // (at your option) any later version. 9 // 10 // This program is distributed in the hope that it will be useful, 11 // but WITHOUT ANY WARRANTY; without even the implied warranty of 12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 // GNU Affero General Public License for more details. 14 // 15 // You should have received a copy of the GNU Affero General Public License 16 // along with this program. If not, see <http://www.gnu.org/licenses/>. 17 18 package gtsmodel 19 20 import ( 21 "time" 22 23 "github.com/superseriousbusiness/gotosocial/internal/log" 24 ) 25 26 // Status represents a user-created 'post' or 'status' in the database, either remote or local 27 type Status struct { 28 ID string `validate:"required,ulid" bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database 29 CreatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created 30 UpdatedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated 31 FetchedAt time.Time `validate:"required_with=!Local" bun:"type:timestamptz,nullzero"` // when was item (remote) last fetched. 32 PinnedAt time.Time `validate:"-" bun:"type:timestamptz,nullzero"` // Status was pinned by owning account at this time. 33 URI string `validate:"required,url" bun:",unique,nullzero,notnull"` // activitypub URI of this status 34 URL string `validate:"url" bun:",nullzero"` // web url for viewing this status 35 Content string `validate:"-" bun:""` // content of this status; likely html-formatted but not guaranteed 36 AttachmentIDs []string `validate:"dive,ulid" bun:"attachments,array"` // Database IDs of any media attachments associated with this status 37 Attachments []*MediaAttachment `validate:"-" bun:"attached_media,rel:has-many"` // Attachments corresponding to attachmentIDs 38 TagIDs []string `validate:"dive,ulid" bun:"tags,array"` // Database IDs of any tags used in this status 39 Tags []*Tag `validate:"-" bun:"attached_tags,m2m:status_to_tags"` // Tags corresponding to tagIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation 40 MentionIDs []string `validate:"dive,ulid" bun:"mentions,array"` // Database IDs of any mentions in this status 41 Mentions []*Mention `validate:"-" bun:"attached_mentions,rel:has-many"` // Mentions corresponding to mentionIDs 42 EmojiIDs []string `validate:"dive,ulid" bun:"emojis,array"` // Database IDs of any emojis used in this status 43 Emojis []*Emoji `validate:"-" bun:"attached_emojis,m2m:status_to_emojis"` // Emojis corresponding to emojiIDs. https://bun.uptrace.dev/guide/relations.html#many-to-many-relation 44 Local *bool `validate:"-" bun:",nullzero,notnull,default:false"` // is this status from a local account? 45 AccountID string `validate:"required,ulid" bun:"type:CHAR(26),nullzero,notnull"` // which account posted this status? 46 Account *Account `validate:"-" bun:"rel:belongs-to"` // account corresponding to accountID 47 AccountURI string `validate:"required,url" bun:",nullzero,notnull"` // activitypub uri of the owner of this status 48 InReplyToID string `validate:"required_with=InReplyToURI InReplyToAccountID,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the status this status replies to 49 InReplyToURI string `validate:"required_with=InReplyToID InReplyToAccountID,omitempty,url" bun:",nullzero"` // activitypub uri of the status this status is a reply to 50 InReplyToAccountID string `validate:"required_with=InReplyToID InReplyToURI,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the account that this status replies to 51 InReplyTo *Status `validate:"-" bun:"-"` // status corresponding to inReplyToID 52 InReplyToAccount *Account `validate:"-" bun:"rel:belongs-to"` // account corresponding to inReplyToAccountID 53 BoostOfID string `validate:"required_with=BoostOfAccountID,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the status this status is a boost of 54 BoostOfAccountID string `validate:"required_with=BoostOfID,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status 55 BoostOf *Status `validate:"-" bun:"-"` // status that corresponds to boostOfID 56 BoostOfAccount *Account `validate:"-" bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID 57 ContentWarning string `validate:"-" bun:",nullzero"` // cw string for this status 58 Visibility Visibility `validate:"oneof=public unlocked followers_only mutuals_only direct" bun:",nullzero,notnull"` // visibility entry for this status 59 Sensitive *bool `validate:"-" bun:",nullzero,notnull,default:false"` // mark the status as sensitive? 60 Language string `validate:"-" bun:",nullzero"` // what language is this status written in? 61 CreatedWithApplicationID string `validate:"required_if=Local true,omitempty,ulid" bun:"type:CHAR(26),nullzero"` // Which application was used to create this status? 62 CreatedWithApplication *Application `validate:"-" bun:"rel:belongs-to"` // application corresponding to createdWithApplicationID 63 ActivityStreamsType string `validate:"required" bun:",nullzero,notnull"` // What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types. Will probably almost always be Note but who knows!. 64 Text string `validate:"-" bun:""` // Original text of the status without formatting 65 Federated *bool `validate:"-" bun:",notnull"` // This status will be federated beyond the local timeline(s) 66 Boostable *bool `validate:"-" bun:",notnull"` // This status can be boosted/reblogged 67 Replyable *bool `validate:"-" bun:",notnull"` // This status can be replied to 68 Likeable *bool `validate:"-" bun:",notnull"` // This status can be liked/faved 69 } 70 71 // GetID implements timeline.Timelineable{}. 72 func (s *Status) GetID() string { 73 return s.ID 74 } 75 76 // GetAccountID implements timeline.Timelineable{}. 77 func (s *Status) GetAccountID() string { 78 return s.AccountID 79 } 80 81 // GetBoostID implements timeline.Timelineable{}. 82 func (s *Status) GetBoostOfID() string { 83 return s.BoostOfID 84 } 85 86 // GetBoostOfAccountID implements timeline.Timelineable{}. 87 func (s *Status) GetBoostOfAccountID() string { 88 return s.BoostOfAccountID 89 } 90 91 func (s *Status) GetAttachmentByID(id string) (*MediaAttachment, bool) { 92 for _, media := range s.Attachments { 93 if media == nil { 94 log.Warnf(nil, "nil attachment in slice for status %s", s.URI) 95 continue 96 } 97 if media.ID == id { 98 return media, true 99 } 100 } 101 return nil, false 102 } 103 104 func (s *Status) GetAttachmentByRemoteURL(url string) (*MediaAttachment, bool) { 105 for _, media := range s.Attachments { 106 if media == nil { 107 log.Warnf(nil, "nil attachment in slice for status %s", s.URI) 108 continue 109 } 110 if media.RemoteURL == url { 111 return media, true 112 } 113 } 114 return nil, false 115 } 116 117 // AttachmentsPopulated returns whether media attachments are populated according to current AttachmentIDs. 118 func (s *Status) AttachmentsPopulated() bool { 119 if len(s.AttachmentIDs) != len(s.Attachments) { 120 // this is the quickest indicator. 121 return false 122 } 123 for _, id := range s.AttachmentIDs { 124 if _, ok := s.GetAttachmentByID(id); !ok { 125 return false 126 } 127 } 128 return true 129 } 130 131 // TagsPopulated returns whether tags are populated according to current TagIDs. 132 func (s *Status) TagsPopulated() bool { 133 if len(s.TagIDs) != len(s.Tags) { 134 // this is the quickest indicator. 135 return false 136 } 137 138 // Tags must be in same order. 139 for i, id := range s.TagIDs { 140 if s.Tags[i] == nil { 141 log.Warnf(nil, "nil tag in slice for status %s", s.URI) 142 continue 143 } 144 if s.Tags[i].ID != id { 145 return false 146 } 147 } 148 149 return true 150 } 151 152 func (s *Status) GetMentionByID(id string) (*Mention, bool) { 153 for _, mention := range s.Mentions { 154 if mention == nil { 155 log.Warnf(nil, "nil mention in slice for status %s", s.URI) 156 continue 157 } 158 if mention.ID == id { 159 return mention, true 160 } 161 } 162 return nil, false 163 } 164 165 func (s *Status) GetMentionByTargetURI(uri string) (*Mention, bool) { 166 for _, mention := range s.Mentions { 167 if mention == nil { 168 log.Warnf(nil, "nil mention in slice for status %s", s.URI) 169 continue 170 } 171 if mention.TargetAccountURI == uri { 172 return mention, true 173 } 174 } 175 return nil, false 176 } 177 178 // MentionsPopulated returns whether mentions are populated according to current MentionIDs. 179 func (s *Status) MentionsPopulated() bool { 180 if len(s.MentionIDs) != len(s.Mentions) { 181 // this is the quickest indicator. 182 return false 183 } 184 for _, id := range s.MentionIDs { 185 if _, ok := s.GetMentionByID(id); !ok { 186 return false 187 } 188 } 189 return true 190 } 191 192 // EmojisPopulated returns whether emojis are populated according to current EmojiIDs. 193 func (s *Status) EmojisPopulated() bool { 194 if len(s.EmojiIDs) != len(s.Emojis) { 195 // this is the quickest indicator. 196 return false 197 } 198 199 // Emojis must be in same order. 200 for i, id := range s.EmojiIDs { 201 if s.Emojis[i] == nil { 202 log.Warnf(nil, "nil emoji in slice for status %s", s.URI) 203 continue 204 } 205 if s.Emojis[i].ID != id { 206 return false 207 } 208 } 209 210 return true 211 } 212 213 // EmojissUpToDate returns whether status emoji attachments of receiving status are up-to-date 214 // according to emoji attachments of the passed status, by comparing their emoji URIs. We don't 215 // use IDs as this is used to determine whether there are new emojis to fetch. 216 func (s *Status) EmojisUpToDate(other *Status) bool { 217 if len(s.Emojis) != len(other.Emojis) { 218 // this is the quickest indicator. 219 return false 220 } 221 222 // Emojis must be in same order. 223 for i := range s.Emojis { 224 if s.Emojis[i] == nil { 225 log.Warnf(nil, "nil emoji in slice for status %s", s.URI) 226 return false 227 } 228 229 if other.Emojis[i] == nil { 230 log.Warnf(nil, "nil emoji in slice for status %s", other.URI) 231 return false 232 } 233 234 if s.Emojis[i].URI != other.Emojis[i].URI { 235 // Emoji URI has changed, not up-to-date! 236 return false 237 } 238 } 239 240 return true 241 } 242 243 // MentionsAccount returns whether status mentions the given account ID. 244 func (s *Status) MentionsAccount(id string) bool { 245 for _, mention := range s.Mentions { 246 if mention.TargetAccountID == id { 247 return true 248 } 249 } 250 return false 251 } 252 253 // StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags. 254 type StatusToTag struct { 255 StatusID string `validate:"ulid,required" bun:"type:CHAR(26),unique:statustag,nullzero,notnull"` 256 Status *Status `validate:"-" bun:"rel:belongs-to"` 257 TagID string `validate:"ulid,required" bun:"type:CHAR(26),unique:statustag,nullzero,notnull"` 258 Tag *Tag `validate:"-" bun:"rel:belongs-to"` 259 } 260 261 // StatusToEmoji is an intermediate struct to facilitate the many2many relationship between a status and one or more emojis. 262 type StatusToEmoji struct { 263 StatusID string `validate:"ulid,required" bun:"type:CHAR(26),unique:statusemoji,nullzero,notnull"` 264 Status *Status `validate:"-" bun:"rel:belongs-to"` 265 EmojiID string `validate:"ulid,required" bun:"type:CHAR(26),unique:statusemoji,nullzero,notnull"` 266 Emoji *Emoji `validate:"-" bun:"rel:belongs-to"` 267 } 268 269 // Visibility represents the visibility granularity of a status. 270 type Visibility string 271 272 const ( 273 // VisibilityPublic means this status will be visible to everyone on all timelines. 274 VisibilityPublic Visibility = "public" 275 // VisibilityUnlocked means this status will be visible to everyone, but will only show on home timeline to followers, and in lists. 276 VisibilityUnlocked Visibility = "unlocked" 277 // VisibilityFollowersOnly means this status is viewable to followers only. 278 VisibilityFollowersOnly Visibility = "followers_only" 279 // VisibilityMutualsOnly means this status is visible to mutual followers only. 280 VisibilityMutualsOnly Visibility = "mutuals_only" 281 // VisibilityDirect means this status is visible only to mentioned recipients. 282 VisibilityDirect Visibility = "direct" 283 // VisibilityDefault is used when no other setting can be found. 284 VisibilityDefault Visibility = VisibilityUnlocked 285 )