mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 15:02:24 -05:00 
			
		
		
		
	moar work
This commit is contained in:
		
					parent
					
						
							
								1710158b39
							
						
					
				
			
			
				commit
				
					
						d1ca4a1219
					
				
			
		
					 5 changed files with 120 additions and 40 deletions
				
			
		|  | @ -87,6 +87,9 @@ func (m *statusModule) CreateTables(db db.DB) error { | ||||||
| 		&model.Application{}, | 		&model.Application{}, | ||||||
| 		&model.EmailDomainBlock{}, | 		&model.EmailDomainBlock{}, | ||||||
| 		&model.MediaAttachment{}, | 		&model.MediaAttachment{}, | ||||||
|  | 		&model.Emoji{}, | ||||||
|  | 		&model.Tag{}, | ||||||
|  | 		&model.Mention{}, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for _, m := range models { | 	for _, m := range models { | ||||||
|  |  | ||||||
|  | @ -27,7 +27,6 @@ import ( | ||||||
| 	"github.com/gin-gonic/gin" | 	"github.com/gin-gonic/gin" | ||||||
| 	"github.com/google/uuid" | 	"github.com/google/uuid" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" |  | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db/model" | 	"github.com/superseriousbusiness/gotosocial/internal/db/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | 	"github.com/superseriousbusiness/gotosocial/internal/distributor" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | @ -62,13 +61,15 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// check this user/account is permitted to post new statuses | 	// First check this user/account is permitted to post new statuses. | ||||||
|  | 	// There's no point continuing otherwise. | ||||||
| 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | 	if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { | ||||||
| 		l.Debugf("couldn't auth: %s", err) | 		l.Debugf("couldn't auth: %s", err) | ||||||
| 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | 		c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Give the fields on the request form a first pass to make sure the request is superficially valid. | ||||||
| 	l.Trace("parsing request form") | 	l.Trace("parsing request form") | ||||||
| 	form := &advancedStatusCreateForm{} | 	form := &advancedStatusCreateForm{} | ||||||
| 	if err := c.ShouldBind(form); err != nil || form == nil { | 	if err := c.ShouldBind(form); err != nil || form == nil { | ||||||
|  | @ -76,24 +77,70 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | 		c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"}) | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 |  | ||||||
| 	l.Tracef("validating form %+v", form) | 	l.Tracef("validating form %+v", form) | ||||||
| 	if err := validateCreateStatus(form, m.config.StatusesConfig, authed.Account.ID, m.db); err != nil { | 	if err := validateCreateStatus(form, m.config.StatusesConfig); 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 | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// At this point we know the account is permitted to post, and we know the request form | ||||||
|  | 	// is valid (at least according to the API specifications and the instance configuration). | ||||||
|  | 	// So now we can start digging a bit deeper into the status itself. | ||||||
|  | 
 | ||||||
|  | 	// If this status is a reply to another status, we need to do a bit of work to establish whether or not this status can be posted: | ||||||
|  | 	// | ||||||
|  | 	// 1. Does the replied status exist in the database? | ||||||
|  | 	// 2. Is the replied status marked as replyable? | ||||||
|  | 	// 3. Does a block exist between either the current account or the account that posted the status it's replying to? | ||||||
|  | 	// | ||||||
|  | 	// If this is all OK, then we fetch the repliedStatus and the repliedAccount for later processing. | ||||||
|  | 	repliedStatus := &model.Status{} | ||||||
|  | 	repliedAccount := &model.Account{} | ||||||
|  | 	if form.InReplyToID != "" { | ||||||
|  | 		// check replied status exists + is replyable | ||||||
|  | 		if err := m.db.GetByID(form.InReplyToID, repliedStatus); err != nil || !repliedStatus.VisibilityAdvanced.Replyable { | ||||||
|  | 			l.Debugf("status id %s cannot be retrieved from the db: %s", form.InReplyToID, err) | ||||||
|  | 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("status with id %s not replyable", form.InReplyToID)}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// check replied account is known to us | ||||||
|  | 		if err := m.db.GetByID(repliedStatus.AccountID, repliedAccount); err != nil { | ||||||
|  | 			l.Debugf("error getting account with id %s from the database: %s", repliedStatus.AccountID, err) | ||||||
|  | 			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("status with id %s not replyable", form.InReplyToID)}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// check if a block exists | ||||||
|  | 		if blocked, err := m.db.Blocked(authed.Account.ID, repliedAccount.ID); err != nil || blocked { | ||||||
|  | 			c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("status with id %s not replyable", form.InReplyToID)}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	attachments := []*model.MediaAttachment{} | ||||||
|  | 	for _, mediaID := range form.MediaIDs { | ||||||
|  | 		// check these attachments exist | ||||||
|  | 		a := &model.MediaAttachment{} | ||||||
|  | 		if err := m.db.GetByID(mediaID, a); err != nil { | ||||||
|  | 			l.Debugf("invalid media type or media not found for media id %s: %s", m, err) | ||||||
|  | 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("invalid media type or media not found for media id %s", mediaID)}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		// check they belong to the requesting account id | ||||||
|  | 		if a.AccountID != authed.Account.ID { | ||||||
|  | 			l.Debugf("media attachment %s does not belong to account id %s", m, authed.Account.ID) | ||||||
|  | 			c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("media with id %s does not belong to account %s", mediaID, authed.Account.ID)}) | ||||||
|  | 			return | ||||||
|  | 		} | ||||||
|  | 		attachments = append(attachments, a) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// here we check if any advanced visibility flags have been set and fiddle with them if so | 	// here we check if any advanced visibility flags have been set and fiddle with them if so | ||||||
| 	l.Trace("deriving visibility") | 	l.Trace("deriving visibility") | ||||||
| 	basicVis, advancedVis, err := deriveTotalVisibility(form.Visibility, form.AdvancedVisibility, authed.Account.Privacy) | 	basicVis, advancedVis, err := deriveTotalVisibility(form.Visibility, form.AdvancedVisibility, authed.Account.Privacy) | ||||||
| 
 | 	if err != nil { | ||||||
| 	clientIP := c.ClientIP() | 		l.Debugf("error parsing visibility: %s", err) | ||||||
| 	l.Tracef("attempting to parse client ip address %s", clientIP) | 		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) | ||||||
| 	signUpIP := net.ParseIP(clientIP) |  | ||||||
| 	if signUpIP == nil { |  | ||||||
| 		l.Debugf("error validating client ip address %s", clientIP) |  | ||||||
| 		c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) |  | ||||||
| 		return | 		return | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -142,17 +189,31 @@ func (m *statusModule) statusCreatePOSTHandler(c *gin.Context) { | ||||||
| 
 | 
 | ||||||
| 	// take care of side effects -- federation, mentions, updating metadata, etc, etc | 	// take care of side effects -- federation, mentions, updating metadata, etc, etc | ||||||
| 	m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | 	m.distributor.FromClientAPI() <- distributor.FromClientAPI{ | ||||||
| 		APObjectType: model.ActivityStreamsNote, | 		APObjectType:   model.ActivityStreamsNote, | ||||||
| 		APActivityType: model.ActivityStreamsCreate, | 		APActivityType: model.ActivityStreamsCreate, | ||||||
| 		Activity: newStatus, | 		Activity:       newStatus, | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// return populated status to submitter | 	// return populated status to submitter | ||||||
| 	 | 	// mastoStatus := &mastotypes.Status{ | ||||||
|  | 	// 	ID:                 newStatus.ID, | ||||||
|  | 	// 	CreatedAt:          time.Now().Format(time.RFC3339), | ||||||
|  | 	// 	InReplyToID:        newStatus.InReplyToID, | ||||||
|  | 	// 	InReplyToAccountID: newStatus.InReplyToAccountID, | ||||||
|  | 	// } | ||||||
|  | 
 | ||||||
|  | 	clientIP := c.ClientIP() | ||||||
|  | 	l.Tracef("attempting to parse client ip address %s", clientIP) | ||||||
|  | 	signUpIP := net.ParseIP(clientIP) | ||||||
|  | 	if signUpIP == nil { | ||||||
|  | 		l.Debugf("error validating client ip address %s", clientIP) | ||||||
|  | 		c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"}) | ||||||
|  | 		return | ||||||
|  | 	} | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig, accountID string, db db.DB) error { | func validateCreateStatus(form *advancedStatusCreateForm, config *config.StatusesConfig) error { | ||||||
| 	// validate that, structurally, we have a valid status/post | 	// validate that, structurally, we have a valid status/post | ||||||
| 	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") | ||||||
|  | @ -174,18 +235,6 @@ func validateCreateStatus(form *advancedStatusCreateForm, config *config.Statuse | ||||||
| 		return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", 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 | 	// validate poll | ||||||
| 	if form.Poll != nil { | 	if form.Poll != nil { | ||||||
| 		if form.Poll.Options == nil { | 		if form.Poll.Options == nil { | ||||||
|  | @ -201,17 +250,6 @@ func validateCreateStatus(form *advancedStatusCreateForm, config *config.Statuse | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// validate reply-to status exists and is reply-able |  | ||||||
| 	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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	// validate spoiler text/cw | 	// validate spoiler text/cw | ||||||
| 	if form.SpoilerText != "" { | 	if form.SpoilerText != "" { | ||||||
| 		if len(form.SpoilerText) > config.CWMaxChars { | 		if len(form.SpoilerText) > config.CWMaxChars { | ||||||
|  |  | ||||||
|  | @ -174,6 +174,10 @@ type DB interface { | ||||||
| 	// The passed mediaAttachment pointer will be populated with the value of the header, if it exists. | 	// The passed mediaAttachment pointer will be populated with the value of the header, if it exists. | ||||||
| 	GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error | 	GetHeaderForAccountID(header *model.MediaAttachment, accountID string) error | ||||||
| 
 | 
 | ||||||
|  | 	// Blocked checks whether a block exists in eiher direction between two accounts. | ||||||
|  | 	// That is, it returns true if account1 blocks account2, OR if account2 blocks account1. | ||||||
|  | 	Blocked(account1 string, account2 string) (bool, error) | ||||||
|  | 
 | ||||||
| 	/* | 	/* | ||||||
| 		USEFUL CONVERSION FUNCTIONS | 		USEFUL CONVERSION FUNCTIONS | ||||||
| 	*/ | 	*/ | ||||||
|  |  | ||||||
							
								
								
									
										19
									
								
								internal/db/model/block.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								internal/db/model/block.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,19 @@ | ||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | import "time" | ||||||
|  | 
 | ||||||
|  | // Block refers to the blocking of one account by another. | ||||||
|  | type Block struct { | ||||||
|  | 	// id of this block in the database | ||||||
|  | 	ID string `pg:"type:uuid,default:gen_random_uuid(),pk,notnull"` | ||||||
|  | 	// When was this block created | ||||||
|  | 	CreatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
|  | 	// When was this block updated | ||||||
|  | 	UpdatedAt time.Time `pg:"type:timestamp,notnull,default:now()"` | ||||||
|  | 	// Who created this block? | ||||||
|  | 	AccountID string `pg:",notnull"` | ||||||
|  | 	// Who is targeted by this block? | ||||||
|  | 	TargetAccountID string `pg:",notnull"` | ||||||
|  | 	// Activitypub URI for this block | ||||||
|  | 	URI string | ||||||
|  | } | ||||||
|  | @ -511,6 +511,22 @@ func (ps *postgresService) GetAvatarForAccountID(avatar *model.MediaAttachment, | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func (ps *postgresService) Blocked(account1 string, account2 string) (bool, error) { | ||||||
|  | 	var blocked bool | ||||||
|  | 	if err := ps.conn.Model(&model.Block{}). | ||||||
|  | 		Where("account_id = ?", account1).Where("target_account_id = ?", account2). | ||||||
|  | 		WhereOr("target_account_id = ?", account1).Where("account_id = ?", account2). | ||||||
|  | 		Select(); err != nil { | ||||||
|  | 		if err == pg.ErrNoRows { | ||||||
|  | 			blocked = false | ||||||
|  | 		} else { | ||||||
|  | 			return blocked, err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	blocked = true | ||||||
|  | 	return blocked, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| /* | /* | ||||||
| 	CONVERSION FUNCTIONS | 	CONVERSION FUNCTIONS | ||||||
| */ | */ | ||||||
|  | @ -712,7 +728,7 @@ func (ps *postgresService) MentionStringsToMentions(targetAccounts []string, ori | ||||||
| 
 | 
 | ||||||
| 		// id, createdAt and updatedAt will be populated by the db, so we have everything we need! | 		// id, createdAt and updatedAt will be populated by the db, so we have everything we need! | ||||||
| 		menchies = append(menchies, &model.Mention{ | 		menchies = append(menchies, &model.Mention{ | ||||||
| 			StatusID: statusID, | 			StatusID:        statusID, | ||||||
| 			OriginAccountID: originAccountID, | 			OriginAccountID: originAccountID, | ||||||
| 			TargetAccountID: mentionedAccount.ID, | 			TargetAccountID: mentionedAccount.ID, | ||||||
| 		}) | 		}) | ||||||
|  | @ -745,7 +761,7 @@ func (ps *postgresService) EmojiStringsToEmojis(emojis []string, originAccountID | ||||||
| 				continue | 				continue | ||||||
| 			} | 			} | ||||||
| 			// a serious error has happened so bail | 			// a serious error has happened so bail | ||||||
| 			return nil, fmt.Errorf("error getting emoji with shortcode %s: %s",e, err) | 			return nil, fmt.Errorf("error getting emoji with shortcode %s: %s", e, err) | ||||||
| 		} | 		} | ||||||
| 		newEmojis = append(newEmojis, emoji) | 		newEmojis = append(newEmojis, emoji) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue