Merge branch 'superseriousbusiness:main' into rss-fixes1

This commit is contained in:
pnwmatt 2025-02-26 19:09:00 -08:00 committed by GitHub
commit ee3978e86c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
689 changed files with 922580 additions and 199098 deletions

View file

@ -27,6 +27,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
@ -34,14 +35,13 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/uris"
"golang.org/x/crypto/bcrypt"
)
func (p *Processor) MoveSelf(
ctx context.Context,
authed *oauth.Auth,
authed *apiutil.Auth,
form *apimodel.AccountMoveRequest,
) gtserror.WithCode {
// Ensure valid MovedToURI.

View file

@ -24,6 +24,7 @@ import (
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
@ -56,7 +57,7 @@ func (suite *MoveTestSuite) TestMoveAccountOK() {
// Trigger move from zork to admin.
if err := suite.accountProcessor.MoveSelf(
ctx,
&oauth.Auth{
&apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
@ -120,7 +121,7 @@ func (suite *MoveTestSuite) TestMoveAccountNotAliased() {
// not aliased back to zork.
err := suite.accountProcessor.MoveSelf(
ctx,
&oauth.Auth{
&apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],
@ -150,7 +151,7 @@ func (suite *MoveTestSuite) TestMoveAccountBadPassword() {
// not aliased back to zork.
err := suite.accountProcessor.MoveSelf(
ctx,
&oauth.Auth{
&apiutil.Auth{
Token: oauth.DBTokenToToken(suite.testTokens["local_account_1"]),
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],

View file

@ -22,13 +22,13 @@ import (
"github.com/google/uuid"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
)
func (p *Processor) AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
func (p *Processor) AppCreate(ctx context.Context, authed *apiutil.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) {
// set default 'read' for scopes if it's not set
var scopes string
if form.Scopes == "" {

View file

@ -93,7 +93,7 @@ func (p *Processor) PollVote(ctx context.Context, requester *gtsmodel.Account, p
// Before enqueuing it, increment the poll
// vote counts on the copy attached to the
// PollVote (that we also later return).
poll.IncrementVotes(choices)
poll.IncrementVotes(choices, true)
// Enqueue worker task to handle side-effects of user poll vote(s).
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{

View file

@ -22,6 +22,7 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/admin"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
@ -66,7 +67,7 @@ type ProcessingStandardTestSuite struct {
testStatuses map[string]*gtsmodel.Status
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
testAutheds map[string]*oauth.Auth
testAutheds map[string]*apiutil.Auth
testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
testLists map[string]*gtsmodel.List
@ -85,7 +86,7 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
suite.testAutheds = map[string]*oauth.Auth{
suite.testAutheds = map[string]*apiutil.Auth{
"local_account_1": {
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],

View file

@ -442,6 +442,33 @@ func (p *Processor) WebContextGet(
_, parentHidden := hiddenStatuses[status.InReplyToID]
v, err := p.visFilter.StatusVisible(ctx, nil, status)
if err != nil || !v || parentHidden {
// If this is the main status whose
// context we're looking for, and it's
// not visible for whatever reason, we
// should just return a 404 here, as we
// can't meaningfully render the thread.
if status.ID == targetStatusID {
var thisErr error
switch {
case err != nil:
thisErr = gtserror.Newf("error checking visibility of target status: %w", err)
case !v:
const errText = "target status not visible"
thisErr = gtserror.New(errText)
case parentHidden:
const errText = "target status parent is hidden"
thisErr = gtserror.New(errText)
}
return nil, gtserror.NewErrorNotFound(thisErr)
}
// This isn't the main status whose
// context we're looking for, just
// your standard not-visible status,
// so add it to the count + map.
if !inReplies {
// Main thread entry hidden.
wCtx.ThreadHidden++

View file

@ -19,10 +19,14 @@ package status
import (
"context"
"errors"
"time"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
@ -92,11 +96,58 @@ func (p *Processor) Create(
// Get current time.
now := time.Now()
// Default to current
// time as creation time.
createdAt := now
// Handle backfilled/scheduled statuses.
backfill := false
if form.ScheduledAt != nil {
scheduledAt := *form.ScheduledAt
// Statuses may only be scheduled
// a minimum time into the future.
if now.Before(scheduledAt) {
const errText = "scheduled statuses are not yet supported"
return nil, gtserror.NewErrorNotImplemented(gtserror.New(errText), errText)
}
// If not scheduled into the future, this status is being backfilled.
if !config.GetInstanceAllowBackdatingStatuses() {
const errText = "backdating statuses has been disabled on this instance"
return nil, gtserror.NewErrorForbidden(gtserror.New(errText), errText)
}
// Statuses can't be backdated to or before the UNIX epoch
// since this would prevent generating a ULID.
// If backdated even further to the Go epoch,
// this would also cause issues with time.Time.IsZero() checks
// that normally signify an absent optional time,
// but this check covers both cases.
if scheduledAt.Compare(time.UnixMilli(0)) <= 0 {
const errText = "statuses can't be backdated to or before the UNIX epoch"
return nil, gtserror.NewErrorNotAcceptable(gtserror.New(errText), errText)
}
var err error
// This is a backfill.
backfill = true
// Update to backfill date.
createdAt = scheduledAt
// Generate an appropriate, (and unique!), ID for the creation time.
if statusID, err = p.backfilledStatusID(ctx, createdAt); err != nil {
return nil, gtserror.NewErrorInternalError(err)
}
}
status := &gtsmodel.Status{
ID: statusID,
URI: accountURIs.StatusesURI + "/" + statusID,
URL: accountURIs.StatusesURL + "/" + statusID,
CreatedAt: now,
CreatedAt: createdAt,
Local: util.Ptr(true),
Account: requester,
AccountID: requester.ID,
@ -134,11 +185,23 @@ func (p *Processor) Create(
PendingApproval: util.Ptr(false),
}
if backfill {
// Ensure backfilled status contains no
// mentions to anyone other than author.
for _, mention := range status.Mentions {
if mention.TargetAccountID != requester.ID {
const errText = "statuses mentioning others can't be backfilled"
return nil, gtserror.NewErrorForbidden(gtserror.New(errText), errText)
}
}
}
// Check + attach in-reply-to status.
if errWithCode := p.processInReplyTo(ctx,
requester,
status,
form.InReplyToID,
backfill,
); errWithCode != nil {
return nil, errWithCode
}
@ -165,11 +228,16 @@ func (p *Processor) Create(
}
if form.Poll != nil {
if backfill {
const errText = "statuses with polls can't be backfilled"
return nil, gtserror.NewErrorForbidden(gtserror.New(errText), errText)
}
// Process poll, inserting into database.
poll, errWithCode := p.processPoll(ctx,
statusID,
form.Poll,
now,
createdAt,
)
if errWithCode != nil {
return nil, errWithCode
@ -199,11 +267,18 @@ func (p *Processor) Create(
}
}
var model any = status
if backfill {
// We specifically wrap backfilled statuses in
// a different type to signal to worker process.
model = &gtsmodel.BackfillStatus{Status: status}
}
// Send it to the client API worker for async side-effects.
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
GTSModel: model,
Origin: requester,
})
@ -227,7 +302,49 @@ func (p *Processor) Create(
return p.c.GetAPIStatus(ctx, requester, status)
}
func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Account, status *gtsmodel.Status, inReplyToID string) gtserror.WithCode {
// backfilledStatusID tries to find an unused ULID for a backfilled status.
func (p *Processor) backfilledStatusID(ctx context.Context, createdAt time.Time) (string, error) {
// Any fetching of statuses here is
// only to check availability of ID,
// no need for any attached models.
ctx = gtscontext.SetBarebones(ctx)
// backfilledStatusIDRetries should
// be more than enough attempts.
const backfilledStatusIDRetries = 100
for try := 0; try < backfilledStatusIDRetries; try++ {
var err error
// Generate a ULID based on the backfilled
// status's original creation time.
statusID := id.NewULIDFromTime(createdAt)
// Check for an existing status with that ID.
status, err := p.state.DB.GetStatusByID(ctx, statusID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return "", gtserror.Newf("DB error checking if a status ID was in use: %w", err)
}
if status == nil {
// We found a free ID!
return statusID, nil
}
// That status ID is
// in use. Try again.
}
return "", gtserror.Newf("failed to find an unused ID after %d tries", backfilledStatusIDRetries)
}
func (p *Processor) processInReplyTo(
ctx context.Context,
requester *gtsmodel.Account,
status *gtsmodel.Status,
inReplyToID string,
backfill bool,
) gtserror.WithCode {
if inReplyToID == "" {
// Not a reply.
// Nothing to do.
@ -269,6 +386,13 @@ func (p *Processor) processInReplyTo(ctx context.Context, requester *gtsmodel.Ac
return gtserror.NewErrorForbidden(err, errText)
}
// When backfilling, only self-replies are allowed.
if backfill && requester.ID != inReplyTo.AccountID {
const errText = "replies to others can't be backfilled"
err := gtserror.New(errText)
return gtserror.NewErrorForbidden(err, errText)
}
// Derive pendingApproval status.
var pendingApproval bool
switch {

View file

@ -48,7 +48,7 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithQuotationMarks(
SpoilerText: "\"test\"", // these should not be html-escaped when the final text is rendered
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypePlain,
}
@ -75,7 +75,7 @@ func (suite *StatusCreateTestSuite) TestProcessContentWarningWithHTMLEscapedQuot
SpoilerText: "&#34test&#34", // the html-escaped quotation marks should appear as normal quotation marks in the finished text
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypePlain,
}
@ -106,7 +106,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithUnderscoreEmoji
Sensitive: false,
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypeMarkdown,
}
@ -133,7 +133,7 @@ func (suite *StatusCreateTestSuite) TestProcessStatusMarkdownWithSpoilerTextEmoj
Sensitive: false,
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypeMarkdown,
}
@ -164,7 +164,7 @@ func (suite *StatusCreateTestSuite) TestProcessMediaDescriptionTooShort() {
SpoilerText: "",
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypePlain,
}
@ -189,7 +189,7 @@ func (suite *StatusCreateTestSuite) TestProcessLanguageWithScriptPart() {
SpoilerText: "",
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "zh-Hans",
ContentType: apimodel.StatusContentTypePlain,
}
@ -219,7 +219,7 @@ func (suite *StatusCreateTestSuite) TestProcessReplyToUnthreadedRemoteStatus() {
SpoilerText: "this is a reply",
Visibility: apimodel.VisibilityPublic,
LocalOnly: util.Ptr(false),
ScheduledAt: "",
ScheduledAt: nil,
Language: "en",
ContentType: apimodel.StatusContentTypePlain,
}

View file

@ -19,8 +19,12 @@ package stream
import (
"context"
"errors"
"fmt"
"slices"
"strings"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@ -58,5 +62,22 @@ func (p *Processor) Authorize(ctx context.Context, accessToken string) (*gtsmode
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure read scope.
//
// TODO: make this more granular
// depending on stream type.
hasScopes := strings.Split(ti.GetScope(), " ")
scopeOK := slices.ContainsFunc(
hasScopes,
func(hasScope string) bool {
return apiutil.Scope(hasScope).Permits(apiutil.ScopeRead)
},
)
if !scopeOK {
const errText = "token has insufficient scope permission"
return nil, gtserror.NewErrorForbidden(errors.New(errText), errText)
}
return acct, nil
}

View file

@ -23,15 +23,15 @@ import (
"fmt"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
func (p *Processor) FavedTimelineGet(ctx context.Context, authed *apiutil.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
statuses, nextMaxID, prevMinID, err := p.state.DB.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = fmt.Errorf("FavedTimelineGet: db error getting statuses: %w", err)

View file

@ -22,6 +22,7 @@ import (
"errors"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
@ -29,7 +30,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@ -118,7 +118,7 @@ func HomeTimelineStatusPrepare(state *state.State, converter *typeutils.Converte
}
}
func (p *Processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) {
func (p *Processor) HomeTimelineGet(ctx context.Context, authed *apiutil.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) {
statuses, err := p.state.Timelines.Home.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
err = gtserror.Newf("error getting statuses: %w", err)

View file

@ -23,10 +23,10 @@ import (
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@ -64,7 +64,7 @@ func (suite *HomeTestSuite) TestHomeTimelineGetHideFiltered() {
var (
ctx = context.Background()
requester = suite.testAccounts["local_account_1"]
authed = &oauth.Auth{Account: requester}
authed = &apiutil.Auth{Account: requester}
maxID = ""
sinceID = ""
minID = "01F8MHAAY43M6RJ473VQFCVH36" // 1 before filteredStatus

View file

@ -22,6 +22,7 @@ import (
"errors"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
@ -29,7 +30,6 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/timeline"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@ -130,7 +130,7 @@ func ListTimelineStatusPrepare(state *state.State, converter *typeutils.Converte
}
}
func (p *Processor) ListTimelineGet(ctx context.Context, authed *oauth.Auth, listID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
func (p *Processor) ListTimelineGet(ctx context.Context, authed *apiutil.Auth, listID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) {
// Ensure list exists + is owned by this account.
list, err := p.state.DB.GetListByID(ctx, listID)
if err != nil {

View file

@ -24,6 +24,7 @@ import (
"net/url"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/filter/status"
"github.com/superseriousbusiness/gotosocial/internal/filter/usermute"
@ -31,14 +32,13 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
func (p *Processor) NotificationsGet(
ctx context.Context,
authed *oauth.Auth,
authed *apiutil.Auth,
page *paging.Page,
types []gtsmodel.NotificationType,
excludeTypes []gtsmodel.NotificationType,
@ -164,7 +164,7 @@ func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Accou
return apiNotif, nil
}
func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode {
func (p *Processor) NotificationsClear(ctx context.Context, authed *apiutil.Auth) gtserror.WithCode {
// Delete all notifications of all types that target the authorized account.
if err := p.state.DB.DeleteNotifications(ctx, nil, authed.Account.ID, ""); err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.NewErrorInternalError(err)

View file

@ -44,34 +44,43 @@ func (p *Processor) Create(
app *gtsmodel.Application,
form *apimodel.AccountCreateRequest,
) (*gtsmodel.User, gtserror.WithCode) {
const (
usersPerDay = 10
regBacklog = 20
var (
usersPerDay = config.GetAccountsRegistrationDailyLimit()
regBacklog = config.GetAccountsRegistrationBacklogLimit()
)
// Ensure no more than usersPerDay
// If usersPerDay limit is in place,
// ensure no more than usersPerDay
// have registered in the last 24h.
newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
if err != nil {
err := fmt.Errorf("db error counting new users: %w", err)
return nil, gtserror.NewErrorInternalError(err)
if usersPerDay > 0 {
newUsersCount, err := p.state.DB.CountApprovedSignupsSince(ctx, time.Now().Add(-24*time.Hour))
if err != nil {
err := fmt.Errorf("db error counting new users: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
if newUsersCount >= usersPerDay {
err := fmt.Errorf("this instance has hit its limit of new sign-ups for today (%d); you can try again tomorrow", usersPerDay)
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
}
if newUsersCount >= usersPerDay {
err := fmt.Errorf("this instance has hit its limit of new sign-ups for today; you can try again tomorrow")
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
// If registration backlog limit is
// in place, ensure backlog isn't full.
if regBacklog > 0 {
backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
if err != nil {
err := fmt.Errorf("db error counting registration backlog length: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
// Ensure the new users backlog isn't full.
backlogLen, err := p.state.DB.CountUnhandledSignups(ctx)
if err != nil {
err := fmt.Errorf("db error counting registration backlog length: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
if backlogLen >= regBacklog {
err := fmt.Errorf("this instance's sign-up backlog is currently full; you must wait until pending sign-ups are handled by the admin(s)")
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
if backlogLen >= regBacklog {
err := fmt.Errorf(
"this instance's sign-up backlog is currently full (%d sign-ups pending approval); "+
"you must wait until some pending sign-ups are handled by the admin(s)", regBacklog,
)
return nil, gtserror.NewErrorUnprocessableEntity(err, err.Error())
}
}
emailAvailable, err := p.state.DB.IsEmailAvailable(ctx, form.Email)

View file

@ -0,0 +1,74 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// 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 user_test
import (
"context"
"net"
"testing"
"github.com/stretchr/testify/suite"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
)
type CreateTestSuite struct {
UserStandardTestSuite
}
func (suite *CreateTestSuite) TestCreateOK() {
var (
ctx = context.Background()
app = suite.testApps["application_1"]
appToken = suite.testTokens["local_account_1_client_application_token"]
form = &apimodel.AccountCreateRequest{
Reason: "a long enough explanation of why I am doing api calls",
Username: "someone_new",
Email: "someone_new@example.org",
Password: "a long enough password for this endpoint",
Agreement: true,
Locale: "en-us",
IP: net.ParseIP("192.0.2.128"),
}
)
// Create user via the API endpoint.
user, errWithCode := suite.user.Create(ctx, app, form)
if errWithCode != nil {
suite.FailNow(errWithCode.Error())
}
// Load the app-level access token that was just used.
appAccessToken, err := suite.oauthServer.LoadAccessToken(ctx, appToken.Access)
if err != nil {
suite.FailNow(err.Error())
}
// Create a user-level access token for the new user.
userAccessToken, err := suite.user.TokenForNewUser(ctx, appAccessToken, app, user)
if err != nil {
suite.FailNow(err.Error())
}
// Check returned user-level access token.
suite.NotEmpty(userAccessToken.AccessToken)
suite.Equal("Bearer", userAccessToken.TokenType)
}
func TestCreateTestSuite(t *testing.T) {
suite.Run(t, &CreateTestSuite{})
}

View file

@ -41,6 +41,7 @@ func New(
return Processor{
state: state,
converter: converter,
oauthServer: oauthServer,
emailSender: emailSender,
}
}

View file

@ -23,6 +23,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing/user"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@ -34,9 +35,11 @@ type UserStandardTestSuite struct {
emailSender email.Sender
db db.DB
state state.State
oauthServer oauth.Server
testUsers map[string]*gtsmodel.User
testApps map[string]*gtsmodel.Application
testTokens map[string]*gtsmodel.Token
testUsers map[string]*gtsmodel.User
sentEmails map[string]string
user user.Processor
@ -51,9 +54,12 @@ func (suite *UserStandardTestSuite) SetupTest() {
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.state.AdminActions = admin.New(suite.state.DB, &suite.state.Workers)
suite.oauthServer = testrig.NewTestOauthServer(suite.state.DB)
suite.sentEmails = make(map[string]string)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", suite.sentEmails)
suite.testApps = testrig.NewTestApplications()
suite.testTokens = testrig.NewTestTokens()
suite.testUsers = testrig.NewTestUsers()
suite.user = user.New(&suite.state, typeutils.NewConverter(&suite.state), testrig.NewTestOauthServer(suite.db), suite.emailSender)

View file

@ -260,9 +260,18 @@ func (p *clientAPI) CreateUser(ctx context.Context, cMsg *messages.FromClientAPI
}
func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
var status *gtsmodel.Status
var backfill bool
// Check passed client message model type.
switch model := cMsg.GTSModel.(type) {
case *gtsmodel.Status:
status = model
case *gtsmodel.BackfillStatus:
status = model.Status
backfill = true
default:
return gtserror.Newf("%T not parseable as *gtsmodel.Status or *gtsmodel.BackfillStatus", cMsg.GTSModel)
}
// If pending approval is true then status must
@ -344,12 +353,19 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error updating account stats: %v", err)
}
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
// We specifically do not timeline
// or notify for backfilled statuses,
// as these are more for archival than
// newly posted content for user feeds.
if !backfill {
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status: %v", err)
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status: %v", err)
}
}
if status.InReplyToID != "" {

View file

@ -368,6 +368,162 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
suite.checkWebPushed(testStructs.WebPushSender, receivingAccount.ID, gtsmodel.NotificationStatus)
}
// Even with notifications on for a user, backfilling a status should not notify or timeline it.
func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithNotification() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx,
testStructs.Processor,
receivingAccount,
[]string{testList.ID},
)
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
notifStream = streams[stream.TimelineNotifications]
// Admin account posts a new top-level status.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
nil,
nil,
false,
nil,
)
)
// Update the follow from receiving account -> posting account so
// that receiving account wants notifs when posting account posts.
follow := new(gtsmodel.Follow)
*follow = *suite.testFollows["local_account_1_admin_account"]
follow.Notify = util.Ptr(true)
if err := testStructs.State.DB.UpdateFollow(ctx, follow); err != nil {
suite.FailNow(err.Error())
}
// Process the new status as a backfill.
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: &gtsmodel.BackfillStatus{Status: status},
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// There should be no message in the home stream.
suite.checkStreamed(
homeStream,
false,
"",
"",
)
// There should be no message in the list stream.
suite.checkStreamed(
listStream,
false,
"",
"",
)
// No notification should appear for the status.
if testrig.WaitFor(func() bool {
var err error
_, err = testStructs.State.DB.GetNotification(
ctx,
gtsmodel.NotificationStatus,
receivingAccount.ID,
postingAccount.ID,
status.ID,
)
return err == nil
}) {
suite.FailNow("a status notification was created, but should not have been")
}
// There should be no message in the notification stream.
suite.checkStreamed(
notifStream,
false,
"",
"",
)
// There should be no Web Push status notification.
suite.checkNotWebPushed(testStructs.WebPushSender, receivingAccount.ID)
}
// Backfilled statuses should not federate when created.
func (suite *FromClientAPITestSuite) TestProcessCreateBackfilledStatusWithRemoteFollower() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["local_account_1"]
receivingAccount = suite.testAccounts["remote_account_1"]
// Local account posts a new top-level status.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
nil,
nil,
false,
nil,
)
)
// Follow the local account from the remote account.
follow := &gtsmodel.Follow{
ID: "01JJHW9RW28SC1NEPZ0WBJQ4ZK",
CreatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
UpdatedAt: testrig.TimeMustParse("2022-05-14T13:21:09+02:00"),
AccountID: receivingAccount.ID,
TargetAccountID: postingAccount.ID,
ShowReblogs: util.Ptr(true),
URI: "http://fossbros-anonymous.io/users/foss_satan/follow/01JJHWEVC7F8W2JDW1136K431K",
Notify: util.Ptr(false),
}
if err := testStructs.State.DB.PutFollow(ctx, follow); err != nil {
suite.FailNow(err.Error())
}
// Process the new status as a backfill.
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: &gtsmodel.BackfillStatus{Status: status},
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// No deliveries should be queued.
suite.Zero(testStructs.State.Workers.Delivery.Queue.Len())
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
defer testrig.TearDownTestStructs(testStructs)

View file

@ -124,6 +124,10 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromF
// UPDATE ACCOUNT
case ap.ActorPerson:
return p.fediAPI.UpdateAccount(ctx, fMsg)
// UPDATE QUESTION
case ap.ActivityQuestion:
return p.fediAPI.UpdatePollVote(ctx, fMsg)
}
// ACCEPT SOMETHING
@ -355,7 +359,8 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
return gtserror.Newf("cannot cast %T -> *gtsmodel.PollVote", fMsg.GTSModel)
}
// Insert the new poll vote in the database.
// Insert the new poll vote in the database, note this
// will handle updating votes on the poll model itself.
if err := p.state.DB.PutPollVote(ctx, vote); err != nil {
return gtserror.Newf("error inserting poll vote in db: %w", err)
}
@ -376,9 +381,9 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
status.Poll = vote.Poll
if *status.Local {
// Before federating it, increment the
// poll vote counts on our local copy.
status.Poll.IncrementVotes(vote.Choices)
// Before federating it, increment the poll vote
// and voter counts, *only on our local copy*.
status.Poll.IncrementVotes(vote.Choices, true)
// These were poll votes in a local status, we need to
// federate the updated status model with latest vote counts.
@ -387,8 +392,43 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
}
}
// Interaction counts changed on the source status, uncache from timelines.
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
// Interaction counts changed, uncache from timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
return nil
}
func (p *fediAPI) UpdatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Cast poll vote type from the worker message.
vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote)
if !ok {
return gtserror.Newf("cannot cast %T -> *gtsmodel.PollVote", fMsg.GTSModel)
}
// Update poll vote model (specifically only choices) in the database.
if err := p.state.DB.UpdatePollVote(ctx, vote, "choices"); err != nil {
return gtserror.Newf("error updating poll vote in db: %w", err)
}
// Update the vote counts on the poll model itself. These will have
// been updated by message pusher as we can't know which were new.
if err := p.state.DB.UpdatePoll(ctx, vote.Poll, "votes"); err != nil {
return gtserror.Newf("error updating poll in db: %w", err)
}
// Get the origin status.
status := vote.Poll.Status
if *status.Local {
// These were poll votes in a local status, we need to
// federate the updated status model with latest vote counts.
if err := p.federate.UpdateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating status update: %v", err)
}
}
// Interaction counts changed, uncache from timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
return nil
}
@ -844,16 +884,16 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
return gtserror.Newf("%T not parseable as *url.URL", fMsg.APObject)
}
acceptIRI := fMsg.APIRI
if acceptIRI == nil {
return gtserror.New("acceptIRI was nil")
approvedByURI := fMsg.APIRI
if approvedByURI == nil {
return gtserror.New("approvedByURI was nil")
}
// Assume we're accepting a status; create a
// barebones status for dereferencing purposes.
bareStatus := &gtsmodel.Status{
URI: objectIRI.String(),
ApprovedByURI: acceptIRI.String(),
ApprovedByURI: approvedByURI.String(),
}
// Call RefreshStatus() to process the provided
@ -872,7 +912,7 @@ func (p *fediAPI) AcceptRemoteStatus(ctx context.Context, fMsg *messages.FromFed
}
// No error means it was indeed a remote status, and the
// given acceptIRI permitted it. Timeline and notify it.
// given approvedByURI permitted it. Timeline and notify it.
if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}

View file

@ -21,8 +21,8 @@ import (
"context"
"github.com/stretchr/testify/suite"
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/testrig"
@ -48,7 +48,7 @@ type WorkersTestSuite struct {
testStatuses map[string]*gtsmodel.Status
testTags map[string]*gtsmodel.Tag
testMentions map[string]*gtsmodel.Mention
testAutheds map[string]*oauth.Auth
testAutheds map[string]*apiutil.Auth
testBlocks map[string]*gtsmodel.Block
testActivities map[string]testrig.ActivityWithSignature
testLists map[string]*gtsmodel.List
@ -66,7 +66,7 @@ func (suite *WorkersTestSuite) SetupSuite() {
suite.testStatuses = testrig.NewTestStatuses()
suite.testTags = testrig.NewTestTags()
suite.testMentions = testrig.NewTestMentions()
suite.testAutheds = map[string]*oauth.Auth{
suite.testAutheds = map[string]*apiutil.Auth{
"local_account_1": {
Application: suite.testApplications["local_account_1"],
User: suite.testUsers["local_account_1"],