mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 16:12:24 -05:00 
			
		
		
		
	additional work on statuses
This commit is contained in:
		
					parent
					
						
							
								6705326752
							
						
					
				
			
			
				commit
				
					
						0b0f3d9e9a
					
				
			
		
					 7 changed files with 178 additions and 49 deletions
				
			
		|  | @ -27,6 +27,7 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/db/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/pkg/mastotypes" | 	"github.com/superseriousbusiness/gotosocial/pkg/mastotypes" | ||||||
|  | @ -57,7 +58,7 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	l.Tracef("validating form %+v", form) | 	l.Tracef("validating form %+v", form) | ||||||
| 	if err := validateCreateStatus(form, m.config.StatusesConfig, m.db); err != nil { | 	if err := validateCreateStatus(form, m.config.StatusesConfig, authed.Account.ID, m.db); err != nil { | ||||||
| 		l.Debugf("error validating form: %s", err) | 		l.Debugf("error validating form: %s", err) | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
| 		return | 		return | ||||||
|  | @ -71,16 +72,15 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) | 		c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
|  | 
 | ||||||
|  | 	// newStatus := &model.Status{ | ||||||
|  | 
 | ||||||
|  | 	// } | ||||||
|  | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.StatusesConfig, db db.DB) error { | func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.StatusesConfig, accountID string, db db.DB) error { | ||||||
| 
 | 	// validate that, structurally, we have a valid status/post | ||||||
| 	if form.Language != "" { |  | ||||||
| 		if err := util.ValidateLanguage(form.Language); err != nil { |  | ||||||
| 			return err |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | 	if form.Status == "" && form.MediaIDs == nil && form.Poll == nil { | ||||||
| 		return errors.New("no status, media, or poll provided") | 		return errors.New("no status, media, or poll provided") | ||||||
| 	} | 	} | ||||||
|  | @ -89,6 +89,31 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S | ||||||
| 		return errors.New("can't post media + poll in same status") | 		return errors.New("can't post media + poll in same status") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// validate status | ||||||
|  | 	if form.Status != "" { | ||||||
|  | 		if len(form.Status) > config.MaxChars { | ||||||
|  | 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// validate media attachments | ||||||
|  | 	if len(form.MediaIDs) > config.MaxMediaFiles { | ||||||
|  | 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, m := range form.MediaIDs { | ||||||
|  | 		// check these attachments exist | ||||||
|  | 		a := &model.MediaAttachment{} | ||||||
|  | 		if err := db.GetByID(m, a); err != nil { | ||||||
|  | 			return fmt.Errorf("invalid media type or media not found for media id %s: %s", m, err) | ||||||
|  | 		} | ||||||
|  | 		// check they belong to the requesting account id | ||||||
|  | 		if a.AccountID != accountID { | ||||||
|  | 			return fmt.Errorf("media attachment %s does not belong to account id %s", m, accountID) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// validate poll | ||||||
| 	if form.Poll != nil { | 	if form.Poll != nil { | ||||||
| 		if form.Poll.Options == nil { | 		if form.Poll.Options == nil { | ||||||
| 			return errors.New("poll with no options") | 			return errors.New("poll with no options") | ||||||
|  | @ -103,13 +128,28 @@ func validateCreateStatus(form *mastotypes.StatusCreateRequest, config *config.S | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if len(form.MediaIDs) > config.MaxMediaFiles { | 	// validate reply-to status exists and is reply-able | ||||||
| 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), config.MaxMediaFiles) | 	if form.InReplyToID != "" { | ||||||
|  | 		s := &model.Status{} | ||||||
|  | 		if err := db.GetByID(form.InReplyToID, s); err != nil { | ||||||
|  | 			return fmt.Errorf("status id %s cannot be retrieved from the db: %s", form.InReplyToID, err) | ||||||
|  | 		} | ||||||
|  | 		if !*s.VisibilityAdvanced.Replyable { | ||||||
|  | 			return fmt.Errorf("status with id %s is not replyable", form.InReplyToID) | ||||||
|  | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if form.Status != "" { | 	// validate spoiler text/cw | ||||||
| 		if len(form.Status) > config.MaxChars { | 	if form.SpoilerText != "" { | ||||||
| 			return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), config.MaxChars) | 		if len(form.SpoilerText) > config.CWMaxChars { | ||||||
|  | 			return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), config.CWMaxChars) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	// validate post language | ||||||
|  | 	if form.Language != "" { | ||||||
|  | 		if err := util.ValidateLanguage(form.Language); err != nil { | ||||||
|  | 			return err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
							
								
								
									
										39
									
								
								internal/db/model/mention.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/db/model/mention.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | // Mention refers to the 'tagging' or 'mention' of a user within a status. | ||||||
|  | type Mention struct { | ||||||
|  | 	// ID of this mention in the database | ||||||
|  | 	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull,unique"` | ||||||
|  | 	// ID of the status this mention originates from | ||||||
|  | 	StatusID string | ||||||
|  | 	// When was this mention created? | ||||||
|  | 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
|  | 	// When was this mention last updated? | ||||||
|  | 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
|  | 	// Who created this mention? | ||||||
|  | 	OriginAccountID string | ||||||
|  | 	// Who does this mention target? | ||||||
|  | 	TargetAccountID string | ||||||
|  | 	// Prevent this mention from generating a notification? | ||||||
|  | 	Silent bool | ||||||
|  | } | ||||||
|  | @ -45,22 +45,45 @@ type Status struct { | ||||||
| 	// cw string for this status | 	// cw string for this status | ||||||
| 	ContentWarning string | 	ContentWarning string | ||||||
| 	// visibility entry for this status | 	// visibility entry for this status | ||||||
| 	Visibility          *Visibility | 	Visibility Visibility | ||||||
|  | 	// advanced visibility for this status | ||||||
|  | 	VisibilityAdvanced VisibilityAdvanced | ||||||
| 	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types | 	// What is the activitystreams type of this status? See: https://www.w3.org/TR/activitystreams-vocabulary/#object-types | ||||||
| 	// Will probably almost always be a note. | 	// Will probably almost always be a note. | ||||||
| 	ActivityStreamsType string | 	ActivityStreamsType string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // Visibility represents the visibility granularity of a status. It is a combination of flags. | // Visibility represents the visibility granularity of a status. | ||||||
| type Visibility struct { | type Visibility string | ||||||
| 	// Is this status viewable as a direct message? | 
 | ||||||
| 	Direct bool | const ( | ||||||
| 	// Is this status viewable to followers? | 	// This status will be visible to everyone on all timelines. | ||||||
| 	Followers bool | 	VisibilityPublic Visibility = "public" | ||||||
| 	// Is this status viewable on the local timeline? | 	// This status will be visible to everyone, but will only show on home timeline to followers, and in lists. | ||||||
| 	Local bool | 	VisibilityUnlocked Visibility = "unlocked" | ||||||
| 	// Is this status boostable but not shown on public timelines? | 	// This status is viewable to followers only. | ||||||
| 	Unlisted bool | 	VisibilityFollowersOnly Visibility = "followers_only" | ||||||
| 	// Is this status shown on public and federated timelines? | 	// This status is visible to mutual followers only. | ||||||
| 	Public bool | 	VisibilityMutualsOnly Visibility = "mutuals_only" | ||||||
|  | 	// This status is visible only to mentioned recipients | ||||||
|  | 	VisibilityDirect Visibility = "direct" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | type VisibilityAdvanced struct { | ||||||
|  | 	/* | ||||||
|  | 		ADVANCED SETTINGS -- These should all default to TRUE. | ||||||
|  | 
 | ||||||
|  | 		If PUBLIC is selected, they will all be overwritten to TRUE regardless of what is selected. | ||||||
|  | 		If UNLOCKED is selected, any of them can be turned on or off in any combination. | ||||||
|  | 		If FOLLOWERS-ONLY or MUTUALS-ONLY are selected, boostable will always be FALSE. The others can be turned on or off as desired. | ||||||
|  | 		If DIRECT is selected, boostable will be FALSE, and all other flags will be TRUE. | ||||||
|  | 	*/ | ||||||
|  | 	// This status will be federated beyond the local timeline(s) | ||||||
|  | 	Federated *bool `pg:"default:true"` | ||||||
|  | 	// This status can be boosted/reblogged | ||||||
|  | 	Boostable *bool `pg:"default:true"` | ||||||
|  | 	// This status can be replied to | ||||||
|  | 	Replyable *bool `pg:"default:true"` | ||||||
|  | 	// This status can be liked/faved | ||||||
|  | 	Likeable *bool `pg:"default:true"` | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,3 +1,21 @@ | ||||||
|  | /* | ||||||
|  |    GoToSocial | ||||||
|  |    Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org | ||||||
|  | 
 | ||||||
|  |    This program is free software: you can redistribute it and/or modify | ||||||
|  |    it under the terms of the GNU Affero General Public License as published by | ||||||
|  |    the Free Software Foundation, either version 3 of the License, or | ||||||
|  |    (at your option) any later version. | ||||||
|  | 
 | ||||||
|  |    This program is distributed in the hope that it will be useful, | ||||||
|  |    but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  |    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  |    GNU Affero General Public License for more details. | ||||||
|  | 
 | ||||||
|  |    You should have received a copy of the GNU Affero General Public License | ||||||
|  |    along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | */ | ||||||
|  | 
 | ||||||
| package util | package util | ||||||
| 
 | 
 | ||||||
| import "fmt" | import "fmt" | ||||||
|  |  | ||||||
|  | @ -33,11 +33,7 @@ type Status struct { | ||||||
| 	// Subject or summary line, below which status content is collapsed until expanded. | 	// Subject or summary line, below which status content is collapsed until expanded. | ||||||
| 	SpoilerText string `json:"spoiler_text"` | 	SpoilerText string `json:"spoiler_text"` | ||||||
| 	// Visibility of this status. | 	// Visibility of this status. | ||||||
| 	// 	public = Visible to everyone, shown in public timelines. | 	Visibility Visibility `json:"visibility"` | ||||||
| 	// 	unlisted = Visible to public, but not included in public timelines. |  | ||||||
| 	// 	private = Visible to followers only, and to any mentioned users. |  | ||||||
| 	// 	direct = Visible only to mentioned users. |  | ||||||
| 	Visibility string `json:"visibility"` |  | ||||||
| 	// Primary language of this status. (ISO 639 Part 1 two-letter language code) | 	// Primary language of this status. (ISO 639 Part 1 two-letter language code) | ||||||
| 	Language string `json:"language"` | 	Language string `json:"language"` | ||||||
| 	// URI of the status used for federation. | 	// URI of the status used for federation. | ||||||
|  | @ -102,9 +98,22 @@ type StatusCreateRequest struct { | ||||||
| 	// Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. | 	// Text to be shown as a warning or subject before the actual content. Statuses are generally collapsed behind this field. | ||||||
| 	SpoilerText string `form:"spoiler_text"` | 	SpoilerText string `form:"spoiler_text"` | ||||||
| 	// Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. | 	// Visibility of the posted status. Enumerable oneOf public, unlisted, private, direct. | ||||||
| 	Visibility string `form:"visibility"` | 	Visibility Visibility `form:"visibility"` | ||||||
| 	// ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. | 	// ISO 8601 Datetime at which to schedule a status. Providing this paramter will cause ScheduledStatus to be returned instead of Status. Must be at least 5 minutes in the future. | ||||||
| 	ScheduledAt string `form:"scheduled_at"` | 	ScheduledAt string `form:"scheduled_at"` | ||||||
| 	// ISO 639 language code for this status. | 	// ISO 639 language code for this status. | ||||||
| 	Language string `form:"language"` | 	Language string `form:"language"` | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | type Visibility string | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// visible to everyone | ||||||
|  | 	VisibilityPublic Visibility = "public" | ||||||
|  | 	// visible to everyone but only on home timelines or in lists | ||||||
|  | 	VisibilityUnlisted Visibility = "unlisted" | ||||||
|  | 	// visible to followers only | ||||||
|  | 	VisibilityPrivate Visibility = "private" | ||||||
|  | 	// visible only to tagged recipients | ||||||
|  | 	VisibilityDirect Visibility = "direct" | ||||||
|  | ) | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue