commit b6fbdc66c1ce1ec61ebfb6fcc0351ea627a1d288 parent adb596600b4705fa22061db5bfe076407c5e0ec6 Author: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Wed, 22 Feb 2023 16:05:26 +0100 [chore] Deinterface processor and subprocessors (#1501) * [chore] Deinterface processor and subprocessors * expose subprocessors via function calls * missing license header Diffstat:
275 files changed, 4931 insertions(+), 6935 deletions(-)
diff --git a/internal/api/activitypub.go b/internal/api/activitypub.go @@ -59,7 +59,7 @@ func (a *ActivityPub) RoutePublicKey(r router.Router, m ...gin.HandlerFunc) { a.publicKey.Route(publicKeyGroup.Handle) } -func NewActivityPub(db db.DB, p processing.Processor) *ActivityPub { +func NewActivityPub(db db.DB, p *processing.Processor) *ActivityPub { return &ActivityPub{ emoji: emoji.New(p), users: users.New(p), diff --git a/internal/api/activitypub/emoji/emoji.go b/internal/api/activitypub/emoji/emoji.go @@ -33,10 +33,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/activitypub/emoji/emojiget.go b/internal/api/activitypub/emoji/emojiget.go @@ -43,7 +43,7 @@ func (m *Module) EmojiGetHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediEmoji(apiutil.TransferSignatureContext(c), requestedEmojiID, c.Request.URL) + resp, errWithCode := m.processor.Fedi().EmojiGet(apiutil.TransferSignatureContext(c), requestedEmojiID, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/emoji/emojiget_test.go b/internal/api/activitypub/emoji/emojiget_test.go @@ -48,7 +48,7 @@ type EmojiGetTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver testEmojis map[string]*gtsmodel.Emoji diff --git a/internal/api/activitypub/publickey/publickey.go b/internal/api/activitypub/publickey/publickey.go @@ -34,10 +34,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/activitypub/publickey/publickeyget.go b/internal/api/activitypub/publickey/publickeyget.go @@ -55,7 +55,7 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().UserGet(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/followers.go b/internal/api/activitypub/users/followers.go @@ -51,7 +51,7 @@ func (m *Module) FollowersGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediFollowers(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().FollowersGet(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/following.go b/internal/api/activitypub/users/following.go @@ -51,7 +51,7 @@ func (m *Module) FollowingGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediFollowing(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().FollowingGet(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/inboxpost.go b/internal/api/activitypub/users/inboxpost.go @@ -38,7 +38,7 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { return } - if posted, err := m.processor.InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil { + if posted, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil { if withCode, ok := err.(gtserror.WithCode); ok { apiutil.ErrorHandler(c, withCode, m.processor.InstanceGetV1) } else { diff --git a/internal/api/activitypub/users/outboxget.go b/internal/api/activitypub/users/outboxget.go @@ -129,7 +129,7 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { maxID = maxIDString } - resp, errWithCode := m.processor.GetFediOutbox(apiutil.TransferSignatureContext(c), requestedUsername, page, maxID, minID, c.Request.URL) + resp, errWithCode := m.processor.Fedi().OutboxGet(apiutil.TransferSignatureContext(c), requestedUsername, page, maxID, minID, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/repliesget.go b/internal/api/activitypub/users/repliesget.go @@ -150,7 +150,7 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { minID = minIDString } - resp, errWithCode := m.processor.GetFediStatusReplies(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) + resp, errWithCode := m.processor.Fedi().StatusRepliesGet(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/statusget.go b/internal/api/activitypub/users/statusget.go @@ -59,7 +59,7 @@ func (m *Module) StatusGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediStatus(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, c.Request.URL) + resp, errWithCode := m.processor.Fedi().StatusGet(apiutil.TransferSignatureContext(c), requestedUsername, requestedStatusID, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/user.go b/internal/api/activitypub/users/user.go @@ -57,10 +57,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/activitypub/users/user_test.go b/internal/api/activitypub/users/user_test.go @@ -44,7 +44,7 @@ type UserStandardTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver // standard suite models diff --git a/internal/api/activitypub/users/userget.go b/internal/api/activitypub/users/userget.go @@ -59,7 +59,7 @@ func (m *Module) UsersGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetFediUser(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) + resp, errWithCode := m.processor.Fedi().UserGet(apiutil.TransferSignatureContext(c), requestedUsername, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/activitypub/users/userget_test.go b/internal/api/activitypub/users/userget_test.go @@ -32,7 +32,6 @@ import ( "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -100,13 +99,7 @@ func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() { userModule := users.New(suite.processor) targetAccount := suite.testAccounts["local_account_1"] - // first delete the account, as though zork had deleted himself - authed := &oauth.Auth{ - Application: suite.testApplications["local_account_1"], - User: suite.testUsers["local_account_1"], - Account: suite.testAccounts["local_account_1"], - } - suite.processor.AccountDeleteLocal(context.Background(), authed, &apimodel.AccountDeleteRequest{ + suite.processor.Account().DeleteLocal(context.Background(), suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{ Password: "password", DeleteOriginID: targetAccount.ID, }) diff --git a/internal/api/auth.go b/internal/api/auth.go @@ -56,7 +56,7 @@ func (a *Auth) Route(r router.Router, m ...gin.HandlerFunc) { a.auth.RouteOauth(oauthGroup.Handle) } -func NewAuth(db db.DB, p processing.Processor, idp oidc.IDP, routerSession *gtsmodel.RouterSession, sessionName string) *Auth { +func NewAuth(db db.DB, p *processing.Processor, idp oidc.IDP, routerSession *gtsmodel.RouterSession, sessionName string) *Auth { return &Auth{ routerSession: routerSession, sessionName: sessionName, diff --git a/internal/api/auth/auth.go b/internal/api/auth/auth.go @@ -78,14 +78,14 @@ const ( type Module struct { db db.DB - processor processing.Processor + processor *processing.Processor idp oidc.IDP } // New returns an Auth module which provides both 'oauth' and 'auth' endpoints. // // It is safe to pass a nil idp if oidc is disabled. -func New(db db.DB, processor processing.Processor, idp oidc.IDP) *Module { +func New(db db.DB, processor *processing.Processor, idp oidc.IDP) *Module { return &Module{ db: db, processor: processor, diff --git a/internal/api/auth/auth_test.go b/internal/api/auth/auth_test.go @@ -49,7 +49,7 @@ type AuthStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender idp oidc.IDP diff --git a/internal/api/client.go b/internal/api/client.go @@ -49,7 +49,7 @@ import ( ) type Client struct { - processor processing.Processor + processor *processing.Processor db db.DB accounts *accounts.Module // api/v1/accounts @@ -110,7 +110,7 @@ func (c *Client) Route(r router.Router, m ...gin.HandlerFunc) { c.user.Route(h) } -func NewClient(db db.DB, p processing.Processor) *Client { +func NewClient(db db.DB, p *processing.Processor) *Client { return &Client{ processor: p, db: db, diff --git a/internal/api/client/accounts/account_test.go b/internal/api/client/accounts/account_test.go @@ -48,7 +48,7 @@ type AccountStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender sentEmails map[string]string diff --git a/internal/api/client/accounts/accountcreate.go b/internal/api/client/accounts/accountcreate.go @@ -102,7 +102,7 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { } form.IP = signUpIP - ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form) + ti, errWithCode := m.processor.Account().Create(c.Request.Context(), authed.Token, authed.Application, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/accountdelete.go b/internal/api/client/accounts/accountdelete.go @@ -86,7 +86,7 @@ func (m *Module) AccountDeletePOSTHandler(c *gin.Context) { form.DeleteOriginID = authed.Account.ID - if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil { + if errWithCode := m.processor.Account().DeleteLocal(c.Request.Context(), authed.Account, form); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/accounts/accountget.go b/internal/api/client/accounts/accountget.go @@ -85,7 +85,7 @@ func (m *Module) AccountGETHandler(c *gin.Context) { return } - acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID) + acctInfo, errWithCode := m.processor.Account().Get(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/accounts.go b/internal/api/client/accounts/accounts.go @@ -74,10 +74,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/accounts/accountupdate.go b/internal/api/client/accounts/accountupdate.go @@ -153,7 +153,7 @@ func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { return } - acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form) + acctSensitive, errWithCode := m.processor.Account().Update(c.Request.Context(), authed.Account, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/accountverify.go b/internal/api/client/accounts/accountverify.go @@ -68,7 +68,7 @@ func (m *Module) AccountVerifyGETHandler(c *gin.Context) { return } - acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) + acctSensitive, errWithCode := m.processor.Account().Get(c.Request.Context(), authed.Account, authed.Account.ID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/block.go b/internal/api/client/accounts/block.go @@ -85,7 +85,7 @@ func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { return } - relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID) + relationship, errWithCode := m.processor.Account().BlockCreate(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/follow.go b/internal/api/client/accounts/follow.go @@ -114,7 +114,7 @@ func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { } form.ID = targetAcctID - relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form) + relationship, errWithCode := m.processor.Account().FollowCreate(c.Request.Context(), authed.Account, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/followers.go b/internal/api/client/accounts/followers.go @@ -88,7 +88,7 @@ func (m *Module) AccountFollowersGETHandler(c *gin.Context) { return } - followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID) + followers, errWithCode := m.processor.Account().FollowersGet(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/following.go b/internal/api/client/accounts/following.go @@ -88,7 +88,7 @@ func (m *Module) AccountFollowingGETHandler(c *gin.Context) { return } - following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID) + following, errWithCode := m.processor.Account().FollowingGet(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/relationships.go b/internal/api/client/accounts/relationships.go @@ -81,7 +81,7 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { relationships := []apimodel.Relationship{} for _, targetAccountID := range targetAccountIDs { - r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID) + r, errWithCode := m.processor.Account().RelationshipGet(c.Request.Context(), authed.Account, targetAccountID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/statuses.go b/internal/api/client/accounts/statuses.go @@ -233,7 +233,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { publicOnly = i } - resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) + resp, errWithCode := m.processor.Account().StatusesGet(c.Request.Context(), authed.Account, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/unblock.go b/internal/api/client/accounts/unblock.go @@ -86,7 +86,7 @@ func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { return } - relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID) + relationship, errWithCode := m.processor.Account().BlockRemove(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/accounts/unfollow.go b/internal/api/client/accounts/unfollow.go @@ -86,7 +86,7 @@ func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { return } - relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID) + relationship, errWithCode := m.processor.Account().FollowRemove(c.Request.Context(), authed.Account, targetAcctID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/accountaction.go b/internal/api/client/admin/accountaction.go @@ -115,7 +115,7 @@ func (m *Module) AccountActionPOSTHandler(c *gin.Context) { } form.TargetAccountID = targetAcctID - if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil { + if errWithCode := m.processor.Admin().AccountAction(c.Request.Context(), authed.Account, form); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go @@ -83,10 +83,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/admin/admin_test.go b/internal/api/client/admin/admin_test.go @@ -48,7 +48,7 @@ type AdminStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender sentEmails map[string]string diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go @@ -167,7 +167,7 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { if imp { // we're importing multiple blocks - domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form) + domainBlocks, errWithCode := m.processor.Admin().DomainBlocksImport(c.Request.Context(), authed.Account, form.Domains) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return @@ -177,7 +177,7 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { } // we're just creating one block - domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form) + domainBlock, errWithCode := m.processor.Admin().DomainBlockCreate(c.Request.Context(), authed.Account, form.Domain, form.Obfuscate, form.PublicComment, form.PrivateComment, "") if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go @@ -94,7 +94,7 @@ func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { return } - domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID) + domainBlock, errWithCode := m.processor.Admin().DomainBlockDelete(c.Request.Context(), authed.Account, domainBlockID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go @@ -107,7 +107,7 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { export = i } - domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export) + domainBlock, errWithCode := m.processor.Admin().DomainBlockGet(c.Request.Context(), authed.Account, domainBlockID, export) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go @@ -105,7 +105,7 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { export = i } - domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export) + domainBlocks, errWithCode := m.processor.Admin().DomainBlocksGet(c.Request.Context(), authed.Account, export) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojicategoriesget.go b/internal/api/client/admin/emojicategoriesget.go @@ -84,7 +84,7 @@ func (m *Module) EmojiCategoriesGETHandler(c *gin.Context) { return } - categories, errWithCode := m.processor.AdminEmojiCategoriesGet(c.Request.Context()) + categories, errWithCode := m.processor.Admin().EmojiCategoriesGet(c.Request.Context()) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go @@ -126,7 +126,7 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) { return } - apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form) + apiEmoji, errWithCode := m.processor.Admin().EmojiCreate(c.Request.Context(), authed.Account, authed.User, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojidelete.go b/internal/api/client/admin/emojidelete.go @@ -100,7 +100,7 @@ func (m *Module) EmojiDELETEHandler(c *gin.Context) { return } - emoji, errWithCode := m.processor.AdminEmojiDelete(c.Request.Context(), authed, emojiID) + emoji, errWithCode := m.processor.Admin().EmojiDelete(c.Request.Context(), emojiID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojiget.go b/internal/api/client/admin/emojiget.go @@ -90,7 +90,7 @@ func (m *Module) EmojiGETHandler(c *gin.Context) { return } - emoji, errWithCode := m.processor.AdminEmojiGet(c.Request.Context(), authed, emojiID) + emoji, errWithCode := m.processor.Admin().EmojiGet(c.Request.Context(), authed.Account, authed.User, emojiID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojisget.go b/internal/api/client/admin/emojisget.go @@ -198,7 +198,7 @@ func (m *Module) EmojisGETHandler(c *gin.Context) { includeEnabled = true } - resp, errWithCode := m.processor.AdminEmojisGet(c.Request.Context(), authed, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) + resp, errWithCode := m.processor.Admin().EmojisGet(c.Request.Context(), authed.Account, authed.User, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/emojiupdate.go b/internal/api/client/admin/emojiupdate.go @@ -156,7 +156,7 @@ func (m *Module) EmojiPATCHHandler(c *gin.Context) { return } - emoji, errWithCode := m.processor.AdminEmojiUpdate(c.Request.Context(), emojiID, form) + emoji, errWithCode := m.processor.Admin().EmojiUpdate(c.Request.Context(), emojiID, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/mediacleanup.go b/internal/api/client/admin/mediacleanup.go @@ -98,7 +98,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) { remoteCacheDays = 0 } - if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { + if errWithCode := m.processor.Admin().MediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/mediarefetch.go b/internal/api/client/admin/mediarefetch.go @@ -84,7 +84,7 @@ func (m *Module) MediaRefetchPOSTHandler(c *gin.Context) { return } - if errWithCode := m.processor.AdminMediaRefetch(c.Request.Context(), authed, c.Query(DomainQueryKey)); errWithCode != nil { + if errWithCode := m.processor.Admin().MediaRefetch(c.Request.Context(), authed.Account, c.Query(DomainQueryKey)); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/admin/reportget.go b/internal/api/client/admin/reportget.go @@ -93,7 +93,7 @@ func (m *Module) ReportGETHandler(c *gin.Context) { return } - report, errWithCode := m.processor.AdminReportGet(c.Request.Context(), authed, reportID) + report, errWithCode := m.processor.Admin().ReportGet(c.Request.Context(), authed.Account, reportID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/reportresolve.go b/internal/api/client/admin/reportresolve.go @@ -115,7 +115,7 @@ func (m *Module) ReportResolvePOSTHandler(c *gin.Context) { return } - report, errWithCode := m.processor.AdminReportResolve(c.Request.Context(), authed, reportID, form.ActionTakenComment) + report, errWithCode := m.processor.Admin().ReportResolve(c.Request.Context(), authed.Account, reportID, form.ActionTakenComment) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/admin/reportsget.go b/internal/api/client/admin/reportsget.go @@ -171,7 +171,7 @@ func (m *Module) ReportsGETHandler(c *gin.Context) { limit = i } - resp, errWithCode := m.processor.AdminReportsGet(c.Request.Context(), authed, resolved, c.Query(AccountIDKey), c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) + resp, errWithCode := m.processor.Admin().ReportsGet(c.Request.Context(), authed.Account, resolved, c.Query(AccountIDKey), c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/apps/apps.go b/internal/api/client/apps/apps.go @@ -29,10 +29,10 @@ import ( const BasePath = "/v1/apps" type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/blocks/blocks.go b/internal/api/client/blocks/blocks.go @@ -38,10 +38,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/bookmarks/bookmarks.go b/internal/api/client/bookmarks/bookmarks.go @@ -31,10 +31,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/bookmarks/bookmarks_test.go b/internal/api/client/bookmarks/bookmarks_test.go @@ -53,7 +53,7 @@ type BookmarkTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver // standard suite models diff --git a/internal/api/client/bookmarks/bookmarksget.go b/internal/api/client/bookmarks/bookmarksget.go @@ -89,7 +89,7 @@ func (m *Module) BookmarksGETHandler(c *gin.Context) { minID = minIDString } - resp, errWithCode := m.processor.BookmarksGet(c.Request.Context(), authed, maxID, minID, limit) + resp, errWithCode := m.processor.Account().BookmarksGet(c.Request.Context(), authed.Account, limit, maxID, minID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/customemojis/customemojis.go b/internal/api/client/customemojis/customemojis.go @@ -31,10 +31,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/customemojis/customemojisget.go b/internal/api/client/customemojis/customemojisget.go @@ -66,7 +66,7 @@ func (m *Module) CustomEmojisGETHandler(c *gin.Context) { return } - emojis, errWithCode := m.processor.CustomEmojisGet(c) + emojis, errWithCode := m.processor.Media().GetCustomEmojis(c) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/favourites/favourites.go b/internal/api/client/favourites/favourites.go @@ -42,10 +42,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/favourites/favourites_test.go b/internal/api/client/favourites/favourites_test.go @@ -42,7 +42,7 @@ type FavouritesStandardTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver // standard suite models diff --git a/internal/api/client/featuredtags/featuredtags.go b/internal/api/client/featuredtags/featuredtags.go @@ -30,10 +30,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/filters/filter.go b/internal/api/client/filters/filter.go @@ -31,10 +31,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/followrequests/followrequest.go b/internal/api/client/followrequests/followrequest.go @@ -40,10 +40,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/followrequests/followrequest_test.go b/internal/api/client/followrequests/followrequest_test.go @@ -46,7 +46,7 @@ type FollowRequestStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender // standard suite models diff --git a/internal/api/client/instance/instance.go b/internal/api/client/instance/instance.go @@ -33,10 +33,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/instance/instance_test.go b/internal/api/client/instance/instance_test.go @@ -47,7 +47,7 @@ type InstanceStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender sentEmails map[string]string diff --git a/internal/api/client/lists/list.go b/internal/api/client/lists/list.go @@ -31,10 +31,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/media/media.go b/internal/api/client/media/media.go @@ -35,10 +35,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go @@ -123,7 +123,7 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { return } - apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form) + apiAttachment, errWithCode := m.processor.Media().Create(c.Request.Context(), authed.Account, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go @@ -59,7 +59,7 @@ type MediaCreateTestSuite struct { tc typeutils.TypeConverter oauthServer oauth.Server emailSender email.Sender - processor processing.Processor + processor *processing.Processor // standard suite models testTokens map[string]*gtsmodel.Token diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go @@ -91,7 +91,7 @@ func (m *Module) MediaGETHandler(c *gin.Context) { return } - attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID) + attachment, errWithCode := m.processor.Media().Get(c.Request.Context(), authed.Account, attachmentID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go @@ -134,7 +134,7 @@ func (m *Module) MediaPUTHandler(c *gin.Context) { return } - attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form) + attachment, errWithCode := m.processor.Media().Update(c.Request.Context(), authed.Account, attachmentID, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/media/mediaupdate_test.go b/internal/api/client/media/mediaupdate_test.go @@ -57,7 +57,7 @@ type MediaUpdateTestSuite struct { mediaManager media.Manager oauthServer oauth.Server emailSender email.Sender - processor processing.Processor + processor *processing.Processor // standard suite models testTokens map[string]*gtsmodel.Token diff --git a/internal/api/client/notifications/notifications.go b/internal/api/client/notifications/notifications.go @@ -46,10 +46,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/reports/reportcreate.go b/internal/api/client/reports/reportcreate.go @@ -102,7 +102,7 @@ func (m *Module) ReportPOSTHandler(c *gin.Context) { return } - apiReport, errWithCode := m.processor.ReportCreate(c.Request.Context(), authed, form) + apiReport, errWithCode := m.processor.Report().Create(c.Request.Context(), authed.Account, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/reports/reportget.go b/internal/api/client/reports/reportget.go @@ -85,7 +85,7 @@ func (m *Module) ReportGETHandler(c *gin.Context) { return } - report, errWithCode := m.processor.ReportGet(c.Request.Context(), authed, targetReportID) + report, errWithCode := m.processor.Report().Get(c.Request.Context(), authed.Account, targetReportID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/reports/reports.go b/internal/api/client/reports/reports.go @@ -38,10 +38,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/reports/reports_test.go b/internal/api/client/reports/reports_test.go @@ -39,7 +39,7 @@ type ReportsStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender sentEmails map[string]string diff --git a/internal/api/client/reports/reportsget.go b/internal/api/client/reports/reportsget.go @@ -160,7 +160,7 @@ func (m *Module) ReportsGETHandler(c *gin.Context) { limit = i } - resp, errWithCode := m.processor.ReportsGet(c.Request.Context(), authed, resolved, c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) + resp, errWithCode := m.processor.Report().GetMultiple(c.Request.Context(), authed.Account, resolved, c.Query(TargetAccountIDKey), c.Query(MaxIDKey), c.Query(SinceIDKey), c.Query(MinIDKey), limit) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/search/search.go b/internal/api/client/search/search.go @@ -62,10 +62,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/search/search_test.go b/internal/api/client/search/search_test.go @@ -47,7 +47,7 @@ type SearchStandardTestSuite struct { storage *storage.Driver mediaManager media.Manager federator federation.Federator - processor processing.Processor + processor *processing.Processor emailSender email.Sender sentEmails map[string]string diff --git a/internal/api/client/statuses/status.go b/internal/api/client/statuses/status.go @@ -68,10 +68,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/statuses/status_test.go b/internal/api/client/statuses/status_test.go @@ -42,7 +42,7 @@ type StatusStandardTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver // standard suite models diff --git a/internal/api/client/statuses/statusbookmark.go b/internal/api/client/statuses/statusbookmark.go @@ -88,7 +88,7 @@ func (m *Module) StatusBookmarkPOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusBookmark(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().BookmarkCreate(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusboost.go b/internal/api/client/statuses/statusboost.go @@ -91,7 +91,7 @@ func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().BoostCreate(c.Request.Context(), authed.Account, authed.Application, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusboostedby.go b/internal/api/client/statuses/statusboostedby.go @@ -79,7 +79,7 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) { return } - apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID) + apiAccounts, errWithCode := m.processor.Status().StatusBoostedBy(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statuscontext.go b/internal/api/client/statuses/statuscontext.go @@ -90,7 +90,7 @@ func (m *Module) StatusContextGETHandler(c *gin.Context) { return } - statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID) + statusContext, errWithCode := m.processor.Status().ContextGet(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statuscreate.go b/internal/api/client/statuses/statuscreate.go @@ -104,7 +104,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form) + apiStatus, errWithCode := m.processor.Status().Create(c.Request.Context(), authed.Account, authed.Application, form) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusdelete.go b/internal/api/client/statuses/statusdelete.go @@ -90,7 +90,7 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().Delete(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusfave.go b/internal/api/client/statuses/statusfave.go @@ -87,7 +87,7 @@ func (m *Module) StatusFavePOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().FaveCreate(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusfavedby.go b/internal/api/client/statuses/statusfavedby.go @@ -88,7 +88,7 @@ func (m *Module) StatusFavedByGETHandler(c *gin.Context) { return } - apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID) + apiAccounts, errWithCode := m.processor.Status().FavedBy(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusget.go b/internal/api/client/statuses/statusget.go @@ -87,7 +87,7 @@ func (m *Module) StatusGETHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().Get(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusunbookmark.go b/internal/api/client/statuses/statusunbookmark.go @@ -88,7 +88,7 @@ func (m *Module) StatusUnbookmarkPOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusUnbookmark(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().BookmarkRemove(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusunboost.go b/internal/api/client/statuses/statusunboost.go @@ -88,7 +88,7 @@ func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().BoostRemove(c.Request.Context(), authed.Account, authed.Application, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/statuses/statusunfave.go b/internal/api/client/statuses/statusunfave.go @@ -87,7 +87,7 @@ func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { return } - apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID) + apiStatus, errWithCode := m.processor.Status().FaveRemove(c.Request.Context(), authed.Account, targetStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/streaming/stream.go b/internal/api/client/streaming/stream.go @@ -154,13 +154,13 @@ func (m *Module) StreamGETHandler(c *gin.Context) { } } - account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), token) + account, errWithCode := m.processor.Stream().Authorize(c.Request.Context(), token) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } - stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType) + stream, errWithCode := m.processor.Stream().Open(c.Request.Context(), account, streamType) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/client/streaming/streaming.go b/internal/api/client/streaming/streaming.go @@ -42,12 +42,12 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor dTicker time.Duration wsUpgrade websocket.Upgrader } -func New(processor processing.Processor, dTicker time.Duration, wsBuf int) *Module { +func New(processor *processing.Processor, dTicker time.Duration, wsBuf int) *Module { return &Module{ processor: processor, dTicker: dTicker, diff --git a/internal/api/client/streaming/streaming_test.go b/internal/api/client/streaming/streaming_test.go @@ -54,7 +54,7 @@ type StreamingTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver // standard suite models diff --git a/internal/api/client/timelines/timeline.go b/internal/api/client/timelines/timeline.go @@ -45,10 +45,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/user/passwordchange.go b/internal/api/client/user/passwordchange.go @@ -95,7 +95,7 @@ func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { return } - if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil { + if errWithCode := m.processor.User().PasswordChange(c.Request.Context(), authed.User, form.OldPassword, form.NewPassword); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return } diff --git a/internal/api/client/user/user.go b/internal/api/client/user/user.go @@ -33,10 +33,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/client/user/user_test.go b/internal/api/client/user/user_test.go @@ -41,7 +41,7 @@ type UserStandardTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver testTokens map[string]*gtsmodel.Token diff --git a/internal/api/fileserver.go b/internal/api/fileserver.go @@ -54,7 +54,7 @@ func (f *Fileserver) Route(r router.Router, m ...gin.HandlerFunc) { f.fileserver.Route(fileserverGroup.Handle) } -func NewFileserver(p processing.Processor) *Fileserver { +func NewFileserver(p *processing.Processor) *Fileserver { return &Fileserver{ fileserver: fileserver.New(p), } diff --git a/internal/api/fileserver/fileserver.go b/internal/api/fileserver/fileserver.go @@ -39,10 +39,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/fileserver/fileserver_test.go b/internal/api/fileserver/fileserver_test.go @@ -45,7 +45,7 @@ type FileserverTestSuite struct { storage *storage.Driver federator federation.Federator tc typeutils.TypeConverter - processor processing.Processor + processor *processing.Processor mediaManager media.Manager oauthServer oauth.Server emailSender email.Sender diff --git a/internal/api/fileserver/servefile.go b/internal/api/fileserver/servefile.go @@ -80,7 +80,7 @@ func (m *Module) ServeFile(c *gin.Context) { // Acquire context from gin request. ctx := c.Request.Context() - content, errWithCode := m.processor.FileGet(ctx, authed, &apimodel.GetContentRequestForm{ + content, errWithCode := m.processor.Media().GetFile(ctx, authed.Account, &apimodel.GetContentRequestForm{ AccountID: accountID, MediaType: mediaType, MediaSize: mediaSize, diff --git a/internal/api/nodeinfo.go b/internal/api/nodeinfo.go @@ -44,7 +44,7 @@ func (w *NodeInfo) Route(r router.Router, m ...gin.HandlerFunc) { w.nodeInfo.Route(nodeInfoGroup.Handle) } -func NewNodeInfo(p processing.Processor) *NodeInfo { +func NewNodeInfo(p *processing.Processor) *NodeInfo { return &NodeInfo{ nodeInfo: nodeinfo.New(p), } diff --git a/internal/api/nodeinfo/nodeinfo.go b/internal/api/nodeinfo/nodeinfo.go @@ -32,10 +32,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/nodeinfo/nodeinfoget.go b/internal/api/nodeinfo/nodeinfoget.go @@ -50,7 +50,7 @@ func (m *Module) NodeInfo2GETHandler(c *gin.Context) { return } - nodeInfo, errWithCode := m.processor.GetNodeInfo(c.Request.Context()) + nodeInfo, errWithCode := m.processor.Fedi().NodeInfoGet(c.Request.Context()) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/wellknown.go b/internal/api/wellknown.go @@ -47,7 +47,7 @@ func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) { w.webfinger.Route(wellKnownGroup.Handle) } -func NewWellKnown(p processing.Processor) *WellKnown { +func NewWellKnown(p *processing.Processor) *WellKnown { return &WellKnown{ nodeInfo: nodeinfo.New(p), webfinger: webfinger.New(p), diff --git a/internal/api/wellknown/nodeinfo/nodeinfo.go b/internal/api/wellknown/nodeinfo/nodeinfo.go @@ -32,11 +32,11 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } // New returns a new nodeinfo module -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/wellknown/nodeinfo/nodeinfoget.go b/internal/api/wellknown/nodeinfo/nodeinfoget.go @@ -50,7 +50,7 @@ func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { return } - resp, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context()) + resp, errWithCode := m.processor.Fedi().NodeInfoRelGet(c.Request.Context()) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/wellknown/webfinger/webfinger.go b/internal/api/wellknown/webfinger/webfinger.go @@ -32,10 +32,10 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor } -func New(processor processing.Processor) *Module { +func New(processor *processing.Processor) *Module { return &Module{ processor: processor, } diff --git a/internal/api/wellknown/webfinger/webfinger_test.go b/internal/api/wellknown/webfinger/webfinger_test.go @@ -48,7 +48,7 @@ type WebfingerStandardTestSuite struct { mediaManager media.Manager federator federation.Federator emailSender email.Sender - processor processing.Processor + processor *processing.Processor storage *storage.Driver oauthServer oauth.Server diff --git a/internal/api/wellknown/webfinger/webfingerget.go b/internal/api/wellknown/webfinger/webfingerget.go @@ -81,7 +81,7 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) { return } - resp, errWithCode := m.processor.GetWebfingerAccount(c.Request.Context(), requestedUsername) + resp, errWithCode := m.processor.Fedi().WebfingerGet(c.Request.Context(), requestedUsername) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/processing/account.go b/internal/processing/account.go @@ -1,92 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - "time" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { - return p.accountProcessor.Create(ctx, authed.Token, authed.Application, form) -} - -func (p *processor) AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode { - return p.accountProcessor.DeleteLocal(ctx, authed.Account, form) -} - -func (p *processor) AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode) { - return p.accountProcessor.Get(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) { - return p.accountProcessor.GetLocalByUsername(ctx, authed.Account, username) -} - -func (p *processor) AccountGetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) { - return p.accountProcessor.GetCustomCSSForUsername(ctx, username) -} - -func (p *processor) AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) { - return p.accountProcessor.GetRSSFeedForUsername(ctx, username) -} - -func (p *processor) AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { - return p.accountProcessor.Update(ctx, authed.Account, form) -} - -func (p *processor) AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.accountProcessor.StatusesGet(ctx, authed.Account, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) -} - -func (p *processor) AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.accountProcessor.WebStatusesGet(ctx, targetAccountID, maxID) -} - -func (p *processor) AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - return p.accountProcessor.FollowersGet(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountFollowingGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - return p.accountProcessor.FollowingGet(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountRelationshipGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - return p.accountProcessor.RelationshipGet(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountFollowCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { - return p.accountProcessor.FollowCreate(ctx, authed.Account, form) -} - -func (p *processor) AccountFollowRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - return p.accountProcessor.FollowRemove(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountBlockCreate(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - return p.accountProcessor.BlockCreate(ctx, authed.Account, targetAccountID) -} - -func (p *processor) AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - return p.accountProcessor.BlockRemove(ctx, authed.Account, targetAccountID) -} diff --git a/internal/processing/account/account.go b/internal/processing/account/account.go @@ -19,15 +19,9 @@ package account import ( - "context" - "mime/multipart" - "time" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" @@ -35,63 +29,12 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/text" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/oauth2/v4" ) -// Processor wraps a bunch of functions for processing account actions. -type Processor interface { - // Create processes the given form for creating a new account, returning an oauth token for that account if successful. - Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) - // Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. - // The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. - Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode - // DeleteLocal is like delete, but specifically for deletion of local accounts rather than federated ones. - // Unlike Delete, it will propagate the deletion out across the federating API to other instances. - DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode - // Get processes the given request for account information. - Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) - // GetLocalByUsername processes the given request for account information targeting a local account by username. - GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) - // GetCustomCSSForUsername returns custom css for the given local username. - GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) - // GetRSSFeedForUsername returns RSS feed for the given local username. - GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) - // Update processes the update of an account with the given form - Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) - // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for - // the account given in authed. - StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) - // WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only - // statuses which are suitable for showing on the public web profile of an account. - WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) - // StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for - // the account given in authed. - BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) - // FollowersGet fetches a list of the target account's followers. - FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) - // FollowingGet fetches a list of the accounts that target account is following. - FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) - // RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. - RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // FollowCreate handles a follow request to an account, either remote or local. - FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) - // FollowRemove handles the removal of a follow/follow request to an account, either remote or local. - FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local. - BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local. - BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // UpdateAvatar does the dirty work of checking the avatar part of an account update form, - // parsing and checking the image, and doing the necessary updates in the database for this to become - // the account's new avatar image. - UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) - // UpdateHeader does the dirty work of checking the header part of an account update form, - // parsing and checking the image, and doing the necessary updates in the database for this to become - // the account's new header image. - UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) -} - -type processor struct { +// Processor wraps functionality for updating, creating, and deleting accounts in response to API requests. +// +// It also contains logic for actions towards accounts such as following, blocking, seeing follows, etc. +type Processor struct { tc typeutils.TypeConverter mediaManager media.Manager clientWorker *concurrency.WorkerPool[messages.FromClientAPI] @@ -104,8 +47,16 @@ type processor struct { } // New returns a new account processor. -func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, oauthServer oauth.Server, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], federator federation.Federator, parseMention gtsmodel.ParseMentionFunc) Processor { - return &processor{ +func New( + db db.DB, + tc typeutils.TypeConverter, + mediaManager media.Manager, + oauthServer oauth.Server, + clientWorker *concurrency.WorkerPool[messages.FromClientAPI], + federator federation.Federator, + parseMention gtsmodel.ParseMentionFunc, +) Processor { + return Processor{ tc: tc, mediaManager: mediaManager, clientWorker: clientWorker, diff --git a/internal/processing/account/block.go b/internal/processing/account/block.go @@ -0,0 +1,192 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "errors" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/uris" +) + +// BlockCreate handles the creation of a block from requestingAccount to targetAccountID, either remote or local. +func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { + // make sure the target account actually exists in our db + targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) + } + + // if requestingAccount already blocks target account, we don't need to do anything + if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, false); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err)) + } else if blocked { + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) + } + + // don't block yourself, silly + if requestingAccount.ID == targetAccountID { + return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("BlockCreate: account %s cannot block itself", requestingAccount.ID)) + } + + // make the block + block := >smodel.Block{} + newBlockID := id.NewULID() + block.ID = newBlockID + block.AccountID = requestingAccount.ID + block.Account = requestingAccount + block.TargetAccountID = targetAccountID + block.TargetAccount = targetAccount + block.URI = uris.GenerateURIForBlock(requestingAccount.Username, newBlockID) + + // whack it in the database + if err := p.db.PutBlock(ctx, block); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err)) + } + + // clear any follows or follow requests from the blocked account to the target account -- this is a simple delete + if err := p.db.DeleteWhere(ctx, []db.Where{ + {Key: "account_id", Value: targetAccountID}, + {Key: "target_account_id", Value: requestingAccount.ID}, + }, >smodel.Follow{}); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) + } + if err := p.db.DeleteWhere(ctx, []db.Where{ + {Key: "account_id", Value: targetAccountID}, + {Key: "target_account_id", Value: requestingAccount.ID}, + }, >smodel.FollowRequest{}); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) + } + + // clear any follows or follow requests from the requesting account to the target account -- + // this might require federation so we need to pass some messages around + + // check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later) + var frChanged bool + var frURI string + fr := >smodel.FollowRequest{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "account_id", Value: requestingAccount.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, fr); err == nil { + frURI = fr.URI + if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err)) + } + frChanged = true + } + + // now do the same thing for any existing follow + var fChanged bool + var fURI string + f := >smodel.Follow{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "account_id", Value: requestingAccount.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, f); err == nil { + fURI = f.URI + if err := p.db.DeleteByID(ctx, f.ID, f); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err)) + } + fChanged = true + } + + // follow request status changed so send the UNDO activity to the channel for async processing + if frChanged { + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityUndo, + GTSModel: >smodel.Follow{ + AccountID: requestingAccount.ID, + TargetAccountID: targetAccountID, + URI: frURI, + }, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) + } + + // follow status changed so send the UNDO activity to the channel for async processing + if fChanged { + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityUndo, + GTSModel: >smodel.Follow{ + AccountID: requestingAccount.ID, + TargetAccountID: targetAccountID, + URI: fURI, + }, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) + } + + // handle the rest of the block process asynchronously + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityBlock, + APActivityType: ap.ActivityCreate, + GTSModel: block, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) + + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) +} + +// BlockRemove handles the removal of a block from requestingAccount to targetAccountID, either remote or local. +func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { + // make sure the target account actually exists in our db + targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) + } + + // check if a block exists, and remove it if it does + block, err := p.db.GetBlock(ctx, requestingAccount.ID, targetAccountID) + if err == nil { + // we got a block, remove it + block.Account = requestingAccount + block.TargetAccount = targetAccount + if err := p.db.DeleteBlockByID(ctx, block.ID); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err)) + } + + // send the UNDO activity to the client worker for async processing + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityBlock, + APActivityType: ap.ActivityUndo, + GTSModel: block, + OriginAccount: requestingAccount, + TargetAccount: targetAccount, + }) + } else if !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err)) + } + + // return whatever relationship results from all this + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) +} diff --git a/internal/processing/account/bookmarks.go b/internal/processing/account/bookmarks.go @@ -0,0 +1,88 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) { + if requestingAccount == nil { + return nil, gtserror.NewErrorForbidden(fmt.Errorf("cannot retrieve bookmarks without a requesting account")) + } + + bookmarks, err := p.db.GetBookmarks(ctx, requestingAccount.ID, limit, maxID, minID) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(bookmarks) + filtered := make([]*gtsmodel.Status, 0, len(bookmarks)) + nextMaxIDValue := "" + prevMinIDValue := "" + for i, b := range bookmarks { + s, err := p.db.GetStatusByID(ctx, b.StatusID) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err == nil && visible { + if i == count-1 { + nextMaxIDValue = b.ID + } + + if i == 0 { + prevMinIDValue = b.ID + } + + filtered = append(filtered, s) + } + } + + count = len(filtered) + + if count == 0 { + return util.EmptyPageableResponse(), nil + } + + items := []interface{}{} + for _, s := range filtered { + item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) + } + items = append(items, item) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "/api/v1/bookmarks", + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + Limit: limit, + ExtraQueryParams: []string{}, + }) +} diff --git a/internal/processing/account/create.go b/internal/processing/account/create.go @@ -33,7 +33,8 @@ import ( "github.com/superseriousbusiness/oauth2/v4" ) -func (p *processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { +// Create processes the given form for creating a new account, returning an oauth token for that account if successful. +func (p *Processor) Create(ctx context.Context, applicationToken oauth2.TokenInfo, application *gtsmodel.Application, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) { emailAvailable, err := p.db.IsEmailAvailable(ctx, form.Email) if err != nil { return nil, gtserror.NewErrorBadRequest(err) diff --git a/internal/processing/account/createblock.go b/internal/processing/account/createblock.go @@ -1,156 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -func (p *processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // make sure the target account actually exists in our db - targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) - } - - // if requestingAccount already blocks target account, we don't need to do anything - if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, false); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error checking existence of block: %s", err)) - } else if blocked { - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) - } - - // don't block yourself, silly - if requestingAccount.ID == targetAccountID { - return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("BlockCreate: account %s cannot block itself", requestingAccount.ID)) - } - - // make the block - block := >smodel.Block{} - newBlockID := id.NewULID() - block.ID = newBlockID - block.AccountID = requestingAccount.ID - block.Account = requestingAccount - block.TargetAccountID = targetAccountID - block.TargetAccount = targetAccount - block.URI = uris.GenerateURIForBlock(requestingAccount.Username, newBlockID) - - // whack it in the database - if err := p.db.PutBlock(ctx, block); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error creating block in db: %s", err)) - } - - // clear any follows or follow requests from the blocked account to the target account -- this is a simple delete - if err := p.db.DeleteWhere(ctx, []db.Where{ - {Key: "account_id", Value: targetAccountID}, - {Key: "target_account_id", Value: requestingAccount.ID}, - }, >smodel.Follow{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) - } - if err := p.db.DeleteWhere(ctx, []db.Where{ - {Key: "account_id", Value: targetAccountID}, - {Key: "target_account_id", Value: requestingAccount.ID}, - }, >smodel.FollowRequest{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow in db: %s", err)) - } - - // clear any follows or follow requests from the requesting account to the target account -- - // this might require federation so we need to pass some messages around - - // check if a follow request exists from the requesting account to the target account, and remove it if it does (storing the URI for later) - var frChanged bool - var frURI string - fr := >smodel.FollowRequest{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, fr); err == nil { - frURI = fr.URI - if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow request from db: %s", err)) - } - frChanged = true - } - - // now do the same thing for any existing follow - var fChanged bool - var fURI string - f := >smodel.Follow{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, f); err == nil { - fURI = f.URI - if err := p.db.DeleteByID(ctx, f.ID, f); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockCreate: error removing follow from db: %s", err)) - } - fChanged = true - } - - // follow request status changed so send the UNDO activity to the channel for async processing - if frChanged { - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: frURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) - } - - // follow status changed so send the UNDO activity to the channel for async processing - if fChanged { - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: fURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) - } - - // handle the rest of the block process asynchronously - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityBlock, - APActivityType: ap.ActivityCreate, - GTSModel: block, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) - - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) -} diff --git a/internal/processing/account/createfollow.go b/internal/processing/account/createfollow.go @@ -1,121 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -func (p *processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { - // if there's a block between the accounts we shouldn't create the request ofc - if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) - } - - // make sure the target account actually exists in our db - targetAcct, err := p.db.GetAccountByID(ctx, form.ID) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err)) - } - return nil, gtserror.NewErrorInternalError(err) - } - - // check if a follow exists already - if follows, err := p.db.IsFollowing(ctx, requestingAccount, targetAcct); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) - } else if follows { - // already follows so just return the relationship - return p.RelationshipGet(ctx, requestingAccount, form.ID) - } - - // check if a follow request exists already - if followRequested, err := p.db.IsFollowRequested(ctx, requestingAccount, targetAcct); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) - } else if followRequested { - // already follow requested so just return the relationship - return p.RelationshipGet(ctx, requestingAccount, form.ID) - } - - // check for attempt to follow self - if requestingAccount.ID == targetAcct.ID { - return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("accountfollowcreate: account %s cannot follow itself", requestingAccount.ID)) - } - - // make the follow request - newFollowID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - showReblogs := true - notify := false - fr := >smodel.FollowRequest{ - ID: newFollowID, - AccountID: requestingAccount.ID, - TargetAccountID: form.ID, - ShowReblogs: &showReblogs, - URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID), - Notify: ¬ify, - } - if form.Reblogs != nil { - fr.ShowReblogs = form.Reblogs - } - if form.Notify != nil { - fr.Notify = form.Notify - } - - // whack it in the database - if err := p.db.Put(ctx, fr); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err)) - } - - // if it's a local account that's not locked we can just straight up accept the follow request - if !*targetAcct.Locked && targetAcct.Domain == "" { - if _, err := p.db.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err)) - } - // return the new relationship - return p.RelationshipGet(ctx, requestingAccount, form.ID) - } - - // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityCreate, - GTSModel: fr, - OriginAccount: requestingAccount, - TargetAccount: targetAcct, - }) - - // return whatever relationship results from this - return p.RelationshipGet(ctx, requestingAccount, form.ID) -} diff --git a/internal/processing/account/delete.go b/internal/processing/account/delete.go @@ -34,28 +34,9 @@ import ( "golang.org/x/crypto/bcrypt" ) -// Delete handles the complete deletion of an account. -// -// To be done in this function: -// 1. Delete account's application(s), clients, and oauth tokens -// 2. Delete account's blocks -// 3. Delete account's emoji -// 4. Delete account's follow requests -// 5. Delete account's follows -// 6. Delete account's statuses -// 7. Delete account's media attachments -// 8. Delete account's mentions -// 9. Delete account's polls -// 10. Delete account's notifications -// 11. Delete account's bookmarks -// 12. Delete account's faves -// 13. Delete account's mutes -// 14. Delete account's streams -// 15. Delete account's tags -// 16. Delete account's user -// 17. Delete account's timeline -// 18. Delete account itself -func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { +// Delete deletes an account, and all of that account's statuses, media, follows, notifications, etc etc etc. +// The origin passed here should be either the ID of the account doing the delete (can be itself), or the ID of a domain block. +func (p *Processor) Delete(ctx context.Context, account *gtsmodel.Account, origin string) gtserror.WithCode { fields := kv.Fields{{"username", account.Username}} if account.Domain != "" { @@ -289,7 +270,9 @@ func (p *processor) Delete(ctx context.Context, account *gtsmodel.Account, origi return nil } -func (p *processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode { +// DeleteLocal is like Delete, but specifically for deletion of local accounts rather than federated ones. +// Unlike Delete, it will propagate the deletion out across the federating API to other instances. +func (p *Processor) DeleteLocal(ctx context.Context, account *gtsmodel.Account, form *apimodel.AccountDeleteRequest) gtserror.WithCode { fromClientAPIMessage := messages.FromClientAPI{ APObjectType: ap.ActorPerson, APActivityType: ap.ActivityDelete, diff --git a/internal/processing/account/follow.go b/internal/processing/account/follow.go @@ -0,0 +1,205 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/uris" +) + +// FollowCreate handles a follow request to an account, either remote or local. +func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) { + // if there's a block between the accounts we shouldn't create the request ofc + if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, form.ID, true); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + + // make sure the target account actually exists in our db + targetAcct, err := p.db.GetAccountByID(ctx, form.ID) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("accountfollowcreate: account %s not found in the db: %s", form.ID, err)) + } + return nil, gtserror.NewErrorInternalError(err) + } + + // check if a follow exists already + if follows, err := p.db.IsFollowing(ctx, requestingAccount, targetAcct); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow in db: %s", err)) + } else if follows { + // already follows so just return the relationship + return p.RelationshipGet(ctx, requestingAccount, form.ID) + } + + // check if a follow request exists already + if followRequested, err := p.db.IsFollowRequested(ctx, requestingAccount, targetAcct); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error checking follow request in db: %s", err)) + } else if followRequested { + // already follow requested so just return the relationship + return p.RelationshipGet(ctx, requestingAccount, form.ID) + } + + // check for attempt to follow self + if requestingAccount.ID == targetAcct.ID { + return nil, gtserror.NewErrorNotAcceptable(fmt.Errorf("accountfollowcreate: account %s cannot follow itself", requestingAccount.ID)) + } + + // make the follow request + newFollowID, err := id.NewRandomULID() + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + showReblogs := true + notify := false + fr := >smodel.FollowRequest{ + ID: newFollowID, + AccountID: requestingAccount.ID, + TargetAccountID: form.ID, + ShowReblogs: &showReblogs, + URI: uris.GenerateURIForFollow(requestingAccount.Username, newFollowID), + Notify: ¬ify, + } + if form.Reblogs != nil { + fr.ShowReblogs = form.Reblogs + } + if form.Notify != nil { + fr.Notify = form.Notify + } + + // whack it in the database + if err := p.db.Put(ctx, fr); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error creating follow request in db: %s", err)) + } + + // if it's a local account that's not locked we can just straight up accept the follow request + if !*targetAcct.Locked && targetAcct.Domain == "" { + if _, err := p.db.AcceptFollowRequest(ctx, requestingAccount.ID, form.ID); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("accountfollowcreate: error accepting folow request for local unlocked account: %s", err)) + } + // return the new relationship + return p.RelationshipGet(ctx, requestingAccount, form.ID) + } + + // otherwise we leave the follow request as it is and we handle the rest of the process asynchronously + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityCreate, + GTSModel: fr, + OriginAccount: requestingAccount, + TargetAccount: targetAcct, + }) + + // return whatever relationship results from this + return p.RelationshipGet(ctx, requestingAccount, form.ID) +} + +// FollowRemove handles the removal of a follow/follow request to an account, either remote or local. +func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { + // if there's a block between the accounts we shouldn't do anything + blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) + } + + // make sure the target account actually exists in our db + targetAcct, err := p.db.GetAccountByID(ctx, targetAccountID) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) + } + } + + // check if a follow request exists, and remove it if it does (storing the URI for later) + var frChanged bool + var frURI string + fr := >smodel.FollowRequest{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "account_id", Value: requestingAccount.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, fr); err == nil { + frURI = fr.URI + if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) + } + frChanged = true + } + + // now do the same thing for any existing follow + var fChanged bool + var fURI string + f := >smodel.Follow{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "account_id", Value: requestingAccount.ID}, + {Key: "target_account_id", Value: targetAccountID}, + }, f); err == nil { + fURI = f.URI + if err := p.db.DeleteByID(ctx, f.ID, f); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) + } + fChanged = true + } + + // follow request status changed so send the UNDO activity to the channel for async processing + if frChanged { + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityUndo, + GTSModel: >smodel.Follow{ + AccountID: requestingAccount.ID, + TargetAccountID: targetAccountID, + URI: frURI, + }, + OriginAccount: requestingAccount, + TargetAccount: targetAcct, + }) + } + + // follow status changed so send the UNDO activity to the channel for async processing + if fChanged { + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityFollow, + APActivityType: ap.ActivityUndo, + GTSModel: >smodel.Follow{ + AccountID: requestingAccount.ID, + TargetAccountID: targetAccountID, + URI: fURI, + }, + OriginAccount: requestingAccount, + TargetAccount: targetAcct, + }) + } + + // return whatever relationship results from all this + return p.RelationshipGet(ctx, requestingAccount, targetAccountID) +} diff --git a/internal/processing/account/get.go b/internal/processing/account/get.go @@ -31,7 +31,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/transport" ) -func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) { +// Get processes the given request for account information. +func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Account, gtserror.WithCode) { targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) if err != nil { if err == db.ErrNoEntries { @@ -40,10 +41,11 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err)) } - return p.getAccountFor(ctx, requestingAccount, targetAccount) + return p.getFor(ctx, requestingAccount, targetAccount) } -func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) { +// GetLocalByUsername processes the given request for account information targeting a local account by username. +func (p *Processor) GetLocalByUsername(ctx context.Context, requestingAccount *gtsmodel.Account, username string) (*apimodel.Account, gtserror.WithCode) { targetAccount, err := p.db.GetAccountByUsernameDomain(ctx, username, "") if err != nil { if err == db.ErrNoEntries { @@ -52,10 +54,11 @@ func (p *processor) GetLocalByUsername(ctx context.Context, requestingAccount *g return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error: %s", err)) } - return p.getAccountFor(ctx, requestingAccount, targetAccount) + return p.getFor(ctx, requestingAccount, targetAccount) } -func (p *processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) { +// GetCustomCSSForUsername returns custom css for the given local username. +func (p *Processor) GetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) { customCSS, err := p.db.GetAccountCustomCSSByUsername(ctx, username) if err != nil { if err == db.ErrNoEntries { @@ -67,7 +70,7 @@ func (p *processor) GetCustomCSSForUsername(ctx context.Context, username string return customCSS, nil } -func (p *processor) getAccountFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) { +func (p *Processor) getFor(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) (*apimodel.Account, gtserror.WithCode) { var blocked bool var err error if requestingAccount != nil { diff --git a/internal/processing/account/getbookmarks.go b/internal/processing/account/getbookmarks.go @@ -1,88 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -func (p *processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmodel.Account, limit int, maxID string, minID string) (*apimodel.PageableResponse, gtserror.WithCode) { - if requestingAccount == nil { - return nil, gtserror.NewErrorForbidden(fmt.Errorf("cannot retrieve bookmarks without a requesting account")) - } - - bookmarks, err := p.db.GetBookmarks(ctx, requestingAccount.ID, limit, maxID, minID) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - count := len(bookmarks) - filtered := make([]*gtsmodel.Status, 0, len(bookmarks)) - nextMaxIDValue := "" - prevMinIDValue := "" - for i, b := range bookmarks { - s, err := p.db.GetStatusByID(ctx, b.StatusID) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err == nil && visible { - if i == count-1 { - nextMaxIDValue = b.ID - } - - if i == 0 { - prevMinIDValue = b.ID - } - - filtered = append(filtered, s) - } - } - - count = len(filtered) - - if count == 0 { - return util.EmptyPageableResponse(), nil - } - - items := []interface{}{} - for _, s := range filtered { - item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) - } - items = append(items, item) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/api/v1/bookmarks", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: []string{}, - }) -} diff --git a/internal/processing/account/getfollowers.go b/internal/processing/account/getfollowers.go @@ -1,74 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) - } - - accounts := []apimodel.Account{} - follows, err := p.db.GetAccountFollowedBy(ctx, targetAccountID, false) - if err != nil { - if err == db.ErrNoEntries { - return accounts, nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - for _, f := range follows { - blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - continue - } - - if f.Account == nil { - a, err := p.db.GetAccountByID(ctx, f.AccountID) - if err != nil { - if err == db.ErrNoEntries { - continue - } - return nil, gtserror.NewErrorInternalError(err) - } - f.Account = a - } - - account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - accounts = append(accounts, *account) - } - return accounts, nil -} diff --git a/internal/processing/account/getfollowing.go b/internal/processing/account/getfollowing.go @@ -1,74 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { - if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) - } - - accounts := []apimodel.Account{} - follows, err := p.db.GetAccountFollows(ctx, targetAccountID) - if err != nil { - if err == db.ErrNoEntries { - return accounts, nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - for _, f := range follows { - blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - continue - } - - if f.TargetAccount == nil { - a, err := p.db.GetAccountByID(ctx, f.TargetAccountID) - if err != nil { - if err == db.ErrNoEntries { - continue - } - return nil, gtserror.NewErrorInternalError(err) - } - f.TargetAccount = a - } - - account, err := p.tc.AccountToAPIAccountPublic(ctx, f.TargetAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - accounts = append(accounts, *account) - } - return accounts, nil -} diff --git a/internal/processing/account/getrelationship.go b/internal/processing/account/getrelationship.go @@ -1,47 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - if requestingAccount == nil { - return nil, gtserror.NewErrorForbidden(errors.New("not authed")) - } - - gtsR, err := p.db.GetRelationship(ctx, requestingAccount.ID, targetAccountID) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) - } - - r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) - } - - return r, nil -} diff --git a/internal/processing/account/getrss.go b/internal/processing/account/getrss.go @@ -1,108 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/gorilla/feeds" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -const rssFeedLength = 20 - -func (p *processor) GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) { - account, err := p.db.GetAccountByUsernameDomain(ctx, username, "") - if err != nil { - if err == db.ErrNoEntries { - return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account not found")) - } - return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) - } - - if !*account.EnableRSS { - return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account RSS feed not enabled")) - } - - lastModified, err := p.db.GetAccountLastPosted(ctx, account.ID, true) - if err != nil { - return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) - } - - return func() (string, gtserror.WithCode) { - statuses, err := p.db.GetAccountWebStatuses(ctx, account.ID, rssFeedLength, "") - if err != nil && err != db.ErrNoEntries { - return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) - } - - author := "@" + account.Username + "@" + config.GetAccountDomain() - title := "Posts from " + author - description := "Posts from " + author - link := &feeds.Link{Href: account.URL} - - var image *feeds.Image - if account.AvatarMediaAttachmentID != "" { - if account.AvatarMediaAttachment == nil { - avatar, err := p.db.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID) - if err != nil { - return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error fetching avatar attachment: %s", err)) - } - account.AvatarMediaAttachment = avatar - } - image = &feeds.Image{ - Url: account.AvatarMediaAttachment.Thumbnail.URL, - Title: "Avatar for " + author, - Link: account.URL, - } - } - - feed := &feeds.Feed{ - Title: title, - Description: description, - Link: link, - Image: image, - } - - for i, s := range statuses { - // take the date of the first (ie., latest) status as feed updated value - if i == 0 { - feed.Updated = s.UpdatedAt - } - - item, err := p.tc.StatusToRSSItem(ctx, s) - if err != nil { - return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting status to feed item: %s", err)) - } - - feed.Add(item) - } - - rss, err := feed.ToRss() - if err != nil { - return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting feed to rss string: %s", err)) - } - - return rss, nil - }, lastModified, nil -} diff --git a/internal/processing/account/getstatuses.go b/internal/processing/account/getstatuses.go @@ -1,155 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -func (p *processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) { - if requestingAccount != nil { - if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } else if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) - } - } - - statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) - if err != nil { - if err == db.ErrNoEntries { - return util.EmptyPageableResponse(), nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - var filtered []*gtsmodel.Status - for _, s := range statuses { - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err == nil && visible { - filtered = append(filtered, s) - } - } - - count := len(filtered) - - if count == 0 { - return util.EmptyPageableResponse(), nil - } - - items := []interface{}{} - nextMaxIDValue := "" - prevMinIDValue := "" - for i, s := range filtered { - item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) - } - - if i == count-1 { - nextMaxIDValue = item.GetID() - } - - if i == 0 { - prevMinIDValue = item.GetID() - } - - items = append(items, item) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID), - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: []string{ - fmt.Sprintf("exclude_replies=%t", excludeReplies), - fmt.Sprintf("exclude_reblogs=%t", excludeReblogs), - fmt.Sprintf("pinned_only=%t", pinnedOnly), - fmt.Sprintf("only_media=%t", mediaOnly), - fmt.Sprintf("only_public=%t", publicOnly), - }, - }) -} - -func (p *processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) { - acct, err := p.db.GetAccountByID(ctx, targetAccountID) - if err != nil { - if err == db.ErrNoEntries { - err := fmt.Errorf("account %s not found in the db, not getting web statuses for it", targetAccountID) - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - if acct.Domain != "" { - err := fmt.Errorf("account %s was not a local account, not getting web statuses for it", targetAccountID) - return nil, gtserror.NewErrorNotFound(err) - } - - statuses, err := p.db.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID) - if err != nil { - if err == db.ErrNoEntries { - return util.EmptyPageableResponse(), nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - count := len(statuses) - - if count == 0 { - return util.EmptyPageableResponse(), nil - } - - items := []interface{}{} - nextMaxIDValue := "" - prevMinIDValue := "" - for i, s := range statuses { - item, err := p.tc.StatusToAPIStatus(ctx, s, nil) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) - } - - if i == count-1 { - nextMaxIDValue = item.GetID() - } - - if i == 0 { - prevMinIDValue = item.GetID() - } - - items = append(items, item) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/@" + acct.Username, - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - ExtraQueryParams: []string{}, - }) -} diff --git a/internal/processing/account/relationships.go b/internal/processing/account/relationships.go @@ -0,0 +1,141 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "errors" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// FollowersGet fetches a list of the target account's followers. +func (p *Processor) FollowersGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { + if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + + accounts := []apimodel.Account{} + follows, err := p.db.GetAccountFollowedBy(ctx, targetAccountID, false) + if err != nil { + if err == db.ErrNoEntries { + return accounts, nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + for _, f := range follows { + blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if blocked { + continue + } + + if f.Account == nil { + a, err := p.db.GetAccountByID(ctx, f.AccountID) + if err != nil { + if err == db.ErrNoEntries { + continue + } + return nil, gtserror.NewErrorInternalError(err) + } + f.Account = a + } + + account, err := p.tc.AccountToAPIAccountPublic(ctx, f.Account) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + accounts = append(accounts, *account) + } + return accounts, nil +} + +// FollowingGet fetches a list of the accounts that target account is following. +func (p *Processor) FollowingGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) { + if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + + accounts := []apimodel.Account{} + follows, err := p.db.GetAccountFollows(ctx, targetAccountID) + if err != nil { + if err == db.ErrNoEntries { + return accounts, nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + for _, f := range follows { + blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, f.AccountID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if blocked { + continue + } + + if f.TargetAccount == nil { + a, err := p.db.GetAccountByID(ctx, f.TargetAccountID) + if err != nil { + if err == db.ErrNoEntries { + continue + } + return nil, gtserror.NewErrorInternalError(err) + } + f.TargetAccount = a + } + + account, err := p.tc.AccountToAPIAccountPublic(ctx, f.TargetAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + accounts = append(accounts, *account) + } + return accounts, nil +} + +// RelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. +func (p *Processor) RelationshipGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { + if requestingAccount == nil { + return nil, gtserror.NewErrorForbidden(errors.New("not authed")) + } + + gtsR, err := p.db.GetRelationship(ctx, requestingAccount.ID, targetAccountID) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error getting relationship: %s", err)) + } + + r, err := p.tc.RelationshipToAPIRelationship(ctx, gtsR) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting relationship: %s", err)) + } + + return r, nil +} diff --git a/internal/processing/account/removeblock.go b/internal/processing/account/removeblock.go @@ -1,65 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "errors" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/messages" -) - -func (p *processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // make sure the target account actually exists in our db - targetAccount, err := p.db.GetAccountByID(ctx, targetAccountID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("BlockCreate: error getting account %s from the db: %s", targetAccountID, err)) - } - - // check if a block exists, and remove it if it does - block, err := p.db.GetBlock(ctx, requestingAccount.ID, targetAccountID) - if err == nil { - // we got a block, remove it - block.Account = requestingAccount - block.TargetAccount = targetAccount - if err := p.db.DeleteBlockByID(ctx, block.ID); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error removing block from db: %s", err)) - } - - // send the UNDO activity to the client worker for async processing - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityBlock, - APActivityType: ap.ActivityUndo, - GTSModel: block, - OriginAccount: requestingAccount, - TargetAccount: targetAccount, - }) - } else if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("BlockRemove: error getting possible block from db: %s", err)) - } - - // return whatever relationship results from all this - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) -} diff --git a/internal/processing/account/removefollow.go b/internal/processing/account/removefollow.go @@ -1,113 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 account - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/messages" -) - -func (p *processor) FollowRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) { - // if there's a block between the accounts we shouldn't do anything - blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: block exists between accounts")) - } - - // make sure the target account actually exists in our db - targetAcct, err := p.db.GetAccountByID(ctx, targetAccountID) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("AccountFollowRemove: account %s not found in the db: %s", targetAccountID, err)) - } - } - - // check if a follow request exists, and remove it if it does (storing the URI for later) - var frChanged bool - var frURI string - fr := >smodel.FollowRequest{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, fr); err == nil { - frURI = fr.URI - if err := p.db.DeleteByID(ctx, fr.ID, fr); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow request from db: %s", err)) - } - frChanged = true - } - - // now do the same thing for any existing follow - var fChanged bool - var fURI string - f := >smodel.Follow{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "account_id", Value: requestingAccount.ID}, - {Key: "target_account_id", Value: targetAccountID}, - }, f); err == nil { - fURI = f.URI - if err := p.db.DeleteByID(ctx, f.ID, f); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("AccountFollowRemove: error removing follow from db: %s", err)) - } - fChanged = true - } - - // follow request status changed so send the UNDO activity to the channel for async processing - if frChanged { - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: frURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAcct, - }) - } - - // follow status changed so send the UNDO activity to the channel for async processing - if fChanged { - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityFollow, - APActivityType: ap.ActivityUndo, - GTSModel: >smodel.Follow{ - AccountID: requestingAccount.ID, - TargetAccountID: targetAccountID, - URI: fURI, - }, - OriginAccount: requestingAccount, - TargetAccount: targetAcct, - }) - } - - // return whatever relationship results from all this - return p.RelationshipGet(ctx, requestingAccount, targetAccountID) -} diff --git a/internal/processing/account/rss.go b/internal/processing/account/rss.go @@ -0,0 +1,109 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/gorilla/feeds" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +const rssFeedLength = 20 + +// GetRSSFeedForUsername returns RSS feed for the given local username. +func (p *Processor) GetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) { + account, err := p.db.GetAccountByUsernameDomain(ctx, username, "") + if err != nil { + if err == db.ErrNoEntries { + return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account not found")) + } + return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + if !*account.EnableRSS { + return nil, time.Time{}, gtserror.NewErrorNotFound(errors.New("GetRSSFeedForUsername: account RSS feed not enabled")) + } + + lastModified, err := p.db.GetAccountLastPosted(ctx, account.ID, true) + if err != nil { + return nil, time.Time{}, gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + return func() (string, gtserror.WithCode) { + statuses, err := p.db.GetAccountWebStatuses(ctx, account.ID, rssFeedLength, "") + if err != nil && err != db.ErrNoEntries { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error: %s", err)) + } + + author := "@" + account.Username + "@" + config.GetAccountDomain() + title := "Posts from " + author + description := "Posts from " + author + link := &feeds.Link{Href: account.URL} + + var image *feeds.Image + if account.AvatarMediaAttachmentID != "" { + if account.AvatarMediaAttachment == nil { + avatar, err := p.db.GetAttachmentByID(ctx, account.AvatarMediaAttachmentID) + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: db error fetching avatar attachment: %s", err)) + } + account.AvatarMediaAttachment = avatar + } + image = &feeds.Image{ + Url: account.AvatarMediaAttachment.Thumbnail.URL, + Title: "Avatar for " + author, + Link: account.URL, + } + } + + feed := &feeds.Feed{ + Title: title, + Description: description, + Link: link, + Image: image, + } + + for i, s := range statuses { + // take the date of the first (ie., latest) status as feed updated value + if i == 0 { + feed.Updated = s.UpdatedAt + } + + item, err := p.tc.StatusToRSSItem(ctx, s) + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting status to feed item: %s", err)) + } + + feed.Add(item) + } + + rss, err := feed.ToRss() + if err != nil { + return "", gtserror.NewErrorInternalError(fmt.Errorf("GetRSSFeedForUsername: error converting feed to rss string: %s", err)) + } + + return rss, nil + }, lastModified, nil +} diff --git a/internal/processing/account/getrss_test.go b/internal/processing/account/rss_test.go diff --git a/internal/processing/account/statuses.go b/internal/processing/account/statuses.go @@ -0,0 +1,159 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 account + +import ( + "context" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// StatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for +// the account given in authed. +func (p *Processor) StatusesGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinnedOnly bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) { + if requestingAccount != nil { + if blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, targetAccountID, true); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } else if blocked { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("block exists between accounts")) + } + } + + statuses, err := p.db.GetAccountStatuses(ctx, targetAccountID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly) + if err != nil { + if err == db.ErrNoEntries { + return util.EmptyPageableResponse(), nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + var filtered []*gtsmodel.Status + for _, s := range statuses { + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err == nil && visible { + filtered = append(filtered, s) + } + } + + count := len(filtered) + + if count == 0 { + return util.EmptyPageableResponse(), nil + } + + items := []interface{}{} + nextMaxIDValue := "" + prevMinIDValue := "" + for i, s := range filtered { + item, err := p.tc.StatusToAPIStatus(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) + } + + if i == count-1 { + nextMaxIDValue = item.GetID() + } + + if i == 0 { + prevMinIDValue = item.GetID() + } + + items = append(items, item) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: fmt.Sprintf("/api/v1/accounts/%s/statuses", targetAccountID), + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + Limit: limit, + ExtraQueryParams: []string{ + fmt.Sprintf("exclude_replies=%t", excludeReplies), + fmt.Sprintf("exclude_reblogs=%t", excludeReblogs), + fmt.Sprintf("pinned_only=%t", pinnedOnly), + fmt.Sprintf("only_media=%t", mediaOnly), + fmt.Sprintf("only_public=%t", publicOnly), + }, + }) +} + +// WebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only +// statuses which are suitable for showing on the public web profile of an account. +func (p *Processor) WebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) { + acct, err := p.db.GetAccountByID(ctx, targetAccountID) + if err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("account %s not found in the db, not getting web statuses for it", targetAccountID) + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + if acct.Domain != "" { + err := fmt.Errorf("account %s was not a local account, not getting web statuses for it", targetAccountID) + return nil, gtserror.NewErrorNotFound(err) + } + + statuses, err := p.db.GetAccountWebStatuses(ctx, targetAccountID, 10, maxID) + if err != nil { + if err == db.ErrNoEntries { + return util.EmptyPageableResponse(), nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(statuses) + + if count == 0 { + return util.EmptyPageableResponse(), nil + } + + items := []interface{}{} + nextMaxIDValue := "" + prevMinIDValue := "" + for i, s := range statuses { + item, err := p.tc.StatusToAPIStatus(ctx, s, nil) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status to api: %s", err)) + } + + if i == count-1 { + nextMaxIDValue = item.GetID() + } + + if i == 0 { + prevMinIDValue = item.GetID() + } + + items = append(items, item) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "/@" + acct.Username, + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + ExtraQueryParams: []string{}, + }) +} diff --git a/internal/processing/account/update.go b/internal/processing/account/update.go @@ -33,10 +33,12 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/validate" ) -func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { +// Update processes the update of an account with the given form. +func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) { if form.Discoverable != nil { account.Discoverable = form.Discoverable } @@ -138,7 +140,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form if err := validate.Privacy(*form.Source.Privacy); err != nil { return nil, gtserror.NewErrorBadRequest(err) } - privacy := p.tc.APIVisToVis(apimodel.Visibility(*form.Source.Privacy)) + privacy := typeutils.APIVisToVis(apimodel.Visibility(*form.Source.Privacy)) account.Privacy = privacy } @@ -185,7 +187,7 @@ func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, form // UpdateAvatar does the dirty work of checking the avatar part of an account update form, // parsing and checking the image, and doing the necessary updates in the database for this to become // the account's new avatar image. -func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) { +func (p *Processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) { maxImageSize := config.GetMediaImageMaxSize() if avatar.Size > int64(maxImageSize) { return nil, fmt.Errorf("UpdateAvatar: avatar with size %d exceeded max image size of %d bytes", avatar.Size, maxImageSize) @@ -213,7 +215,7 @@ func (p *processor) UpdateAvatar(ctx context.Context, avatar *multipart.FileHead // UpdateHeader does the dirty work of checking the header part of an account update form, // parsing and checking the image, and doing the necessary updates in the database for this to become // the account's new header image. -func (p *processor) UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) { +func (p *Processor) UpdateHeader(ctx context.Context, header *multipart.FileHeader, description *string, accountID string) (*gtsmodel.MediaAttachment, error) { maxImageSize := config.GetMediaImageMaxSize() if header.Size > int64(maxImageSize) { return nil, fmt.Errorf("UpdateHeader: header with size %d exceeded max image size of %d bytes", header.Size, maxImageSize) diff --git a/internal/processing/account_test.go b/internal/processing/account_test.go @@ -53,7 +53,7 @@ func (suite *AccountTestSuite) TestAccountDeleteLocal() { err := suite.db.Put(ctx, follow) suite.NoError(err) - errWithCode := suite.processor.AccountDeleteLocal(ctx, suite.testAutheds["local_account_1"], &apimodel.AccountDeleteRequest{ + errWithCode := suite.processor.Account().DeleteLocal(ctx, suite.testAccounts["local_account_1"], &apimodel.AccountDeleteRequest{ Password: "password", DeleteOriginID: deletingAccount.ID, }) diff --git a/internal/processing/admin.go b/internal/processing/admin.go @@ -1,95 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) AdminAccountAction(ctx context.Context, authed *oauth.Auth, form *apimodel.AdminAccountActionRequest) gtserror.WithCode { - return p.adminProcessor.AccountAction(ctx, authed.Account, form) -} - -func (p *processor) AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) { - return p.adminProcessor.EmojiCreate(ctx, authed.Account, authed.User, form) -} - -func (p *processor) AdminEmojisGet(ctx context.Context, authed *oauth.Auth, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.adminProcessor.EmojisGet(ctx, authed.Account, authed.User, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) -} - -func (p *processor) AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { - return p.adminProcessor.EmojiGet(ctx, authed.Account, authed.User, id) -} - -func (p *processor) AdminEmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) { - return p.adminProcessor.EmojiUpdate(ctx, id, form) -} - -func (p *processor) AdminEmojiDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { - return p.adminProcessor.EmojiDelete(ctx, id) -} - -func (p *processor) AdminEmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) { - return p.adminProcessor.EmojiCategoriesGet(ctx) -} - -func (p *processor) AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) { - return p.adminProcessor.DomainBlockCreate(ctx, authed.Account, form.Domain, form.Obfuscate, form.PublicComment, form.PrivateComment, "") -} - -func (p *processor) AdminDomainBlocksImport(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) ([]*apimodel.DomainBlock, gtserror.WithCode) { - return p.adminProcessor.DomainBlocksImport(ctx, authed.Account, form.Domains) -} - -func (p *processor) AdminDomainBlocksGet(ctx context.Context, authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { - return p.adminProcessor.DomainBlocksGet(ctx, authed.Account, export) -} - -func (p *processor) AdminDomainBlockGet(ctx context.Context, authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { - return p.adminProcessor.DomainBlockGet(ctx, authed.Account, id, export) -} - -func (p *processor) AdminDomainBlockDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) { - return p.adminProcessor.DomainBlockDelete(ctx, authed.Account, id) -} - -func (p *processor) AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode { - return p.adminProcessor.MediaPrune(ctx, mediaRemoteCacheDays) -} - -func (p *processor) AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode { - return p.adminProcessor.MediaRefetch(ctx, authed.Account, domain) -} - -func (p *processor) AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.adminProcessor.ReportsGet(ctx, authed.Account, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit) -} - -func (p *processor) AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode) { - return p.adminProcessor.ReportGet(ctx, authed.Account, id) -} - -func (p *processor) AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) { - return p.adminProcessor.ReportResolve(ctx, authed.Account, id, actionTakenComment) -} diff --git a/internal/processing/admin/account.go b/internal/processing/admin/account.go @@ -0,0 +1,65 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 admin + +import ( + "context" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/messages" +) + +func (p *Processor) AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode { + targetAccount, err := p.db.GetAccountByID(ctx, form.TargetAccountID) + if err != nil { + return gtserror.NewErrorInternalError(err) + } + + adminAction := >smodel.AdminAccountAction{ + ID: id.NewULID(), + AccountID: account.ID, + TargetAccountID: targetAccount.ID, + Text: form.Text, + } + + switch form.Type { + case string(gtsmodel.AdminActionSuspend): + adminAction.Type = gtsmodel.AdminActionSuspend + // pass the account delete through the client api channel for processing + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActorPerson, + APActivityType: ap.ActivityDelete, + OriginAccount: account, + TargetAccount: targetAccount, + }) + default: + return gtserror.NewErrorBadRequest(fmt.Errorf("admin action type %s is not supported for this endpoint", form.Type)) + } + + if err := p.db.Put(ctx, adminAction); err != nil { + return gtserror.NewErrorInternalError(err) + } + + return nil +} diff --git a/internal/processing/admin/accountaction.go b/internal/processing/admin/accountaction.go @@ -1,47 +0,0 @@ -package admin - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/messages" -) - -func (p *processor) AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode { - targetAccount, err := p.db.GetAccountByID(ctx, form.TargetAccountID) - if err != nil { - return gtserror.NewErrorInternalError(err) - } - - adminAction := >smodel.AdminAccountAction{ - ID: id.NewULID(), - AccountID: account.ID, - TargetAccountID: targetAccount.ID, - Text: form.Text, - } - - switch form.Type { - case string(gtsmodel.AdminActionSuspend): - adminAction.Type = gtsmodel.AdminActionSuspend - // pass the account delete through the client api channel for processing - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActorPerson, - APActivityType: ap.ActivityDelete, - OriginAccount: account, - TargetAccount: targetAccount, - }) - default: - return gtserror.NewErrorBadRequest(fmt.Errorf("admin action type %s is not supported for this endpoint", form.Type)) - } - - if err := p.db.Put(ctx, adminAction); err != nil { - return gtserror.NewErrorInternalError(err) - } - - return nil -} diff --git a/internal/processing/admin/admin.go b/internal/processing/admin/admin.go @@ -19,14 +19,8 @@ package admin import ( - "context" - "mime/multipart" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/storage" @@ -34,28 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) -// Processor wraps a bunch of functions for processing admin actions. -type Processor interface { - DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) - DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) - DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) - DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) - DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) - AccountAction(ctx context.Context, account *gtsmodel.Account, form *apimodel.AdminAccountActionRequest) gtserror.WithCode - EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) - EmojisGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) - EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) - EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) - EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) - MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode - MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode - ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) - ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) -} - -type processor struct { +type Processor struct { tc typeutils.TypeConverter mediaManager media.Manager transportController transport.Controller @@ -66,7 +39,7 @@ type processor struct { // New returns a new admin processor. func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller, storage *storage.Driver, clientWorker *concurrency.WorkerPool[messages.FromClientAPI]) Processor { - return &processor{ + return Processor{ tc: tc, mediaManager: mediaManager, transportController: transportController, diff --git a/internal/processing/admin/createdomainblock.go b/internal/processing/admin/createdomainblock.go @@ -1,176 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - "codeberg.org/gruf/go-kv" - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/text" -) - -func (p *processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) { - // domain blocks will always be lowercase - domain = strings.ToLower(domain) - - // first check if we already have a block -- if err == nil we already had a block so we can skip a whole lot of work - block, err := p.db.GetDomainBlock(ctx, domain) - if err != nil { - if !errors.Is(err, db.ErrNoEntries) { - // something went wrong in the DB - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error checking for existence of domain block %s: %s", domain, err)) - } - - // there's no block for this domain yet so create one - newBlock := >smodel.DomainBlock{ - ID: id.NewULID(), - Domain: domain, - CreatedByAccountID: account.ID, - PrivateComment: text.SanitizePlaintext(privateComment), - PublicComment: text.SanitizePlaintext(publicComment), - Obfuscate: &obfuscate, - SubscriptionID: subscriptionID, - } - - // Insert the new block into the database - if err := p.db.CreateDomainBlock(ctx, newBlock); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error putting new domain block %s: %s", domain, err)) - } - - // Set the newly created block - block = newBlock - - // Process the side effects of the domain block asynchronously since it might take a while - go func() { - p.initiateDomainBlockSideEffects(context.Background(), account, block) - }() - } - - // Convert our gts model domain block into an API model - apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, block, false) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting domain block to frontend/api representation %s: %s", domain, err)) - } - - return apiDomainBlock, nil -} - -// initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block: -// -// 1. Strip most info away from the instance entry for the domain. -// 2. Delete the instance account for that instance if it exists. -// 3. Select all accounts from this instance and pass them through the delete functionality of the processor. -func (p *processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) { - l := log.WithContext(ctx). - WithFields(kv.Fields{ - {"domain", block.Domain}, - }...) - - l.Debug("processing domain block side effects") - - // if we have an instance entry for this domain, update it with the new block ID and clear all fields - instance := >smodel.Instance{} - if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: block.Domain}}, instance); err == nil { - updatingColumns := []string{ - "title", - "updated_at", - "suspended_at", - "domain_block_id", - "short_description", - "description", - "terms", - "contact_email", - "contact_account_username", - "contact_account_id", - "version", - } - instance.Title = "" - instance.UpdatedAt = time.Now() - instance.SuspendedAt = time.Now() - instance.DomainBlockID = block.ID - instance.ShortDescription = "" - instance.Description = "" - instance.Terms = "" - instance.ContactEmail = "" - instance.ContactAccountUsername = "" - instance.ContactAccountID = "" - instance.Version = "" - if err := p.db.UpdateByID(ctx, instance, instance.ID, updatingColumns...); err != nil { - l.Errorf("domainBlockProcessSideEffects: db error updating instance: %s", err) - } - l.Debug("domainBlockProcessSideEffects: instance entry updated") - } - - // if we have an instance account for this instance, delete it - if instanceAccount, err := p.db.GetAccountByUsernameDomain(ctx, block.Domain, block.Domain); err == nil { - if err := p.db.DeleteAccount(ctx, instanceAccount.ID); err != nil { - l.Errorf("domainBlockProcessSideEffects: db error deleting instance account: %s", err) - } - } - - // delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines) - - limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query - var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID) - -selectAccountsLoop: - for { - accounts, err := p.db.GetInstanceAccounts(ctx, block.Domain, maxID, limit) - if err != nil { - if err == db.ErrNoEntries { - // no accounts left for this instance so we're done - l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain) - break selectAccountsLoop - } - // an actual error has occurred - l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err) - break selectAccountsLoop - } - - for i, a := range accounts { - l.Debugf("putting delete for account %s in the clientAPI channel", a.Username) - - // pass the account delete through the client api channel for processing - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActorPerson, - APActivityType: ap.ActivityDelete, - GTSModel: block, - OriginAccount: account, - TargetAccount: a, - }) - - // if this is the last account in the slice, set the maxID appropriately for the next query - if i == len(accounts)-1 { - maxID = a.ID - } - } - } -} diff --git a/internal/processing/admin/createemoji.go b/internal/processing/admin/createemoji.go @@ -1,89 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - "io" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -func (p *processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) { - if !*user.Admin { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") - } - - maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "") - if maybeExisting != nil { - return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode)) - } - - if err != nil && err != db.ErrNoEntries { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err)) - } - - emojiID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID") - } - - emojiURI := uris.GenerateURIForEmoji(emojiID) - - data := func(innerCtx context.Context) (io.ReadCloser, int64, error) { - f, err := form.Image.Open() - return f, form.Image.Size, err - } - - var ai *media.AdditionalEmojiInfo - if form.CategoryName != "" { - category, err := p.GetOrCreateEmojiCategory(ctx, form.CategoryName) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting id in category: %s", err), "error putting id in category") - } - - ai = &media.AdditionalEmojiInfo{ - CategoryID: &category.ID, - } - } - - processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, ai, false) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji") - } - - emoji, err := processingEmoji.LoadEmoji(ctx) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji") - } - - apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation") - } - - return &apiEmoji, nil -} diff --git a/internal/processing/admin/deletedomainblock.go b/internal/processing/admin/deletedomainblock.go @@ -1,86 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - "time" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) { - domainBlock := >smodel.DomainBlock{} - - if err := p.db.GetByID(ctx, id, domainBlock); err != nil { - if err != db.ErrNoEntries { - // something has gone really wrong - return nil, gtserror.NewErrorInternalError(err) - } - // there are no entries for this ID - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) - } - - // prepare the domain block to return - apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // Delete the domain block - if err := p.db.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // remove the domain block reference from the instance, if we have an entry for it - i := >smodel.Instance{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "domain", Value: domainBlock.Domain}, - {Key: "domain_block_id", Value: id}, - }, i); err == nil { - updatingColumns := []string{"suspended_at", "domain_block_id", "updated_at"} - i.SuspendedAt = time.Time{} - i.DomainBlockID = "" - i.UpdatedAt = time.Now() - if err := p.db.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("couldn't update database entry for instance %s: %s", domainBlock.Domain, err)) - } - } - - // unsuspend all accounts whose suspension origin was this domain block - // 1. remove the 'suspended_at' entry from their accounts - if err := p.db.UpdateWhere(ctx, []db.Where{ - {Key: "suspension_origin", Value: domainBlock.ID}, - }, "suspended_at", nil, &[]*gtsmodel.Account{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspended_at from accounts: %s", err)) - } - - // 2. remove the 'suspension_origin' entry from their accounts - if err := p.db.UpdateWhere(ctx, []db.Where{ - {Key: "suspension_origin", Value: domainBlock.ID}, - }, "suspension_origin", nil, &[]*gtsmodel.Account{}); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspension_origin from accounts: %s", err)) - } - - return apiDomainBlock, nil -} diff --git a/internal/processing/admin/deleteemoji.go b/internal/processing/admin/deleteemoji.go @@ -1,59 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -func (p *processor) EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { - emoji, err := p.db.GetEmojiByID(ctx, id) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - err = fmt.Errorf("EmojiDelete: no emoji with id %s found in the db", id) - return nil, gtserror.NewErrorNotFound(err) - } - err := fmt.Errorf("EmojiDelete: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - if emoji.Domain != "" { - err = fmt.Errorf("EmojiDelete: emoji with id %s was not a local emoji, will not delete", id) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) - if err != nil { - err = fmt.Errorf("EmojiDelete: error converting emoji to admin api emoji: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - if err := p.db.DeleteEmojiByID(ctx, id); err != nil { - err := fmt.Errorf("EmojiDelete: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - return adminEmoji, nil -} diff --git a/internal/processing/admin/domainblock.go b/internal/processing/admin/domainblock.go @@ -0,0 +1,294 @@ +package admin + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "mime/multipart" + "strings" + "time" + + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/ap" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/messages" + "github.com/superseriousbusiness/gotosocial/internal/text" +) + +func (p *Processor) DomainBlockCreate(ctx context.Context, account *gtsmodel.Account, domain string, obfuscate bool, publicComment string, privateComment string, subscriptionID string) (*apimodel.DomainBlock, gtserror.WithCode) { + // domain blocks will always be lowercase + domain = strings.ToLower(domain) + + // first check if we already have a block -- if err == nil we already had a block so we can skip a whole lot of work + block, err := p.db.GetDomainBlock(ctx, domain) + if err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // something went wrong in the DB + return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error checking for existence of domain block %s: %s", domain, err)) + } + + // there's no block for this domain yet so create one + newBlock := >smodel.DomainBlock{ + ID: id.NewULID(), + Domain: domain, + CreatedByAccountID: account.ID, + PrivateComment: text.SanitizePlaintext(privateComment), + PublicComment: text.SanitizePlaintext(publicComment), + Obfuscate: &obfuscate, + SubscriptionID: subscriptionID, + } + + // Insert the new block into the database + if err := p.db.CreateDomainBlock(ctx, newBlock); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error putting new domain block %s: %s", domain, err)) + } + + // Set the newly created block + block = newBlock + + // Process the side effects of the domain block asynchronously since it might take a while + go func() { + p.initiateDomainBlockSideEffects(context.Background(), account, block) + }() + } + + // Convert our gts model domain block into an API model + apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, block, false) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting domain block to frontend/api representation %s: %s", domain, err)) + } + + return apiDomainBlock, nil +} + +// initiateDomainBlockSideEffects should be called asynchronously, to process the side effects of a domain block: +// +// 1. Strip most info away from the instance entry for the domain. +// 2. Delete the instance account for that instance if it exists. +// 3. Select all accounts from this instance and pass them through the delete functionality of the processor. +func (p *Processor) initiateDomainBlockSideEffects(ctx context.Context, account *gtsmodel.Account, block *gtsmodel.DomainBlock) { + l := log.WithContext(ctx).WithFields(kv.Fields{{"domain", block.Domain}}...) + l.Debug("processing domain block side effects") + + // if we have an instance entry for this domain, update it with the new block ID and clear all fields + instance := >smodel.Instance{} + if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: block.Domain}}, instance); err == nil { + updatingColumns := []string{ + "title", + "updated_at", + "suspended_at", + "domain_block_id", + "short_description", + "description", + "terms", + "contact_email", + "contact_account_username", + "contact_account_id", + "version", + } + instance.Title = "" + instance.UpdatedAt = time.Now() + instance.SuspendedAt = time.Now() + instance.DomainBlockID = block.ID + instance.ShortDescription = "" + instance.Description = "" + instance.Terms = "" + instance.ContactEmail = "" + instance.ContactAccountUsername = "" + instance.ContactAccountID = "" + instance.Version = "" + if err := p.db.UpdateByID(ctx, instance, instance.ID, updatingColumns...); err != nil { + l.Errorf("domainBlockProcessSideEffects: db error updating instance: %s", err) + } + l.Debug("domainBlockProcessSideEffects: instance entry updated") + } + + // if we have an instance account for this instance, delete it + if instanceAccount, err := p.db.GetAccountByUsernameDomain(ctx, block.Domain, block.Domain); err == nil { + if err := p.db.DeleteAccount(ctx, instanceAccount.ID); err != nil { + l.Errorf("domainBlockProcessSideEffects: db error deleting instance account: %s", err) + } + } + + // delete accounts through the normal account deletion system (which should also delete media + posts + remove posts from timelines) + + limit := 20 // just select 20 accounts at a time so we don't nuke our DB/mem with one huge query + var maxID string // this is initially an empty string so we'll start at the top of accounts list (sorted by ID) + +selectAccountsLoop: + for { + accounts, err := p.db.GetInstanceAccounts(ctx, block.Domain, maxID, limit) + if err != nil { + if err == db.ErrNoEntries { + // no accounts left for this instance so we're done + l.Infof("domainBlockProcessSideEffects: done iterating through accounts for domain %s", block.Domain) + break selectAccountsLoop + } + // an actual error has occurred + l.Errorf("domainBlockProcessSideEffects: db error selecting accounts for domain %s: %s", block.Domain, err) + break selectAccountsLoop + } + + for i, a := range accounts { + l.Debugf("putting delete for account %s in the clientAPI channel", a.Username) + + // pass the account delete through the client api channel for processing + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActorPerson, + APActivityType: ap.ActivityDelete, + GTSModel: block, + OriginAccount: account, + TargetAccount: a, + }) + + // if this is the last account in the slice, set the maxID appropriately for the next query + if i == len(accounts)-1 { + maxID = a.ID + } + } + } +} + +// DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file. +func (p *Processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) { + f, err := domains.Open() + if err != nil { + return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err)) + } + buf := new(bytes.Buffer) + size, err := io.Copy(buf, f) + if err != nil { + return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error reading attachment: %s", err)) + } + if size == 0 { + return nil, gtserror.NewErrorBadRequest(errors.New("DomainBlocksImport: could not read provided attachment: size 0 bytes")) + } + + d := []apimodel.DomainBlock{} + if err := json.Unmarshal(buf.Bytes(), &d); err != nil { + return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: could not read provided attachment: %s", err)) + } + + blocks := []*apimodel.DomainBlock{} + for _, d := range d { + block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "") + if err != nil { + return nil, err + } + + blocks = append(blocks, block) + } + + return blocks, nil +} + +// DomainBlocksGet returns all existing domain blocks. +// If export is true, the format will be suitable for writing out to an export. +func (p *Processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { + domainBlocks := []*gtsmodel.DomainBlock{} + + if err := p.db.GetAll(ctx, &domainBlocks); err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // something has gone really wrong + return nil, gtserror.NewErrorInternalError(err) + } + } + + apiDomainBlocks := []*apimodel.DomainBlock{} + for _, b := range domainBlocks { + apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, b, export) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock) + } + + return apiDomainBlocks, nil +} + +// DomainBlockGet returns one domain block with the given id. +// If export is true, the format will be suitable for writing out to an export. +func (p *Processor) DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { + domainBlock := >smodel.DomainBlock{} + + if err := p.db.GetByID(ctx, id, domainBlock); err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // something has gone really wrong + return nil, gtserror.NewErrorInternalError(err) + } + // there are no entries for this ID + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) + } + + apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, export) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return apiDomainBlock, nil +} + +// DomainBlockDelete removes one domain block with the given ID. +func (p *Processor) DomainBlockDelete(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.DomainBlock, gtserror.WithCode) { + domainBlock := >smodel.DomainBlock{} + + if err := p.db.GetByID(ctx, id, domainBlock); err != nil { + if !errors.Is(err, db.ErrNoEntries) { + // something has gone really wrong + return nil, gtserror.NewErrorInternalError(err) + } + // there are no entries for this ID + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) + } + + // prepare the domain block to return + apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, false) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + // Delete the domain block + if err := p.db.DeleteDomainBlock(ctx, domainBlock.Domain); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + // remove the domain block reference from the instance, if we have an entry for it + i := >smodel.Instance{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "domain", Value: domainBlock.Domain}, + {Key: "domain_block_id", Value: id}, + }, i); err == nil { + updatingColumns := []string{"suspended_at", "domain_block_id", "updated_at"} + i.SuspendedAt = time.Time{} + i.DomainBlockID = "" + i.UpdatedAt = time.Now() + if err := p.db.UpdateByID(ctx, i, i.ID, updatingColumns...); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("couldn't update database entry for instance %s: %s", domainBlock.Domain, err)) + } + } + + // unsuspend all accounts whose suspension origin was this domain block + // 1. remove the 'suspended_at' entry from their accounts + if err := p.db.UpdateWhere(ctx, []db.Where{ + {Key: "suspension_origin", Value: domainBlock.ID}, + }, "suspended_at", nil, &[]*gtsmodel.Account{}); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspended_at from accounts: %s", err)) + } + + // 2. remove the 'suspension_origin' entry from their accounts + if err := p.db.UpdateWhere(ctx, []db.Where{ + {Key: "suspension_origin", Value: domainBlock.ID}, + }, "suspension_origin", nil, &[]*gtsmodel.Account{}); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("database error removing suspension_origin from accounts: %s", err)) + } + + return apiDomainBlock, nil +} diff --git a/internal/processing/admin/emoji.go b/internal/processing/admin/emoji.go @@ -0,0 +1,485 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 admin + +import ( + "context" + "errors" + "fmt" + "io" + "mime/multipart" + "strings" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/media" + "github.com/superseriousbusiness/gotosocial/internal/uris" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// EmojiCreate creates a custom emoji on this instance. +func (p *Processor) EmojiCreate(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) { + if !*user.Admin { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") + } + + maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, form.Shortcode, "") + if maybeExisting != nil { + return nil, gtserror.NewErrorConflict(fmt.Errorf("emoji with shortcode %s already exists", form.Shortcode), fmt.Sprintf("emoji with shortcode %s already exists", form.Shortcode)) + } + + if err != nil && err != db.ErrNoEntries { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking existence of emoji with shortcode %s: %s", form.Shortcode, err)) + } + + emojiID, err := id.NewRandomULID() + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error creating id for new emoji: %s", err), "error creating emoji ID") + } + + emojiURI := uris.GenerateURIForEmoji(emojiID) + + data := func(innerCtx context.Context) (io.ReadCloser, int64, error) { + f, err := form.Image.Open() + return f, form.Image.Size, err + } + + var ai *media.AdditionalEmojiInfo + if form.CategoryName != "" { + category, err := p.getOrCreateEmojiCategory(ctx, form.CategoryName) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error putting id in category: %s", err), "error putting id in category") + } + + ai = &media.AdditionalEmojiInfo{ + CategoryID: &category.ID, + } + } + + processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, form.Shortcode, emojiID, emojiURI, ai, false) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error processing emoji: %s", err), "error processing emoji") + } + + emoji, err := processingEmoji.LoadEmoji(ctx) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error loading emoji: %s", err), "error loading emoji") + } + + apiEmoji, err := p.tc.EmojiToAPIEmoji(ctx, emoji) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting emoji: %s", err), "error converting emoji to api representation") + } + + return &apiEmoji, nil +} + +// EmojisGet returns an admin view of custom emojis, filtered with the given parameters. +func (p *Processor) EmojisGet( + ctx context.Context, + account *gtsmodel.Account, + user *gtsmodel.User, + domain string, + includeDisabled bool, + includeEnabled bool, + shortcode string, + maxShortcodeDomain string, + minShortcodeDomain string, + limit int, +) (*apimodel.PageableResponse, gtserror.WithCode) { + if !*user.Admin { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") + } + + emojis, err := p.db.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err := fmt.Errorf("EmojisGet: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(emojis) + if count == 0 { + return util.EmptyPageableResponse(), nil + } + + items := make([]interface{}, 0, count) + for _, emoji := range emojis { + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) + if err != nil { + err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + items = append(items, adminEmoji) + } + + filterBuilder := strings.Builder{} + filterBuilder.WriteString("filter=") + + switch domain { + case "", "local": + filterBuilder.WriteString("domain:local") + case db.EmojiAllDomains: + filterBuilder.WriteString("domain:all") + default: + filterBuilder.WriteString("domain:") + filterBuilder.WriteString(domain) + } + + if includeDisabled != includeEnabled { + if includeDisabled { + filterBuilder.WriteString(",disabled") + } + if includeEnabled { + filterBuilder.WriteString(",enabled") + } + } + + if shortcode != "" { + filterBuilder.WriteString(",shortcode:") + filterBuilder.WriteString(shortcode) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "api/v1/admin/custom_emojis", + NextMaxIDKey: "max_shortcode_domain", + NextMaxIDValue: util.ShortcodeDomain(emojis[count-1]), + PrevMinIDKey: "min_shortcode_domain", + PrevMinIDValue: util.ShortcodeDomain(emojis[0]), + Limit: limit, + ExtraQueryParams: []string{filterBuilder.String()}, + }) +} + +// EmojiGet returns the admin view of one custom emoji with the given id. +func (p *Processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { + if !*user.Admin { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") + } + + emoji, err := p.db.GetEmojiByID(ctx, id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id) + return nil, gtserror.NewErrorNotFound(err) + } + err := fmt.Errorf("EmojiGet: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) + if err != nil { + err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} + +// EmojiDelete deletes one emoji from the database, with the given id. +func (p *Processor) EmojiDelete(ctx context.Context, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { + emoji, err := p.db.GetEmojiByID(ctx, id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("EmojiDelete: no emoji with id %s found in the db", id) + return nil, gtserror.NewErrorNotFound(err) + } + err := fmt.Errorf("EmojiDelete: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + if emoji.Domain != "" { + err = fmt.Errorf("EmojiDelete: emoji with id %s was not a local emoji, will not delete", id) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) + if err != nil { + err = fmt.Errorf("EmojiDelete: error converting emoji to admin api emoji: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + if err := p.db.DeleteEmojiByID(ctx, id); err != nil { + err := fmt.Errorf("EmojiDelete: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} + +// EmojiUpdate updates one emoji with the given id, using the provided form parameters. +func (p *Processor) EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) { + emoji, err := p.db.GetEmojiByID(ctx, id) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("EmojiUpdate: no emoji with id %s found in the db", id) + return nil, gtserror.NewErrorNotFound(err) + } + err := fmt.Errorf("EmojiUpdate: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + switch form.Type { + case apimodel.EmojiUpdateCopy: + return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName) + case apimodel.EmojiUpdateDisable: + return p.emojiUpdateDisable(ctx, emoji) + case apimodel.EmojiUpdateModify: + return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName) + default: + err := errors.New("unrecognized emoji action type") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } +} + +// EmojiCategoriesGet returns all custom emoji categories that exist on this instance. +func (p *Processor) EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) { + categories, err := p.db.GetEmojiCategories(ctx) + if err != nil { + err := fmt.Errorf("EmojiCategoriesGet: db error: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories)) + for _, category := range categories { + apiCategory, err := p.tc.EmojiCategoryToAPIEmojiCategory(ctx, category) + if err != nil { + err := fmt.Errorf("EmojiCategoriesGet: error converting emoji category to api emoji category: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + apiCategories = append(apiCategories, apiCategory) + } + + return apiCategories, nil +} + +/* + UTIL FUNCTIONS +*/ + +func (p *Processor) getOrCreateEmojiCategory(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) { + category, err := p.db.GetEmojiCategoryByName(ctx, name) + if err == nil { + return category, nil + } + + if err != nil && !errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("GetOrCreateEmojiCategory: database error trying get emoji category by name: %s", err) + return nil, err + } + + // we don't have the category yet, just create it with the given name + categoryID, err := id.NewRandomULID() + if err != nil { + err = fmt.Errorf("GetOrCreateEmojiCategory: error generating id for new emoji category: %s", err) + return nil, err + } + + category = >smodel.EmojiCategory{ + ID: categoryID, + Name: name, + } + + if err := p.db.PutEmojiCategory(ctx, category); err != nil { + err = fmt.Errorf("GetOrCreateEmojiCategory: error putting new emoji category in the database: %s", err) + return nil, err + } + + return category, nil +} + +// copy an emoji from remote to local +func (p *Processor) emojiUpdateCopy(ctx context.Context, emoji *gtsmodel.Emoji, shortcode *string, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) { + if emoji.Domain == "" { + err := fmt.Errorf("emojiUpdateCopy: emoji %s is not a remote emoji, cannot copy it to local", emoji.ID) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + if shortcode == nil { + err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, no shortcode provided", emoji.ID) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, *shortcode, "") + if maybeExisting != nil { + err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, emoji with shortcode %s already exists on this instance", emoji.ID, *shortcode) + return nil, gtserror.NewErrorConflict(err, err.Error()) + } + + if err != nil && err != db.ErrNoEntries { + err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error checking existence of emoji with shortcode %s: %s", emoji.ID, *shortcode, err) + return nil, gtserror.NewErrorInternalError(err) + } + + newEmojiID, err := id.NewRandomULID() + if err != nil { + err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error creating id for new emoji: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + newEmojiURI := uris.GenerateURIForEmoji(newEmojiID) + + data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) { + rc, err := p.storage.GetStream(ctx, emoji.ImagePath) + return rc, int64(emoji.ImageFileSize), err + } + + var ai *media.AdditionalEmojiInfo + if categoryName != nil { + category, err := p.getOrCreateEmojiCategory(ctx, *categoryName) + if err != nil { + err = fmt.Errorf("emojiUpdateCopy: error getting or creating category: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + ai = &media.AdditionalEmojiInfo{ + CategoryID: &category.ID, + } + } + + processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, *shortcode, newEmojiID, newEmojiURI, ai, false) + if err != nil { + err = fmt.Errorf("emojiUpdateCopy: error processing emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + newEmoji, err := processingEmoji.LoadEmoji(ctx) + if err != nil { + err = fmt.Errorf("emojiUpdateCopy: error loading processed emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, newEmoji) + if err != nil { + err = fmt.Errorf("emojiUpdateCopy: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} + +// disable a remote emoji +func (p *Processor) emojiUpdateDisable(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, gtserror.WithCode) { + if emoji.Domain == "" { + err := fmt.Errorf("emojiUpdateDisable: emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + emojiDisabled := true + emoji.Disabled = &emojiDisabled + updatedEmoji, err := p.db.UpdateEmoji(ctx, emoji, "updated_at", "disabled") + if err != nil { + err = fmt.Errorf("emojiUpdateDisable: error updating emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji) + if err != nil { + err = fmt.Errorf("emojiUpdateDisable: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} + +// modify a local emoji +func (p *Processor) emojiUpdateModify(ctx context.Context, emoji *gtsmodel.Emoji, image *multipart.FileHeader, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) { + if emoji.Domain != "" { + err := fmt.Errorf("emojiUpdateModify: emoji %s is not a local emoji, cannot do a modify action on it", emoji.ID) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) + } + + var updatedEmoji *gtsmodel.Emoji + + // keep existing categoryID unless a new one is defined + var ( + updatedCategoryID = emoji.CategoryID + updateCategoryID bool + ) + if categoryName != nil { + category, err := p.getOrCreateEmojiCategory(ctx, *categoryName) + if err != nil { + err = fmt.Errorf("emojiUpdateModify: error getting or creating category: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + + updatedCategoryID = category.ID + updateCategoryID = true + } + + // only update image if provided with one + var updateImage bool + if image != nil && image.Size != 0 { + updateImage = true + } + + if !updateImage { + // only updating fields, we only need + // to do a database update for this + columns := []string{"updated_at"} + + if updateCategoryID { + emoji.CategoryID = updatedCategoryID + columns = append(columns, "category_id") + } + + var err error + updatedEmoji, err = p.db.UpdateEmoji(ctx, emoji, columns...) + if err != nil { + err = fmt.Errorf("emojiUpdateModify: error updating emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + } else { + // new image, so we need to reprocess the emoji + data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) { + i, err := image.Open() + return i, image.Size, err + } + + var ai *media.AdditionalEmojiInfo + if updateCategoryID { + ai = &media.AdditionalEmojiInfo{ + CategoryID: &updatedCategoryID, + } + } + + processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, emoji.Shortcode, emoji.ID, emoji.URI, ai, true) + if err != nil { + err = fmt.Errorf("emojiUpdateModify: error processing emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + updatedEmoji, err = processingEmoji.LoadEmoji(ctx) + if err != nil { + err = fmt.Errorf("emojiUpdateModify: error loading processed emoji %s: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + } + + adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji) + if err != nil { + err = fmt.Errorf("emojiUpdateModify: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) + return nil, gtserror.NewErrorInternalError(err) + } + + return adminEmoji, nil +} diff --git a/internal/processing/admin/emojicategory.go b/internal/processing/admin/emojicategory.go @@ -1,60 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" -) - -func (p *processor) GetOrCreateEmojiCategory(ctx context.Context, name string) (*gtsmodel.EmojiCategory, error) { - category, err := p.db.GetEmojiCategoryByName(ctx, name) - if err == nil { - return category, nil - } - - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err = fmt.Errorf("GetOrCreateEmojiCategory: database error trying get emoji category by name: %s", err) - return nil, err - } - - // we don't have the category yet, just create it with the given name - categoryID, err := id.NewRandomULID() - if err != nil { - err = fmt.Errorf("GetOrCreateEmojiCategory: error generating id for new emoji category: %s", err) - return nil, err - } - - category = >smodel.EmojiCategory{ - ID: categoryID, - Name: name, - } - - if err := p.db.PutEmojiCategory(ctx, category); err != nil { - err = fmt.Errorf("GetOrCreateEmojiCategory: error putting new emoji category in the database: %s", err) - return nil, err - } - - return category, nil -} diff --git a/internal/processing/admin/getcategories.go b/internal/processing/admin/getcategories.go @@ -1,47 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -func (p *processor) EmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) { - categories, err := p.db.GetEmojiCategories(ctx) - if err != nil { - err := fmt.Errorf("EmojiCategoriesGet: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - apiCategories := make([]*apimodel.EmojiCategory, 0, len(categories)) - for _, category := range categories { - apiCategory, err := p.tc.EmojiCategoryToAPIEmojiCategory(ctx, category) - if err != nil { - err := fmt.Errorf("EmojiCategoriesGet: error converting emoji category to api emoji category: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - apiCategories = append(apiCategories, apiCategory) - } - - return apiCategories, nil -} diff --git a/internal/processing/admin/getdomainblock.go b/internal/processing/admin/getdomainblock.go @@ -1,49 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) DomainBlockGet(ctx context.Context, account *gtsmodel.Account, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) { - domainBlock := >smodel.DomainBlock{} - - if err := p.db.GetByID(ctx, id, domainBlock); err != nil { - if err != db.ErrNoEntries { - // something has gone really wrong - return nil, gtserror.NewErrorInternalError(err) - } - // there are no entries for this ID - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no entry for ID %s", id)) - } - - apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, domainBlock, export) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return apiDomainBlock, nil -} diff --git a/internal/processing/admin/getdomainblocks.go b/internal/processing/admin/getdomainblocks.go @@ -1,50 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) DomainBlocksGet(ctx context.Context, account *gtsmodel.Account, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) { - domainBlocks := []*gtsmodel.DomainBlock{} - - if err := p.db.GetAll(ctx, &domainBlocks); err != nil { - if err != db.ErrNoEntries { - // something has gone really wrong - return nil, gtserror.NewErrorInternalError(err) - } - } - - apiDomainBlocks := []*apimodel.DomainBlock{} - for _, b := range domainBlocks { - apiDomainBlock, err := p.tc.DomainBlockToAPIDomainBlock(ctx, b, export) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - apiDomainBlocks = append(apiDomainBlocks, apiDomainBlock) - } - - return apiDomainBlocks, nil -} diff --git a/internal/processing/admin/getemoji.go b/internal/processing/admin/getemoji.go @@ -1,54 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) EmojiGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, id string) (*apimodel.AdminEmoji, gtserror.WithCode) { - if !*user.Admin { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") - } - - emoji, err := p.db.GetEmojiByID(ctx, id) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - err = fmt.Errorf("EmojiGet: no emoji with id %s found in the db", id) - return nil, gtserror.NewErrorNotFound(err) - } - err := fmt.Errorf("EmojiGet: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) - if err != nil { - err = fmt.Errorf("EmojiGet: error converting emoji to admin api emoji: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - return adminEmoji, nil -} diff --git a/internal/processing/admin/getemojis.go b/internal/processing/admin/getemojis.go @@ -1,97 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - "strings" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -func (p *processor) EmojisGet(ctx context.Context, account *gtsmodel.Account, user *gtsmodel.User, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - if !*user.Admin { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("user %s not an admin", user.ID), "user is not an admin") - } - - emojis, err := p.db.GetEmojis(ctx, domain, includeDisabled, includeEnabled, shortcode, maxShortcodeDomain, minShortcodeDomain, limit) - if err != nil && !errors.Is(err, db.ErrNoEntries) { - err := fmt.Errorf("EmojisGet: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - count := len(emojis) - if count == 0 { - return util.EmptyPageableResponse(), nil - } - - items := make([]interface{}, 0, count) - for _, emoji := range emojis { - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, emoji) - if err != nil { - err := fmt.Errorf("EmojisGet: error converting emoji to admin model emoji: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - items = append(items, adminEmoji) - } - - filterBuilder := strings.Builder{} - filterBuilder.WriteString("filter=") - - switch domain { - case "", "local": - filterBuilder.WriteString("domain:local") - case db.EmojiAllDomains: - filterBuilder.WriteString("domain:all") - default: - filterBuilder.WriteString("domain:") - filterBuilder.WriteString(domain) - } - - if includeDisabled != includeEnabled { - if includeDisabled { - filterBuilder.WriteString(",disabled") - } - if includeEnabled { - filterBuilder.WriteString(",enabled") - } - } - - if shortcode != "" { - filterBuilder.WriteString(",shortcode:") - filterBuilder.WriteString(shortcode) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "api/v1/admin/custom_emojis", - NextMaxIDKey: "max_shortcode_domain", - NextMaxIDValue: util.ShortcodeDomain(emojis[count-1]), - PrevMinIDKey: "min_shortcode_domain", - PrevMinIDValue: util.ShortcodeDomain(emojis[0]), - Limit: limit, - ExtraQueryParams: []string{filterBuilder.String()}, - }) -} diff --git a/internal/processing/admin/getreport.go b/internal/processing/admin/getreport.go @@ -1,45 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) { - report, err := p.db.GetReportByID(ctx, id) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, report, account) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return apimodelReport, nil -} diff --git a/internal/processing/admin/getreports.go b/internal/processing/admin/getreports.go @@ -1,92 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - "strconv" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -func (p *processor) ReportsGet( - ctx context.Context, - account *gtsmodel.Account, - resolved *bool, - accountID string, - targetAccountID string, - maxID string, - sinceID string, - minID string, - limit int, -) (*apimodel.PageableResponse, gtserror.WithCode) { - reports, err := p.db.GetReports(ctx, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit) - if err != nil { - if err == db.ErrNoEntries { - return util.EmptyPageableResponse(), nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - count := len(reports) - items := make([]interface{}, 0, count) - nextMaxIDValue := "" - prevMinIDValue := "" - for i, r := range reports { - item, err := p.tc.ReportToAdminAPIReport(ctx, r, account) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) - } - - if i == count-1 { - nextMaxIDValue = item.ID - } - - if i == 0 { - prevMinIDValue = item.ID - } - - items = append(items, item) - } - - extraQueryParams := []string{} - if resolved != nil { - extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) - } - if accountID != "" { - extraQueryParams = append(extraQueryParams, "account_id="+accountID) - } - if targetAccountID != "" { - extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/api/v1/admin/reports", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: extraQueryParams, - }) -} diff --git a/internal/processing/admin/importdomainblocks.go b/internal/processing/admin/importdomainblocks.go @@ -1,66 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "mime/multipart" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -// DomainBlocksImport handles the import of a bunch of domain blocks at once, by calling the DomainBlockCreate function for each domain in the provided file. -func (p *processor) DomainBlocksImport(ctx context.Context, account *gtsmodel.Account, domains *multipart.FileHeader) ([]*apimodel.DomainBlock, gtserror.WithCode) { - f, err := domains.Open() - if err != nil { - return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error opening attachment: %s", err)) - } - buf := new(bytes.Buffer) - size, err := io.Copy(buf, f) - if err != nil { - return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: error reading attachment: %s", err)) - } - if size == 0 { - return nil, gtserror.NewErrorBadRequest(errors.New("DomainBlocksImport: could not read provided attachment: size 0 bytes")) - } - - d := []apimodel.DomainBlock{} - if err := json.Unmarshal(buf.Bytes(), &d); err != nil { - return nil, gtserror.NewErrorBadRequest(fmt.Errorf("DomainBlocksImport: could not read provided attachment: %s", err)) - } - - blocks := []*apimodel.DomainBlock{} - for _, d := range d { - block, err := p.DomainBlockCreate(ctx, account, d.Domain.Domain, false, d.PublicComment, "", "") - if err != nil { - return nil, err - } - - blocks = append(blocks, block) - } - - return blocks, nil -} diff --git a/internal/processing/admin/media.go b/internal/processing/admin/media.go @@ -0,0 +1,64 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 admin + +import ( + "context" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/log" +) + +// MediaRefetch forces a refetch of remote emojis. +func (p *Processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode { + transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username) + if err != nil { + err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err) + return gtserror.NewErrorInternalError(err) + } + + go func() { + log.Info(ctx, "starting emoji refetch") + refetched, err := p.mediaManager.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia) + if err != nil { + log.Errorf(ctx, "error refetching emojis: %s", err) + } else { + log.Infof(ctx, "refetched %d emojis from remote", refetched) + } + }() + + return nil +} + +// MediaPrune triggers a non-blocking prune of remote media, local unused media, etc. +func (p *Processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode { + if mediaRemoteCacheDays < 0 { + err := fmt.Errorf("MediaPrune: invalid value for mediaRemoteCacheDays prune: value was %d, cannot be less than 0", mediaRemoteCacheDays) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + if err := p.mediaManager.PruneAll(ctx, mediaRemoteCacheDays, false); err != nil { + err = fmt.Errorf("MediaPrune: %w", err) + return gtserror.NewErrorInternalError(err) + } + + return nil +} diff --git a/internal/processing/admin/mediaprune.go b/internal/processing/admin/mediaprune.go @@ -1,40 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -func (p *processor) MediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode { - if mediaRemoteCacheDays < 0 { - err := fmt.Errorf("MediaPrune: invalid value for mediaRemoteCacheDays prune: value was %d, cannot be less than 0", mediaRemoteCacheDays) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - if err := p.mediaManager.PruneAll(ctx, mediaRemoteCacheDays, false); err != nil { - err = fmt.Errorf("MediaPrune: %w", err) - return gtserror.NewErrorInternalError(err) - } - - return nil -} diff --git a/internal/processing/admin/mediarefetch.go b/internal/processing/admin/mediarefetch.go @@ -1,48 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/log" -) - -func (p *processor) MediaRefetch(ctx context.Context, requestingAccount *gtsmodel.Account, domain string) gtserror.WithCode { - transport, err := p.transportController.NewTransportForUsername(ctx, requestingAccount.Username) - if err != nil { - err = fmt.Errorf("error getting transport for user %s during media refetch request: %w", requestingAccount.Username, err) - return gtserror.NewErrorInternalError(err) - } - - go func() { - log.Info(ctx, "starting emoji refetch") - refetched, err := p.mediaManager.RefetchEmojis(context.Background(), domain, transport.DereferenceMedia) - if err != nil { - log.Errorf(ctx, "error refetching emojis: %s", err) - } else { - log.Infof(ctx, "refetched %d emojis from remote", refetched) - } - }() - - return nil -} diff --git a/internal/processing/admin/report.go b/internal/processing/admin/report.go @@ -0,0 +1,148 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 admin + +import ( + "context" + "fmt" + "strconv" + "time" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// ReportsGet returns all reports stored on this instance, with the given parameters. +func (p *Processor) ReportsGet( + ctx context.Context, + account *gtsmodel.Account, + resolved *bool, + accountID string, + targetAccountID string, + maxID string, + sinceID string, + minID string, + limit int, +) (*apimodel.PageableResponse, gtserror.WithCode) { + reports, err := p.db.GetReports(ctx, resolved, accountID, targetAccountID, maxID, sinceID, minID, limit) + if err != nil { + if err == db.ErrNoEntries { + return util.EmptyPageableResponse(), nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(reports) + items := make([]interface{}, 0, count) + nextMaxIDValue := "" + prevMinIDValue := "" + for i, r := range reports { + item, err := p.tc.ReportToAdminAPIReport(ctx, r, account) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) + } + + if i == count-1 { + nextMaxIDValue = item.ID + } + + if i == 0 { + prevMinIDValue = item.ID + } + + items = append(items, item) + } + + extraQueryParams := []string{} + if resolved != nil { + extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) + } + if accountID != "" { + extraQueryParams = append(extraQueryParams, "account_id="+accountID) + } + if targetAccountID != "" { + extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "/api/v1/admin/reports", + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + Limit: limit, + ExtraQueryParams: extraQueryParams, + }) +} + +// ReportGet returns one report, with the given ID. +func (p *Processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.AdminReport, gtserror.WithCode) { + report, err := p.db.GetReportByID(ctx, id) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, report, account) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return apimodelReport, nil +} + +// ReportResolve marks a report with the given id as resolved, and stores the provided actionTakenComment (if not null). +func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) { + report, err := p.db.GetReportByID(ctx, id) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + columns := []string{ + "action_taken_at", + "action_taken_by_account_id", + } + + report.ActionTakenAt = time.Now() + report.ActionTakenByAccountID = account.ID + + if actionTakenComment != nil { + report.ActionTaken = *actionTakenComment + columns = append(columns, "action_taken") + } + + updatedReport, err := p.db.UpdateReport(ctx, report, columns...) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return apimodelReport, nil +} diff --git a/internal/processing/admin/resolvereport.go b/internal/processing/admin/resolvereport.go @@ -1,64 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "time" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) { - report, err := p.db.GetReportByID(ctx, id) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - columns := []string{ - "action_taken_at", - "action_taken_by_account_id", - } - - report.ActionTakenAt = time.Now() - report.ActionTakenByAccountID = account.ID - - if actionTakenComment != nil { - report.ActionTaken = *actionTakenComment - columns = append(columns, "action_taken") - } - - updatedReport, err := p.db.UpdateReport(ctx, report, columns...) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return apimodelReport, nil -} diff --git a/internal/processing/admin/updateemoji.go b/internal/processing/admin/updateemoji.go @@ -1,236 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 admin - -import ( - "context" - "errors" - "fmt" - "io" - "mime/multipart" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -func (p *processor) EmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) { - emoji, err := p.db.GetEmojiByID(ctx, id) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - err = fmt.Errorf("EmojiUpdate: no emoji with id %s found in the db", id) - return nil, gtserror.NewErrorNotFound(err) - } - err := fmt.Errorf("EmojiUpdate: db error: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - switch form.Type { - case apimodel.EmojiUpdateCopy: - return p.emojiUpdateCopy(ctx, emoji, form.Shortcode, form.CategoryName) - case apimodel.EmojiUpdateDisable: - return p.emojiUpdateDisable(ctx, emoji) - case apimodel.EmojiUpdateModify: - return p.emojiUpdateModify(ctx, emoji, form.Image, form.CategoryName) - default: - err := errors.New("unrecognized emoji action type") - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } -} - -// copy an emoji from remote to local -func (p *processor) emojiUpdateCopy(ctx context.Context, emoji *gtsmodel.Emoji, shortcode *string, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) { - if emoji.Domain == "" { - err := fmt.Errorf("emojiUpdateCopy: emoji %s is not a remote emoji, cannot copy it to local", emoji.ID) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - if shortcode == nil { - err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, no shortcode provided", emoji.ID) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - maybeExisting, err := p.db.GetEmojiByShortcodeDomain(ctx, *shortcode, "") - if maybeExisting != nil { - err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, emoji with shortcode %s already exists on this instance", emoji.ID, *shortcode) - return nil, gtserror.NewErrorConflict(err, err.Error()) - } - - if err != nil && err != db.ErrNoEntries { - err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error checking existence of emoji with shortcode %s: %s", emoji.ID, *shortcode, err) - return nil, gtserror.NewErrorInternalError(err) - } - - newEmojiID, err := id.NewRandomULID() - if err != nil { - err := fmt.Errorf("emojiUpdateCopy: emoji %s could not be copied, error creating id for new emoji: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - newEmojiURI := uris.GenerateURIForEmoji(newEmojiID) - - data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) { - rc, err := p.storage.GetStream(ctx, emoji.ImagePath) - return rc, int64(emoji.ImageFileSize), err - } - - var ai *media.AdditionalEmojiInfo - if categoryName != nil { - category, err := p.GetOrCreateEmojiCategory(ctx, *categoryName) - if err != nil { - err = fmt.Errorf("emojiUpdateCopy: error getting or creating category: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - ai = &media.AdditionalEmojiInfo{ - CategoryID: &category.ID, - } - } - - processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, *shortcode, newEmojiID, newEmojiURI, ai, false) - if err != nil { - err = fmt.Errorf("emojiUpdateCopy: error processing emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - newEmoji, err := processingEmoji.LoadEmoji(ctx) - if err != nil { - err = fmt.Errorf("emojiUpdateCopy: error loading processed emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, newEmoji) - if err != nil { - err = fmt.Errorf("emojiUpdateCopy: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - return adminEmoji, nil -} - -// disable a remote emoji -func (p *processor) emojiUpdateDisable(ctx context.Context, emoji *gtsmodel.Emoji) (*apimodel.AdminEmoji, gtserror.WithCode) { - if emoji.Domain == "" { - err := fmt.Errorf("emojiUpdateDisable: emoji %s is not a remote emoji, cannot disable it via this endpoint", emoji.ID) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - emojiDisabled := true - emoji.Disabled = &emojiDisabled - updatedEmoji, err := p.db.UpdateEmoji(ctx, emoji, "updated_at", "disabled") - if err != nil { - err = fmt.Errorf("emojiUpdateDisable: error updating emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji) - if err != nil { - err = fmt.Errorf("emojiUpdateDisable: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - return adminEmoji, nil -} - -// modify a local emoji -func (p *processor) emojiUpdateModify(ctx context.Context, emoji *gtsmodel.Emoji, image *multipart.FileHeader, categoryName *string) (*apimodel.AdminEmoji, gtserror.WithCode) { - if emoji.Domain != "" { - err := fmt.Errorf("emojiUpdateModify: emoji %s is not a local emoji, cannot do a modify action on it", emoji.ID) - return nil, gtserror.NewErrorBadRequest(err, err.Error()) - } - - var updatedEmoji *gtsmodel.Emoji - - // keep existing categoryID unless a new one is defined - var ( - updatedCategoryID = emoji.CategoryID - updateCategoryID bool - ) - if categoryName != nil { - category, err := p.GetOrCreateEmojiCategory(ctx, *categoryName) - if err != nil { - err = fmt.Errorf("emojiUpdateModify: error getting or creating category: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - - updatedCategoryID = category.ID - updateCategoryID = true - } - - // only update image if provided with one - var updateImage bool - if image != nil && image.Size != 0 { - updateImage = true - } - - if !updateImage { - // only updating fields, we only need - // to do a database update for this - columns := []string{"updated_at"} - - if updateCategoryID { - emoji.CategoryID = updatedCategoryID - columns = append(columns, "category_id") - } - - var err error - updatedEmoji, err = p.db.UpdateEmoji(ctx, emoji, columns...) - if err != nil { - err = fmt.Errorf("emojiUpdateModify: error updating emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - } else { - // new image, so we need to reprocess the emoji - data := func(ctx context.Context) (reader io.ReadCloser, fileSize int64, err error) { - i, err := image.Open() - return i, image.Size, err - } - - var ai *media.AdditionalEmojiInfo - if updateCategoryID { - ai = &media.AdditionalEmojiInfo{ - CategoryID: &updatedCategoryID, - } - } - - processingEmoji, err := p.mediaManager.PreProcessEmoji(ctx, data, nil, emoji.Shortcode, emoji.ID, emoji.URI, ai, true) - if err != nil { - err = fmt.Errorf("emojiUpdateModify: error processing emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - updatedEmoji, err = processingEmoji.LoadEmoji(ctx) - if err != nil { - err = fmt.Errorf("emojiUpdateModify: error loading processed emoji %s: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - } - - adminEmoji, err := p.tc.EmojiToAdminAPIEmoji(ctx, updatedEmoji) - if err != nil { - err = fmt.Errorf("emojiUpdateModify: error converting updated emoji %s to admin emoji: %s", emoji.ID, err) - return nil, gtserror.NewErrorInternalError(err) - } - - return adminEmoji, nil -} diff --git a/internal/processing/app.go b/internal/processing/app.go @@ -29,7 +29,7 @@ import ( "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 *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) { // set default 'read' for scopes if it's not set var scopes string if form.Scopes == "" { diff --git a/internal/processing/blocks.go b/internal/processing/blocks.go @@ -30,7 +30,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { +func (p *Processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { accounts, nextMaxID, prevMinID, err := p.db.GetAccountBlocks(ctx, authed.Account.ID, maxID, sinceID, limit) if err != nil { if err == db.ErrNoEntries { @@ -55,7 +55,7 @@ func (p *processor) BlocksGet(ctx context.Context, authed *oauth.Auth, maxID str return p.packageBlocksResponse(apiAccounts, "/api/v1/blocks", nextMaxID, prevMinID, limit) } -func (p *processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { +func (p *Processor) packageBlocksResponse(accounts []*apimodel.Account, path string, nextMaxID string, prevMinID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) { resp := &apimodel.BlocksResponse{ Accounts: []*apimodel.Account{}, } diff --git a/internal/processing/bookmark.go b/internal/processing/bookmark.go @@ -1,31 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.accountProcessor.BookmarksGet(ctx, authed.Account, limit, maxID, minID) -} diff --git a/internal/processing/federation.go b/internal/processing/federation.go @@ -1,72 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - "net/http" - "net/url" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -func (p *processor) GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetUser(ctx, requestedUsername, requestURL) -} - -func (p *processor) GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetFollowers(ctx, requestedUsername, requestURL) -} - -func (p *processor) GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetFollowing(ctx, requestedUsername, requestURL) -} - -func (p *processor) GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetStatus(ctx, requestedUsername, requestedStatusID, requestURL) -} - -func (p *processor) GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetStatusReplies(ctx, requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, requestURL) -} - -func (p *processor) GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetOutbox(ctx, requestedUsername, page, maxID, minID, requestURL) -} - -func (p *processor) GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - return p.federationProcessor.GetEmoji(ctx, requestedEmojiID, requestURL) -} - -func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { - return p.federationProcessor.GetWebfingerAccount(ctx, requestedUsername) -} - -func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) { - return p.federationProcessor.GetNodeInfoRel(ctx) -} - -func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) { - return p.federationProcessor.GetNodeInfo(ctx) -} - -func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { - return p.federationProcessor.PostInbox(ctx, w, r) -} diff --git a/internal/processing/federation/federation.go b/internal/processing/federation/federation.go @@ -1,100 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "net/http" - "net/url" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" - "github.com/superseriousbusiness/gotosocial/internal/visibility" -) - -// Processor wraps functions for processing federation API requests. -type Processor interface { - // GetUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication - // before returning a JSON serializable interface to the caller. - GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. - GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) - - // GetFediEmoji handles the GET for a federated emoji originating from this instance. - GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // GetNodeInfoRel returns a well known response giving the path to node info. - GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) - - // GetNodeInfo returns a node info struct in response to a node info request. - GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) - - // GetOutbox returns the activitypub representation of a local user's outbox. - // This contains links to PUBLIC posts made by this user. - GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - - // PostInbox handles POST requests to a user's inbox for new activitypub messages. - // - // PostInbox returns true if the request was handled as an ActivityPub POST to an actor's inbox. - // If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page. - // - // If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written. - // - // If the Actor was constructed with the Federated Protocol enabled, side effects will occur. - // - // If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur. - PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) -} - -type processor struct { - db db.DB - federator federation.Federator - tc typeutils.TypeConverter - filter visibility.Filter -} - -// New returns a new federation processor. -func New(db db.DB, tc typeutils.TypeConverter, federator federation.Federator) Processor { - return &processor{ - db: db, - federator: federator, - tc: tc, - filter: visibility.NewFilter(db), - } -} diff --git a/internal/processing/federation/getemoji.go b/internal/processing/federation/getemoji.go @@ -1,59 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -func (p *processor) GetEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil { - return nil, errWithCode - } - - requestedEmoji, err := p.db.GetEmojiByID(ctx, requestedEmojiID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err)) - } - - if requestedEmoji.Domain != "" { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain)) - } - - if *requestedEmoji.Disabled { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID)) - } - - apEmoji, err := p.tc.EmojiToAS(ctx, requestedEmoji) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err)) - } - - data, err := streams.Serialize(apEmoji) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getfollowers.go b/internal/processing/federation/getfollowers.go @@ -1,76 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -func (p *processor) GetFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - requestedAccountURI, err := url.Parse(requestedAccount.URI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) - } - - requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) - } - - data, err := streams.Serialize(requestedFollowers) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getfollowing.go b/internal/processing/federation/getfollowing.go @@ -1,76 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -func (p *processor) GetFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - requestedAccountURI, err := url.Parse(requestedAccount.URI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) - } - - requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) - } - - data, err := streams.Serialize(requestedFollowing) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getnodeinfo.go b/internal/processing/federation/getnodeinfo.go @@ -1,89 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -const ( - nodeInfoVersion = "2.0" - nodeInfoSoftwareName = "gotosocial" -) - -var ( - nodeInfoRel = fmt.Sprintf("http://nodeinfo.diaspora.software/ns/schema/%s", nodeInfoVersion) - nodeInfoProtocols = []string{"activitypub"} -) - -func (p *processor) GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) { - protocol := config.GetProtocol() - host := config.GetHost() - - return &apimodel.WellKnownResponse{ - Links: []apimodel.Link{ - { - Rel: nodeInfoRel, - Href: fmt.Sprintf("%s://%s/nodeinfo/%s", protocol, host, nodeInfoVersion), - }, - }, - }, nil -} - -func (p *processor) GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) { - openRegistration := config.GetAccountsRegistrationOpen() - softwareVersion := config.GetSoftwareVersion() - - host := config.GetHost() - userCount, err := p.db.CountInstanceUsers(ctx, host) - if err != nil { - return nil, gtserror.NewErrorInternalError(err, "Unable to query instance user count") - } - - postCount, err := p.db.CountInstanceStatuses(ctx, host) - if err != nil { - return nil, gtserror.NewErrorInternalError(err, "Unable to query instance status count") - } - - return &apimodel.Nodeinfo{ - Version: nodeInfoVersion, - Software: apimodel.NodeInfoSoftware{ - Name: nodeInfoSoftwareName, - Version: softwareVersion, - }, - Protocols: nodeInfoProtocols, - Services: apimodel.NodeInfoServices{ - Inbound: []string{}, - Outbound: []string{}, - }, - OpenRegistrations: openRegistration, - Usage: apimodel.NodeInfoUsage{ - Users: apimodel.NodeInfoUsers{ - Total: userCount, - }, - LocalPosts: postCount, - }, - Metadata: make(map[string]interface{}), - }, nil -} diff --git a/internal/processing/federation/getoutbox.go b/internal/processing/federation/getoutbox.go @@ -1,109 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -func (p *processor) GetOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - // authorize the request: - // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - var data map[string]interface{} - // now there are two scenarios: - // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. - // 2. we're asked for a specific page; this can be either the first page or any other page - - if !page { - /* - scenario 1: return the collection with no items - we want something that looks like this: - { - "@context": "https://www.w3.org/ns/activitystreams", - "id": "https://example.org/users/whatever/outbox", - "type": "OrderedCollection", - "first": "https://example.org/users/whatever/outbox?page=true", - "last": "https://example.org/users/whatever/outbox?min_id=0&page=true" - } - */ - collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - data, err = streams.Serialize(collection) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil - } - - // scenario 2 -- get the requested page - // limit pages to 30 entries per page - publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true) - if err != nil && err != db.ErrNoEntries { - return nil, gtserror.NewErrorInternalError(err) - } - - outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - data, err = streams.Serialize(outboxPage) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getstatus.go b/internal/processing/federation/getstatus.go @@ -1,92 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -func (p *processor) GetStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - // authorize the request: - // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - // get the status out of the database here - s, err := p.db.GetStatusByID(ctx, requestedStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) - } - - if s.AccountID != requestedAccount.ID { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s does not belong to account with id %s", s.ID, requestedAccount.ID)) - } - - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if !visible { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) - } - - // requester is authorized to view the status, so convert it to AP representation and serialize it - asStatus, err := p.tc.StatusToAS(ctx, s) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - data, err := streams.Serialize(asStatus) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getstatusreplies.go b/internal/processing/federation/getstatusreplies.go @@ -1,164 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -func (p *processor) GetStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - // authenticate the request - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - // authorize the request: - // 1. check if a block exists between the requester and the requestee - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - - // get the status out of the database here - s := >smodel.Status{} - if err := p.db.GetWhere(ctx, []db.Where{ - {Key: "id", Value: requestedStatusID}, - {Key: "account_id", Value: requestedAccount.ID}, - }, s); err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) - } - - visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - if !visible { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) - } - - var data map[string]interface{} - - // now there are three scenarios: - // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. - // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items. - // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items! - switch { - case !page: - // scenario 1 - // get the collection - collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - data, err = streams.Serialize(collection) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - case page && requestURL.Query().Get("only_other_accounts") == "": - // scenario 2 - // get the collection - collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - // but only return the first page - data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage()) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - default: - // scenario 3 - // get immediate children - replies, err := p.db.GetStatusChildren(ctx, s, true, minID) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - // filter children and extract URIs - replyURIs := map[string]*url.URL{} - for _, r := range replies { - // only show public or unlocked statuses as replies - if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked { - continue - } - - // respect onlyOtherAccounts parameter - if onlyOtherAccounts && r.AccountID == requestedAccount.ID { - continue - } - - // only show replies that the status owner can see - visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount) - if err != nil || !visibleToStatusOwner { - continue - } - - // only show replies that the requester can see - visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount) - if err != nil || !visibleToRequester { - continue - } - - rURI, err := url.Parse(r.URI) - if err != nil { - continue - } - - replyURIs[r.ID] = rURI - } - - repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - data, err = streams.Serialize(repliesPage) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } - - return data, nil -} diff --git a/internal/processing/federation/getuser.go b/internal/processing/federation/getuser.go @@ -1,86 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - "net/url" - - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/transport" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -func (p *processor) GetUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { - // Get the instance-local account the request is referring to. - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - var requestedPerson vocab.ActivityStreamsPerson - - if uris.IsPublicKeyPath(requestURL) { - // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key - requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } else { - // if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile - requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) - if errWithCode != nil { - return nil, errWithCode - } - - // if we're not already handshaking/dereferencing a remote account, dereference it now - if !p.federator.Handshaking(requestedUsername, requestingAccountURI) { - requestingAccount, err := p.federator.GetAccountByURI( - transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, - ) - if err != nil { - return nil, gtserror.NewErrorUnauthorized(err) - } - - blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - if blocked { - return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) - } - } - - requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - } - - data, err := streams.Serialize(requestedPerson) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return data, nil -} diff --git a/internal/processing/federation/getwebfinger.go b/internal/processing/federation/getwebfinger.go @@ -1,70 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -const ( - webfingerProfilePage = "http://webfinger.net/rel/profile-page" - webFingerProfilePageContentType = "text/html" - webfingerSelf = "self" - webFingerSelfContentType = "application/activity+json" - webfingerAccount = "acct" -) - -func (p *processor) GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { - // get the account the request is referring to - requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) - } - - accountDomain := config.GetAccountDomain() - if accountDomain == "" { - accountDomain = config.GetHost() - } - - // return the webfinger representation - return &apimodel.WellKnownResponse{ - Subject: fmt.Sprintf("%s:%s@%s", webfingerAccount, requestedAccount.Username, accountDomain), - Aliases: []string{ - requestedAccount.URI, - requestedAccount.URL, - }, - Links: []apimodel.Link{ - { - Rel: webfingerProfilePage, - Type: webFingerProfilePageContentType, - Href: requestedAccount.URL, - }, - { - Rel: webfingerSelf, - Type: webFingerSelfContentType, - Href: requestedAccount.URI, - }, - }, - }, nil -} diff --git a/internal/processing/federation/postinbox.go b/internal/processing/federation/postinbox.go @@ -1,28 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 federation - -import ( - "context" - "net/http" -) - -func (p *processor) PostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { - return p.federator.FederatingActor().PostInbox(ctx, w, r) -} diff --git a/internal/processing/fedi/collections.go b/internal/processing/fedi/collections.go @@ -0,0 +1,224 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "context" + "fmt" + "net/http" + "net/url" + + "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/transport" +) + +// FollowersGet handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate +// authentication before returning a JSON serializable interface to the caller. +func (p *Processor) FollowersGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + requestedAccountURI, err := url.Parse(requestedAccount.URI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + } + + requestedFollowers, err := p.federator.FederatingDB().Followers(ctx, requestedAccountURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching followers for uri %s: %s", requestedAccountURI.String(), err)) + } + + data, err := streams.Serialize(requestedFollowers) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// FollowingGet handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate +// authentication before returning a JSON serializable interface to the caller. +func (p *Processor) FollowingGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + requestedAccountURI, err := url.Parse(requestedAccount.URI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error parsing url %s: %s", requestedAccount.URI, err)) + } + + requestedFollowing, err := p.federator.FederatingDB().Following(ctx, requestedAccountURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching following for uri %s: %s", requestedAccountURI.String(), err)) + } + + data, err := streams.Serialize(requestedFollowing) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// OutboxGet returns the activitypub representation of a local user's outbox. +// This contains links to PUBLIC posts made by this user. +func (p *Processor) OutboxGet(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + var data map[string]interface{} + // now there are two scenarios: + // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. + // 2. we're asked for a specific page; this can be either the first page or any other page + + if !page { + /* + scenario 1: return the collection with no items + we want something that looks like this: + { + "@context": "https://www.w3.org/ns/activitystreams", + "id": "https://example.org/users/whatever/outbox", + "type": "OrderedCollection", + "first": "https://example.org/users/whatever/outbox?page=true", + "last": "https://example.org/users/whatever/outbox?min_id=0&page=true" + } + */ + collection, err := p.tc.OutboxToASCollection(ctx, requestedAccount.OutboxURI) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err = streams.Serialize(collection) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil + } + + // scenario 2 -- get the requested page + // limit pages to 30 entries per page + publicStatuses, err := p.db.GetAccountStatuses(ctx, requestedAccount.ID, 30, true, true, maxID, minID, false, false, true) + if err != nil && err != db.ErrNoEntries { + return nil, gtserror.NewErrorInternalError(err) + } + + outboxPage, err := p.tc.StatusesToASOutboxPage(ctx, requestedAccount.OutboxURI, maxID, minID, publicStatuses) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + data, err = streams.Serialize(outboxPage) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// InboxPost handles POST requests to a user's inbox for new activitypub messages. +// +// InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox. +// If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page. +// +// If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written. +// +// If the Actor was constructed with the Federated Protocol enabled, side effects will occur. +// +// If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur. +func (p *Processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + return p.federator.FederatingActor().PostInbox(ctx, w, r) +} diff --git a/internal/processing/fedi/emoji.go b/internal/processing/fedi/emoji.go @@ -0,0 +1,60 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "context" + "fmt" + "net/url" + + "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +// EmojiGet handles the GET for a federated emoji originating from this instance. +func (p *Processor) EmojiGet(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + if _, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, ""); errWithCode != nil { + return nil, errWithCode + } + + requestedEmoji, err := p.db.GetEmojiByID(ctx, requestedEmojiID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting emoji with id %s: %s", requestedEmojiID, err)) + } + + if requestedEmoji.Domain != "" { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s doesn't belong to this instance (domain %s)", requestedEmojiID, requestedEmoji.Domain)) + } + + if *requestedEmoji.Disabled { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("emoji with id %s has been disabled", requestedEmojiID)) + } + + apEmoji, err := p.tc.EmojiToAS(ctx, requestedEmoji) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting gtsmodel emoji with id %s to ap emoji: %s", requestedEmojiID, err)) + } + + data, err := streams.Serialize(apEmoji) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/fedi/fedi.go b/internal/processing/fedi/fedi.go @@ -0,0 +1,43 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" + "github.com/superseriousbusiness/gotosocial/internal/visibility" +) + +type Processor struct { + db db.DB + federator federation.Federator + tc typeutils.TypeConverter + filter visibility.Filter +} + +// New returns a new fedi processor. +func New(db db.DB, tc typeutils.TypeConverter, federator federation.Federator) Processor { + return Processor{ + db: db, + federator: federator, + tc: tc, + filter: visibility.NewFilter(db), + } +} diff --git a/internal/processing/fedi/status.go b/internal/processing/fedi/status.go @@ -0,0 +1,231 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "context" + "fmt" + "net/url" + + "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/transport" +) + +// StatusGet handles the getting of a fedi/activitypub representation of a particular status, performing appropriate +// authentication before returning a JSON serializable interface to the caller. +func (p *Processor) StatusGet(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + // get the status out of the database here + s, err := p.db.GetStatusByID(ctx, requestedStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + } + + if s.AccountID != requestedAccount.ID { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s does not belong to account with id %s", s.ID, requestedAccount.ID)) + } + + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if !visible { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) + } + + // requester is authorized to view the status, so convert it to AP representation and serialize it + asStatus, err := p.tc.StatusToAS(ctx, s) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err := streams.Serialize(asStatus) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} + +// GetStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate +// authentication before returning a JSON serializable interface to the caller. +func (p *Processor) StatusRepliesGet(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // get the account the request is referring to + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + // authenticate the request + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + // authorize the request: + // 1. check if a block exists between the requester and the requestee + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + + // get the status out of the database here + s := >smodel.Status{} + if err := p.db.GetWhere(ctx, []db.Where{ + {Key: "id", Value: requestedStatusID}, + {Key: "account_id", Value: requestedAccount.ID}, + }, s); err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting status with id %s and account id %s: %s", requestedStatusID, requestedAccount.ID, err)) + } + + visible, err := p.filter.StatusVisible(ctx, s, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + if !visible { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("status with id %s not visible to user with id %s", s.ID, requestingAccount.ID)) + } + + var data map[string]interface{} + + // now there are three scenarios: + // 1. we're asked for the whole collection and not a page -- we can just return the collection, with no items, but a link to 'first' page. + // 2. we're asked for a page but only_other_accounts has not been set in the query -- so we should just return the first page of the collection, with no items. + // 3. we're asked for a page, and only_other_accounts has been set, and min_id has optionally been set -- so we need to return some actual items! + switch { + case !page: + // scenario 1 + // get the collection + collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + data, err = streams.Serialize(collection) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + case page && requestURL.Query().Get("only_other_accounts") == "": + // scenario 2 + // get the collection + collection, err := p.tc.StatusToASRepliesCollection(ctx, s, onlyOtherAccounts) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + // but only return the first page + data, err = streams.Serialize(collection.GetActivityStreamsFirst().GetActivityStreamsCollectionPage()) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + default: + // scenario 3 + // get immediate children + replies, err := p.db.GetStatusChildren(ctx, s, true, minID) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + // filter children and extract URIs + replyURIs := map[string]*url.URL{} + for _, r := range replies { + // only show public or unlocked statuses as replies + if r.Visibility != gtsmodel.VisibilityPublic && r.Visibility != gtsmodel.VisibilityUnlocked { + continue + } + + // respect onlyOtherAccounts parameter + if onlyOtherAccounts && r.AccountID == requestedAccount.ID { + continue + } + + // only show replies that the status owner can see + visibleToStatusOwner, err := p.filter.StatusVisible(ctx, r, requestedAccount) + if err != nil || !visibleToStatusOwner { + continue + } + + // only show replies that the requester can see + visibleToRequester, err := p.filter.StatusVisible(ctx, r, requestingAccount) + if err != nil || !visibleToRequester { + continue + } + + rURI, err := url.Parse(r.URI) + if err != nil { + continue + } + + replyURIs[r.ID] = rURI + } + + repliesPage, err := p.tc.StatusURIsToASRepliesPage(ctx, s, onlyOtherAccounts, minID, replyURIs) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + data, err = streams.Serialize(repliesPage) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } + + return data, nil +} diff --git a/internal/processing/fedi/user.go b/internal/processing/fedi/user.go @@ -0,0 +1,88 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "context" + "fmt" + "net/url" + + "github.com/superseriousbusiness/activity/streams" + "github.com/superseriousbusiness/activity/streams/vocab" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/transport" + "github.com/superseriousbusiness/gotosocial/internal/uris" +) + +// UserGet handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication +// before returning a JSON serializable interface to the caller. +func (p *Processor) UserGet(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) { + // Get the instance-local account the request is referring to. + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + var requestedPerson vocab.ActivityStreamsPerson + + if uris.IsPublicKeyPath(requestURL) { + // if it's a public key path, we don't need to authenticate but we'll only serve the bare minimum user profile needed for the public key + requestedPerson, err = p.tc.AccountToASMinimal(ctx, requestedAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } else { + // if it's any other path, we want to fully authenticate the request before we serve any data, and then we can serve a more complete profile + requestingAccountURI, errWithCode := p.federator.AuthenticateFederatedRequest(ctx, requestedUsername) + if errWithCode != nil { + return nil, errWithCode + } + + // if we're not already handshaking/dereferencing a remote account, dereference it now + if !p.federator.Handshaking(requestedUsername, requestingAccountURI) { + requestingAccount, err := p.federator.GetAccountByURI( + transport.WithFastfail(ctx), requestedUsername, requestingAccountURI, false, + ) + if err != nil { + return nil, gtserror.NewErrorUnauthorized(err) + } + + blocked, err := p.db.IsBlocked(ctx, requestedAccount.ID, requestingAccount.ID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + if blocked { + return nil, gtserror.NewErrorUnauthorized(fmt.Errorf("block exists between accounts %s and %s", requestedAccount.ID, requestingAccount.ID)) + } + } + + requestedPerson, err = p.tc.AccountToAS(ctx, requestedAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + } + + data, err := streams.Serialize(requestedPerson) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return data, nil +} diff --git a/internal/processing/fedi/wellknown.go b/internal/processing/fedi/wellknown.go @@ -0,0 +1,126 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 fedi + +import ( + "context" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +const ( + nodeInfoVersion = "2.0" + nodeInfoSoftwareName = "gotosocial" + nodeInfoRel = "http://nodeinfo.diaspora.software/ns/schema/" + nodeInfoVersion + webfingerProfilePage = "http://webfinger.net/rel/profile-page" + webFingerProfilePageContentType = "text/html" + webfingerSelf = "self" + webFingerSelfContentType = "application/activity+json" + webfingerAccount = "acct" +) + +var ( + nodeInfoProtocols = []string{"activitypub"} + nodeInfoInbound = []string{} + nodeInfoOutbound = []string{} + nodeInfoMetadata = make(map[string]interface{}) +) + +// NodeInfoRelGet returns a well known response giving the path to node info. +func (p *Processor) NodeInfoRelGet(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) { + protocol := config.GetProtocol() + host := config.GetHost() + + return &apimodel.WellKnownResponse{ + Links: []apimodel.Link{ + { + Rel: nodeInfoRel, + Href: fmt.Sprintf("%s://%s/nodeinfo/%s", protocol, host, nodeInfoVersion), + }, + }, + }, nil +} + +// NodeInfoGet returns a node info struct in response to a node info request. +func (p *Processor) NodeInfoGet(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) { + host := config.GetHost() + + userCount, err := p.db.CountInstanceUsers(ctx, host) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + postCount, err := p.db.CountInstanceStatuses(ctx, host) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return &apimodel.Nodeinfo{ + Version: nodeInfoVersion, + Software: apimodel.NodeInfoSoftware{ + Name: nodeInfoSoftwareName, + Version: config.GetSoftwareVersion(), + }, + Protocols: nodeInfoProtocols, + Services: apimodel.NodeInfoServices{ + Inbound: nodeInfoInbound, + Outbound: nodeInfoOutbound, + }, + OpenRegistrations: config.GetAccountsRegistrationOpen(), + Usage: apimodel.NodeInfoUsage{ + Users: apimodel.NodeInfoUsers{ + Total: userCount, + }, + LocalPosts: postCount, + }, + Metadata: nodeInfoMetadata, + }, nil +} + +// WebfingerGet handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. +func (p *Processor) WebfingerGet(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) { + // Get the local account the request is referring to. + requestedAccount, err := p.db.GetAccountByUsernameDomain(ctx, requestedUsername, "") + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("database error getting account with username %s: %s", requestedUsername, err)) + } + + return &apimodel.WellKnownResponse{ + Subject: webfingerAccount + ":" + requestedAccount.Username + "@" + config.GetAccountDomain(), + Aliases: []string{ + requestedAccount.URI, + requestedAccount.URL, + }, + Links: []apimodel.Link{ + { + Rel: webfingerProfilePage, + Type: webFingerProfilePageContentType, + Href: requestedAccount.URL, + }, + { + Rel: webfingerSelf, + Type: webFingerSelfContentType, + Href: requestedAccount.URI, + }, + }, + }, nil +} diff --git a/internal/processing/followrequest.go b/internal/processing/followrequest.go @@ -29,7 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) -func (p *processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { +func (p *Processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) { frs, err := p.db.GetAccountFollowRequests(ctx, auth.Account.ID) if err != nil { if err != db.ErrNoEntries { @@ -56,7 +56,7 @@ func (p *processor) FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([] return accts, nil } -func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { +func (p *Processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { follow, err := p.db.AcceptFollowRequest(ctx, accountID, auth.Account.ID) if err != nil { return nil, gtserror.NewErrorNotFound(err) @@ -99,7 +99,7 @@ func (p *processor) FollowRequestAccept(ctx context.Context, auth *oauth.Auth, a return r, nil } -func (p *processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { +func (p *Processor) FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) { followRequest, err := p.db.RejectFollowRequest(ctx, accountID, auth.Account.ID) if err != nil { return nil, gtserror.NewErrorNotFound(err) diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go @@ -34,7 +34,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/messages" ) -func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { // Allocate new log fields slice fields := make([]kv.Field, 3, 4) fields[0] = kv.Field{"activityType", clientMsg.APActivityType} @@ -131,7 +131,7 @@ func (p *processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages return nil } -func (p *processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { account, ok := clientMsg.GTSModel.(*gtsmodel.Account) if !ok { return errors.New("account was not parseable as *gtsmodel.Account") @@ -149,10 +149,10 @@ func (p *processor) processCreateAccountFromClientAPI(ctx context.Context, clien } // email a confirmation to this user - return p.userProcessor.SendConfirmEmail(ctx, user, account.Username) + return p.User().EmailSendConfirmation(ctx, user, account.Username) } -func (p *processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { status, ok := clientMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("note was not parseable as *gtsmodel.Status") @@ -169,7 +169,7 @@ func (p *processor) processCreateStatusFromClientAPI(ctx context.Context, client return p.federateStatus(ctx, status) } -func (p *processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateFollowRequestFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) if !ok { return errors.New("followrequest was not parseable as *gtsmodel.FollowRequest") @@ -182,7 +182,7 @@ func (p *processor) processCreateFollowRequestFromClientAPI(ctx context.Context, return p.federateFollow(ctx, followRequest, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) if !ok { return errors.New("fave was not parseable as *gtsmodel.StatusFave") @@ -195,7 +195,7 @@ func (p *processor) processCreateFaveFromClientAPI(ctx context.Context, clientMs return p.federateFave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { boostWrapperStatus, ok := clientMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("boost was not parseable as *gtsmodel.Status") @@ -212,7 +212,7 @@ func (p *processor) processCreateAnnounceFromClientAPI(ctx context.Context, clie return p.federateAnnounce(ctx, boostWrapperStatus, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processCreateBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { block, ok := clientMsg.GTSModel.(*gtsmodel.Block) if !ok { return errors.New("block was not parseable as *gtsmodel.Block") @@ -232,7 +232,7 @@ func (p *processor) processCreateBlockFromClientAPI(ctx context.Context, clientM return p.federateBlock(ctx, block) } -func (p *processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processUpdateAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { account, ok := clientMsg.GTSModel.(*gtsmodel.Account) if !ok { return errors.New("account was not parseable as *gtsmodel.Account") @@ -241,7 +241,7 @@ func (p *processor) processUpdateAccountFromClientAPI(ctx context.Context, clien return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount) } -func (p *processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) if !ok { return errors.New("accept was not parseable as *gtsmodel.Follow") @@ -254,7 +254,7 @@ func (p *processor) processAcceptFollowFromClientAPI(ctx context.Context, client return p.federateAcceptFollowRequest(ctx, follow) } -func (p *processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processRejectFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { followRequest, ok := clientMsg.GTSModel.(*gtsmodel.FollowRequest) if !ok { return errors.New("reject was not parseable as *gtsmodel.FollowRequest") @@ -263,7 +263,7 @@ func (p *processor) processRejectFollowFromClientAPI(ctx context.Context, client return p.federateRejectFollowRequest(ctx, followRequest) } -func (p *processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processUndoFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) if !ok { return errors.New("undo was not parseable as *gtsmodel.Follow") @@ -271,7 +271,7 @@ func (p *processor) processUndoFollowFromClientAPI(ctx context.Context, clientMs return p.federateUnfollow(ctx, follow, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { block, ok := clientMsg.GTSModel.(*gtsmodel.Block) if !ok { return errors.New("undo was not parseable as *gtsmodel.Block") @@ -279,7 +279,7 @@ func (p *processor) processUndoBlockFromClientAPI(ctx context.Context, clientMsg return p.federateUnblock(ctx, block) } -func (p *processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { fave, ok := clientMsg.GTSModel.(*gtsmodel.StatusFave) if !ok { return errors.New("undo was not parseable as *gtsmodel.StatusFave") @@ -287,7 +287,7 @@ func (p *processor) processUndoFaveFromClientAPI(ctx context.Context, clientMsg return p.federateUnfave(ctx, fave, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processUndoAnnounceFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { boost, ok := clientMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("undo was not parseable as *gtsmodel.Status") @@ -304,7 +304,7 @@ func (p *processor) processUndoAnnounceFromClientAPI(ctx context.Context, client return p.federateUnannounce(ctx, boost, clientMsg.OriginAccount, clientMsg.TargetAccount) } -func (p *processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processDeleteStatusFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { statusToDelete, ok := clientMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("note was not parseable as *gtsmodel.Status") @@ -326,7 +326,7 @@ func (p *processor) processDeleteStatusFromClientAPI(ctx context.Context, client return p.federateStatusDelete(ctx, statusToDelete) } -func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processDeleteAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { // the origin of the delete could be either a domain block, or an action by another (or this) account var origin string if domainBlock, ok := clientMsg.GTSModel.(*gtsmodel.DomainBlock); ok { @@ -341,10 +341,10 @@ func (p *processor) processDeleteAccountFromClientAPI(ctx context.Context, clien return err } - return p.accountProcessor.Delete(ctx, clientMsg.TargetAccount, origin) + return p.account.Delete(ctx, clientMsg.TargetAccount, origin) } -func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { +func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { report, ok := clientMsg.GTSModel.(*gtsmodel.Report) if !ok { return errors.New("report was not parseable as *gtsmodel.Report") @@ -362,7 +362,7 @@ func (p *processor) processReportAccountFromClientAPI(ctx context.Context, clien // TODO: move all the below functions into federation.Federator -func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { +func (p *Processor) federateAccountDelete(ctx context.Context, account *gtsmodel.Account) error { // do nothing if this isn't our account if account.Domain != "" { return nil @@ -415,7 +415,7 @@ func (p *processor) federateAccountDelete(ctx context.Context, account *gtsmodel return err } -func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) federateStatus(ctx context.Context, status *gtsmodel.Status) error { // do nothing if the status shouldn't be federated if !*status.Federated { return nil @@ -453,7 +453,7 @@ func (p *processor) federateStatus(ctx context.Context, status *gtsmodel.Status) return err } -func (p *processor) federateStatusDelete(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) federateStatusDelete(ctx context.Context, status *gtsmodel.Status) error { if status.Account == nil { statusAccount, err := p.db.GetAccountByID(ctx, status.AccountID) if err != nil { @@ -503,7 +503,7 @@ func (p *processor) federateStatusDelete(ctx context.Context, status *gtsmodel.S return err } -func (p *processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { +func (p *Processor) federateFollow(ctx context.Context, followRequest *gtsmodel.FollowRequest, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { // if both accounts are local there's nothing to do here if originAccount.Domain == "" && targetAccount.Domain == "" { return nil @@ -525,7 +525,7 @@ func (p *processor) federateFollow(ctx context.Context, followRequest *gtsmodel. return err } -func (p *processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { +func (p *Processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follow, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { // if both accounts are local there's nothing to do here if originAccount.Domain == "" && targetAccount.Domain == "" { return nil @@ -566,7 +566,7 @@ func (p *processor) federateUnfollow(ctx context.Context, follow *gtsmodel.Follo return err } -func (p *processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { +func (p *Processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { // if both accounts are local there's nothing to do here if originAccount.Domain == "" && targetAccount.Domain == "" { return nil @@ -605,7 +605,7 @@ func (p *processor) federateUnfave(ctx context.Context, fave *gtsmodel.StatusFav return err } -func (p *processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { +func (p *Processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Status, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { if originAccount.Domain != "" { // nothing to do here return nil @@ -640,7 +640,7 @@ func (p *processor) federateUnannounce(ctx context.Context, boost *gtsmodel.Stat return err } -func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error { +func (p *Processor) federateAcceptFollowRequest(ctx context.Context, follow *gtsmodel.Follow) error { if follow.Account == nil { a, err := p.db.GetAccountByID(ctx, follow.AccountID) if err != nil { @@ -713,7 +713,7 @@ func (p *processor) federateAcceptFollowRequest(ctx context.Context, follow *gts return err } -func (p *processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { +func (p *Processor) federateRejectFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { if followRequest.Account == nil { a, err := p.db.GetAccountByID(ctx, followRequest.AccountID) if err != nil { @@ -787,7 +787,7 @@ func (p *processor) federateRejectFollowRequest(ctx context.Context, followReque return err } -func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { +func (p *Processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, originAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) error { // if both accounts are local there's nothing to do here if originAccount.Domain == "" && targetAccount.Domain == "" { return nil @@ -807,7 +807,7 @@ func (p *processor) federateFave(ctx context.Context, fave *gtsmodel.StatusFave, return err } -func (p *processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error { +func (p *Processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gtsmodel.Status, boostingAccount *gtsmodel.Account, boostedAccount *gtsmodel.Account) error { announce, err := p.tc.BoostToAS(ctx, boostWrapperStatus, boostingAccount, boostedAccount) if err != nil { return fmt.Errorf("federateAnnounce: error converting status to announce: %s", err) @@ -822,7 +822,7 @@ func (p *processor) federateAnnounce(ctx context.Context, boostWrapperStatus *gt return err } -func (p *processor) federateAccountUpdate(ctx context.Context, updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error { +func (p *Processor) federateAccountUpdate(ctx context.Context, updatedAccount *gtsmodel.Account, originAccount *gtsmodel.Account) error { person, err := p.tc.AccountToAS(ctx, updatedAccount) if err != nil { return fmt.Errorf("federateAccountUpdate: error converting account to person: %s", err) @@ -842,7 +842,7 @@ func (p *processor) federateAccountUpdate(ctx context.Context, updatedAccount *g return err } -func (p *processor) federateBlock(ctx context.Context, block *gtsmodel.Block) error { +func (p *Processor) federateBlock(ctx context.Context, block *gtsmodel.Block) error { if block.Account == nil { blockAccount, err := p.db.GetAccountByID(ctx, block.AccountID) if err != nil { @@ -878,7 +878,7 @@ func (p *processor) federateBlock(ctx context.Context, block *gtsmodel.Block) er return err } -func (p *processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) error { +func (p *Processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) error { if block.Account == nil { blockAccount, err := p.db.GetAccountByID(ctx, block.AccountID) if err != nil { @@ -932,7 +932,7 @@ func (p *processor) federateUnblock(ctx context.Context, block *gtsmodel.Block) return err } -func (p *processor) federateReport(ctx context.Context, report *gtsmodel.Report) error { +func (p *Processor) federateReport(ctx context.Context, report *gtsmodel.Report) error { if report.TargetAccount == nil { reportTargetAccount, err := p.db.GetAccountByID(ctx, report.TargetAccountID) if err != nil { diff --git a/internal/processing/fromclientapi_test.go b/internal/processing/fromclientapi_test.go @@ -46,12 +46,12 @@ func (suite *FromClientAPITestSuite) TestProcessStreamNewStatus() { receivingAccount := suite.testAccounts["local_account_1"] // open a home timeline stream for zork - wssStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelineHome) suite.NoError(errWithCode) // open another stream for zork, but for a different timeline; // this shouldn't get stuff streamed into it, since it's for the public timeline - irrelevantStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelinePublic) + irrelevantStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelinePublic) suite.NoError(errWithCode) // make a new status from admin account @@ -125,7 +125,7 @@ func (suite *FromClientAPITestSuite) TestProcessStatusDelete() { boostOfDeletedStatus := suite.testStatuses["admin_account_status_4"] // open a home timeline stream for turtle, who follows zork - wssStream, errWithCode := suite.processor.OpenStreamForAccount(ctx, receivingAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(ctx, receivingAccount, stream.TimelineHome) suite.NoError(errWithCode) // delete the status from the db first, to mimic what would have already happened earlier up the flow diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go @@ -30,7 +30,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/stream" ) -func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) error { // if there are no mentions in this status then just bail if len(status.MentionIDs) == 0 { return nil @@ -97,7 +97,7 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, m.TargetAccount); err != nil { + if err := p.stream.Notify(apiNotif, m.TargetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } } @@ -105,7 +105,7 @@ func (p *processor) notifyStatus(ctx context.Context, status *gtsmodel.Status) e return nil } -func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { +func (p *Processor) notifyFollowRequest(ctx context.Context, followRequest *gtsmodel.FollowRequest) error { // make sure we have the target account pinned on the follow request if followRequest.TargetAccount == nil { a, err := p.db.GetAccountByID(ctx, followRequest.TargetAccountID) @@ -139,14 +139,14 @@ func (p *processor) notifyFollowRequest(ctx context.Context, followRequest *gtsm return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil { + if err := p.stream.Notify(apiNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } return nil } -func (p *processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error { +func (p *Processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, targetAccount *gtsmodel.Account) error { // return if this isn't a local account if targetAccount.Domain != "" { return nil @@ -180,14 +180,14 @@ func (p *processor) notifyFollow(ctx context.Context, follow *gtsmodel.Follow, t return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil { + if err := p.stream.Notify(apiNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } return nil } -func (p *processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error { +func (p *Processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) error { // ignore self-faves if fave.TargetAccountID == fave.AccountID { return nil @@ -228,14 +228,14 @@ func (p *processor) notifyFave(ctx context.Context, fave *gtsmodel.StatusFave) e return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, targetAccount); err != nil { + if err := p.stream.Notify(apiNotif, targetAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } return nil } -func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) error { if status.BoostOfID == "" { // not a boost, nothing to do return nil @@ -302,7 +302,7 @@ func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) return fmt.Errorf("notifyStatus: error converting notification to api representation: %s", err) } - if err := p.streamingProcessor.StreamNotificationToAccount(apiNotif, status.BoostOfAccount); err != nil { + if err := p.stream.Notify(apiNotif, status.BoostOfAccount); err != nil { return fmt.Errorf("notifyStatus: error streaming notification to account: %s", err) } @@ -311,7 +311,7 @@ func (p *processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) // timelineStatus processes the given new status and inserts it into // the HOME timelines of accounts that follow the status author. -func (p *processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error { // make sure the author account is pinned onto the status if status.Account == nil { a, err := p.db.GetAccountByID(ctx, status.AccountID) @@ -370,7 +370,7 @@ func (p *processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) // // If the status was inserted into the home timeline of the given account, // it will also be streamed via websockets to the user. -func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) { +func (p *Processor) timelineStatusForAccount(ctx context.Context, status *gtsmodel.Status, accountID string, errors chan error, wg *sync.WaitGroup) { defer wg.Done() // get the timeline owner account @@ -406,7 +406,7 @@ func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmod return } - if err := p.streamingProcessor.StreamUpdateToAccount(apiStatus, timelineAccount, stream.TimelineHome); err != nil { + if err := p.stream.Update(apiStatus, timelineAccount, stream.TimelineHome); err != nil { errors <- fmt.Errorf("timelineStatusForAccount: error streaming status %s: %s", status.ID, err) } } @@ -414,17 +414,17 @@ func (p *processor) timelineStatusForAccount(ctx context.Context, status *gtsmod // deleteStatusFromTimelines completely removes the given status from all timelines. // It will also stream deletion of the status to all open streams. -func (p *processor) deleteStatusFromTimelines(ctx context.Context, status *gtsmodel.Status) error { +func (p *Processor) deleteStatusFromTimelines(ctx context.Context, status *gtsmodel.Status) error { if err := p.statusTimelines.WipeItemFromAllTimelines(ctx, status.ID); err != nil { return err } - return p.streamingProcessor.StreamDelete(status.ID) + return p.stream.Delete(status.ID) } // wipeStatus contains common logic used to totally delete a status // + all its attachments, notifications, boosts, and timeline entries. -func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error { +func (p *Processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Status, deleteAttachments bool) error { // either delete all attachments for this status, or simply // unattach all attachments for this status, so they'll be // cleaned later by a separate process; reason to unattach rather @@ -432,13 +432,13 @@ func (p *processor) wipeStatus(ctx context.Context, statusToDelete *gtsmodel.Sta // to another status immediately (in case of delete + redraft) if deleteAttachments { for _, a := range statusToDelete.AttachmentIDs { - if err := p.mediaProcessor.Delete(ctx, a); err != nil { + if err := p.media.Delete(ctx, a); err != nil { return err } } } else { for _, a := range statusToDelete.AttachmentIDs { - if _, err := p.mediaProcessor.Unattach(ctx, statusToDelete.Account, a); err != nil { + if _, err := p.media.Unattach(ctx, statusToDelete.Account, a); err != nil { return err } } diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go @@ -36,7 +36,7 @@ import ( // ProcessFromFederator reads the APActivityType and APObjectType of an incoming message from the federator, // and directs the message into the appropriate side effect handler function, or simply does nothing if there's // no handler function defined for the combination of Activity and Object. -func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { // Allocate new log fields slice fields := make([]kv.Field, 3, 5) fields[0] = kv.Field{"activityType", federatorMsg.APActivityType} @@ -108,7 +108,7 @@ func (p *processor) ProcessFromFederator(ctx context.Context, federatorMsg messa } // processCreateStatusFromFederator handles Activity Create and Object Note -func (p *processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { // check for either an IRI that we still need to dereference, OR an already dereferenced // and converted status pinned to the message. var status *gtsmodel.Status @@ -177,7 +177,7 @@ func (p *processor) processCreateStatusFromFederator(ctx context.Context, federa } // processCreateFaveFromFederator handles Activity Create and Object Like -func (p *processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateFaveFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { incomingFave, ok := federatorMsg.GTSModel.(*gtsmodel.StatusFave) if !ok { return errors.New("like was not parseable as *gtsmodel.StatusFave") @@ -219,7 +219,7 @@ func (p *processor) processCreateFaveFromFederator(ctx context.Context, federato } // processCreateFollowRequestFromFederator handles Activity Create and Object Follow -func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateFollowRequestFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { followRequest, ok := federatorMsg.GTSModel.(*gtsmodel.FollowRequest) if !ok { return errors.New("incomingFollowRequest was not parseable as *gtsmodel.FollowRequest") @@ -280,7 +280,7 @@ func (p *processor) processCreateFollowRequestFromFederator(ctx context.Context, } // processCreateAnnounceFromFederator handles Activity Create and Object Announce -func (p *processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateAnnounceFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { incomingAnnounce, ok := federatorMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("announce was not parseable as *gtsmodel.Status") @@ -340,7 +340,7 @@ func (p *processor) processCreateAnnounceFromFederator(ctx context.Context, fede } // processCreateBlockFromFederator handles Activity Create and Object Block -func (p *processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateBlockFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { block, ok := federatorMsg.GTSModel.(*gtsmodel.Block) if !ok { return errors.New("block was not parseable as *gtsmodel.Block") @@ -359,7 +359,7 @@ func (p *processor) processCreateBlockFromFederator(ctx context.Context, federat return nil } -func (p *processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { // TODO: handle side effects of flag creation: // - send email to admins // - notify admins @@ -367,7 +367,7 @@ func (p *processor) processCreateFlagFromFederator(ctx context.Context, federato } // processUpdateAccountFromFederator handles Activity Update and Object Profile -func (p *processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processUpdateAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { incomingAccount, ok := federatorMsg.GTSModel.(*gtsmodel.Account) if !ok { return errors.New("profile was not parseable as *gtsmodel.Account") @@ -391,7 +391,7 @@ func (p *processor) processUpdateAccountFromFederator(ctx context.Context, feder } // processDeleteStatusFromFederator handles Activity Delete and Object Note -func (p *processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processDeleteStatusFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { statusToDelete, ok := federatorMsg.GTSModel.(*gtsmodel.Status) if !ok { return errors.New("note was not parseable as *gtsmodel.Status") @@ -405,11 +405,11 @@ func (p *processor) processDeleteStatusFromFederator(ctx context.Context, federa } // processDeleteAccountFromFederator handles Activity Delete and Object Profile -func (p *processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { +func (p *Processor) processDeleteAccountFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { account, ok := federatorMsg.GTSModel.(*gtsmodel.Account) if !ok { return errors.New("account delete was not parseable as *gtsmodel.Account") } - return p.accountProcessor.Delete(ctx, account, account.ID) + return p.account.Delete(ctx, account, account.ID) } diff --git a/internal/processing/fromfederator_test.go b/internal/processing/fromfederator_test.go @@ -117,7 +117,7 @@ func (suite *FromFederatorTestSuite) TestProcessReplyMention() { Likeable: testrig.FalseBool(), } - wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), repliedAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome) suite.NoError(errWithCode) // id the status based on the time it was created @@ -183,7 +183,7 @@ func (suite *FromFederatorTestSuite) TestProcessFave() { favedStatus := suite.testStatuses["local_account_1_status_1"] favingAccount := suite.testAccounts["remote_account_1"] - wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), favedAccount, stream.TimelineNotifications) + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications) suite.NoError(errWithCode) fave := >smodel.StatusFave{ @@ -256,7 +256,7 @@ func (suite *FromFederatorTestSuite) TestProcessFaveWithDifferentReceivingAccoun favedStatus := suite.testStatuses["local_account_1_status_1"] favingAccount := suite.testAccounts["remote_account_1"] - wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), receivingAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome) suite.NoError(errWithCode) fave := >smodel.StatusFave{ @@ -400,7 +400,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestLocked() { // target is a locked account targetAccount := suite.testAccounts["local_account_2"] - wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) suite.NoError(errWithCode) // put the follow request in the database as though it had passed through the federating db already @@ -457,7 +457,7 @@ func (suite *FromFederatorTestSuite) TestProcessFollowRequestUnlocked() { // target is an unlocked account targetAccount := suite.testAccounts["local_account_1"] - wssStream, errWithCode := suite.processor.OpenStreamForAccount(context.Background(), targetAccount, stream.TimelineHome) + wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome) suite.NoError(errWithCode) // put the follow request in the database as though it had passed through the federating db already diff --git a/internal/processing/instance.go b/internal/processing/instance.go @@ -33,7 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/validate" ) -func (p *processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) { +func (p *Processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, error) { i := >smodel.Instance{} if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: config.GetHost()}}, i); err != nil { return nil, err @@ -41,7 +41,7 @@ func (p *processor) getThisInstance(ctx context.Context) (*gtsmodel.Instance, er return i, nil } -func (p *processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { +func (p *Processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) { i, err := p.getThisInstance(ctx) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err)) @@ -55,7 +55,7 @@ func (p *processor) InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gt return ai, nil } -func (p *processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) { +func (p *Processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) { i, err := p.getThisInstance(ctx) if err != nil { return nil, gtserror.NewErrorInternalError(fmt.Errorf("db error fetching instance: %s", err)) @@ -69,7 +69,7 @@ func (p *processor) InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gt return ai, nil } -func (p *processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) { +func (p *Processor) InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) { domains := []*apimodel.Domain{} if includeOpen { @@ -120,7 +120,7 @@ func (p *processor) InstancePeersGet(ctx context.Context, includeSuspended bool, return domains, nil } -func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) { +func (p *Processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) { // fetch the instance entry from the db for processing i := >smodel.Instance{} host := config.GetHost() @@ -223,7 +223,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe if form.Avatar != nil && form.Avatar.Size != 0 { // process instance avatar image + description - avatarInfo, err := p.accountProcessor.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID) + avatarInfo, err := p.account.UpdateAvatar(ctx, form.Avatar, form.AvatarDescription, ia.ID) if err != nil { return nil, gtserror.NewErrorBadRequest(err, "error processing avatar") } @@ -240,7 +240,7 @@ func (p *processor) InstancePatch(ctx context.Context, form *apimodel.InstanceSe if form.Header != nil && form.Header.Size != 0 { // process instance header image - headerInfo, err := p.accountProcessor.UpdateHeader(ctx, form.Header, nil, ia.ID) + headerInfo, err := p.account.UpdateHeader(ctx, form.Header, nil, ia.ID) if err != nil { return nil, gtserror.NewErrorBadRequest(err, "error processing header") } diff --git a/internal/processing/media.go b/internal/processing/media.go @@ -1,47 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) { - return p.mediaProcessor.Create(ctx, authed.Account, form) -} - -func (p *processor) MediaGet(ctx context.Context, authed *oauth.Auth, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { - return p.mediaProcessor.GetMedia(ctx, authed.Account, mediaAttachmentID) -} - -func (p *processor) MediaUpdate(ctx context.Context, authed *oauth.Auth, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { - return p.mediaProcessor.Update(ctx, authed.Account, mediaAttachmentID, form) -} - -func (p *processor) FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) { - return p.mediaProcessor.GetFile(ctx, authed.Account, form) -} - -func (p *processor) CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) { - return p.mediaProcessor.GetCustomEmojis(ctx) -} diff --git a/internal/processing/media/create.go b/internal/processing/media/create.go @@ -29,7 +29,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/media" ) -func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) { +// Create creates a new media attachment belonging to the given account, using the request form. +func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) { data := func(innerCtx context.Context) (io.ReadCloser, int64, error) { f, err := form.File.Open() return f, form.File.Size, err diff --git a/internal/processing/media/delete.go b/internal/processing/media/delete.go @@ -11,7 +11,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) -func (p *processor) Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode { +// Delete deletes the media attachment with the given ID, including all files pertaining to that attachment. +func (p *Processor) Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode { attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID) if err != nil { if err == db.ErrNoEntries { diff --git a/internal/processing/media/getemoji.go b/internal/processing/media/getemoji.go @@ -28,7 +28,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/log" ) -func (p *processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) { +// GetCustomEmojis returns a list of all useable local custom emojis stored on this instance. +// 'useable' in this context means visible and picker, and not disabled. +func (p *Processor) GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) { emojis, err := p.db.GetUseableEmojis(ctx) if err != nil { if err != db.ErrNoEntries { diff --git a/internal/processing/media/getfile.go b/internal/processing/media/getfile.go @@ -33,42 +33,15 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/uris" ) -// ParseMediaType converts s to a recognized MediaType, or returns an error if unrecognized -func parseMediaType(s string) (media.Type, error) { - switch s { - case string(media.TypeAttachment): - return media.TypeAttachment, nil - case string(media.TypeHeader): - return media.TypeHeader, nil - case string(media.TypeAvatar): - return media.TypeAvatar, nil - case string(media.TypeEmoji): - return media.TypeEmoji, nil - } - return "", fmt.Errorf("%s not a recognized media.Type", s) -} - -// ParseMediaSize converts s to a recognized MediaSize, or returns an error if unrecognized -func parseMediaSize(s string) (media.Size, error) { - switch s { - case string(media.SizeSmall): - return media.SizeSmall, nil - case string(media.SizeOriginal): - return media.SizeOriginal, nil - case string(media.SizeStatic): - return media.SizeStatic, nil - } - return "", fmt.Errorf("%s not a recognized media.Size", s) -} - -func (p *processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) { +// GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content. +func (p *Processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) { // parse the form fields - mediaSize, err := parseMediaSize(form.MediaSize) + mediaSize, err := parseSize(form.MediaSize) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("media size %s not valid", form.MediaSize)) } - mediaType, err := parseMediaType(form.MediaType) + mediaType, err := parseType(form.MediaType) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("media type %s not valid", form.MediaType)) } @@ -112,7 +85,37 @@ func (p *processor) GetFile(ctx context.Context, requestingAccount *gtsmodel.Acc } } -func (p *processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) { +/* + UTIL FUNCTIONS +*/ + +func parseType(s string) (media.Type, error) { + switch s { + case string(media.TypeAttachment): + return media.TypeAttachment, nil + case string(media.TypeHeader): + return media.TypeHeader, nil + case string(media.TypeAvatar): + return media.TypeAvatar, nil + case string(media.TypeEmoji): + return media.TypeEmoji, nil + } + return "", fmt.Errorf("%s not a recognized media.Type", s) +} + +func parseSize(s string) (media.Size, error) { + switch s { + case string(media.SizeSmall): + return media.SizeSmall, nil + case string(media.SizeOriginal): + return media.SizeOriginal, nil + case string(media.SizeStatic): + return media.SizeStatic, nil + } + return "", fmt.Errorf("%s not a recognized media.Size", s) +} + +func (p *Processor) getAttachmentContent(ctx context.Context, requestingAccount *gtsmodel.Account, wantedMediaID string, owningAccountID string, mediaSize media.Size) (*apimodel.Content, gtserror.WithCode) { // retrieve attachment from the database and do basic checks on it a, err := p.db.GetAttachmentByID(ctx, wantedMediaID) if err != nil { @@ -196,7 +199,7 @@ func (p *processor) getAttachmentContent(ctx context.Context, requestingAccount return p.retrieveFromStorage(ctx, storagePath, attachmentContent) } -func (p *processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) { +func (p *Processor) getEmojiContent(ctx context.Context, fileName string, owningAccountID string, emojiSize media.Size) (*apimodel.Content, gtserror.WithCode) { emojiContent := &apimodel.Content{} var storagePath string @@ -231,7 +234,7 @@ func (p *processor) getEmojiContent(ctx context.Context, fileName string, owning return p.retrieveFromStorage(ctx, storagePath, emojiContent) } -func (p *processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) { +func (p *Processor) retrieveFromStorage(ctx context.Context, storagePath string, content *apimodel.Content) (*apimodel.Content, gtserror.WithCode) { // If running on S3 storage with proxying disabled then // just fetch a pre-signed URL instead of serving the content. if url := p.storage.URL(ctx, storagePath); url != nil { diff --git a/internal/processing/media/getmedia.go b/internal/processing/media/getmedia.go @@ -29,7 +29,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) GetMedia(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { +func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID) if err != nil { if err == db.ErrNoEntries { diff --git a/internal/processing/media/media.go b/internal/processing/media/media.go @@ -19,35 +19,14 @@ package media import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) -// Processor wraps a bunch of functions for processing media actions. -type Processor interface { - // Create creates a new media attachment belonging to the given account, using the request form. - Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) - // Delete deletes the media attachment with the given ID, including all files pertaining to that attachment. - Delete(ctx context.Context, mediaAttachmentID string) gtserror.WithCode - // Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available - // for reattachment again. - Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) - // GetFile retrieves a file from storage and streams it back to the caller via an io.reader embedded in *apimodel.Content. - GetFile(ctx context.Context, account *gtsmodel.Account, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) - GetCustomEmojis(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) - GetMedia(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) - Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) -} - -type processor struct { +type Processor struct { tc typeutils.TypeConverter mediaManager media.Manager transportController transport.Controller @@ -57,7 +36,7 @@ type processor struct { // New returns a new media processor. func New(db db.DB, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller, storage *storage.Driver) Processor { - return &processor{ + return Processor{ tc: tc, mediaManager: mediaManager, transportController: transportController, diff --git a/internal/processing/media/unattach.go b/internal/processing/media/unattach.go @@ -30,7 +30,9 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { +// Unattach unattaches the media attachment with the given ID from any statuses it was attached to, making it available +// for reattachment again. +func (p *Processor) Unattach(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string) (*apimodel.Attachment, gtserror.WithCode) { attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID) if err != nil { if err == db.ErrNoEntries { diff --git a/internal/processing/media/update.go b/internal/processing/media/update.go @@ -30,7 +30,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/text" ) -func (p *processor) Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { +// Update updates a media attachment with the given id, using the provided form parameters. +func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, mediaAttachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) { attachment, err := p.db.GetAttachmentByID(ctx, mediaAttachmentID) if err != nil { if err == db.ErrNoEntries { diff --git a/internal/processing/notification.go b/internal/processing/notification.go @@ -28,7 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/util" ) -func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) { +func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) { notifs, err := p.db.GetNotifications(ctx, authed.Account.ID, excludeTypes, limit, maxID, sinceID) if err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -71,7 +71,7 @@ func (p *processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ex }) } -func (p *processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode { +func (p *Processor) NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode { err := p.db.ClearNotifications(ctx, authed.Account.ID) if err != nil { return gtserror.NewErrorInternalError(err) diff --git a/internal/processing/oauth.go b/internal/processing/oauth.go @@ -25,17 +25,17 @@ import ( "github.com/superseriousbusiness/oauth2/v4" ) -func (p *processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode { +func (p *Processor) OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode { // todo: some kind of metrics stuff here return p.oauthServer.HandleAuthorizeRequest(w, r) } -func (p *processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) { +func (p *Processor) OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) { // todo: some kind of metrics stuff here return p.oauthServer.HandleTokenRequest(r) } -func (p *processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) { +func (p *Processor) OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) { // todo: some kind of metrics stuff here return p.oauthServer.ValidationBearerToken(r) } diff --git a/internal/processing/processor.go b/internal/processing/processor.go @@ -19,291 +19,35 @@ package processing import ( - "context" - "net/http" - "net/url" - "time" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" + mm "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/processing/account" "github.com/superseriousbusiness/gotosocial/internal/processing/admin" - federationProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/federation" - mediaProcessor "github.com/superseriousbusiness/gotosocial/internal/processing/media" + "github.com/superseriousbusiness/gotosocial/internal/processing/fedi" + "github.com/superseriousbusiness/gotosocial/internal/processing/media" "github.com/superseriousbusiness/gotosocial/internal/processing/report" "github.com/superseriousbusiness/gotosocial/internal/processing/status" - "github.com/superseriousbusiness/gotosocial/internal/processing/streaming" + "github.com/superseriousbusiness/gotosocial/internal/processing/stream" "github.com/superseriousbusiness/gotosocial/internal/processing/user" "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/internal/stream" "github.com/superseriousbusiness/gotosocial/internal/timeline" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/visibility" - "github.com/superseriousbusiness/oauth2/v4" ) -// Processor should be passed to api modules (see internal/apimodule/...). It is used for -// passing messages back and forth from the client API and the federating interface, via channels. -// It also contains logic for filtering which messages should end up where. -// It is designed to be used asynchronously: the client API and the federating API should just be able to -// fire messages into the processor and not wait for a reply before proceeding with other work. This allows -// for clean distribution of messages without slowing down the client API and harming the user experience. -type Processor interface { - // Start starts the Processor, reading from its channels and passing messages back and forth. - Start() error - // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. - Stop() error - // ProcessFromClientAPI processes one message coming from the clientAPI channel, and triggers appropriate side effects. - ProcessFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error - // ProcessFromFederator processes one message coming from the federator channel, and triggers appropriate side effects. - ProcessFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error - - /* - CLIENT API-FACING PROCESSING FUNCTIONS - These functions are intended to be called when the API client needs an immediate (ie., synchronous) reply - to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly - formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate - response, pass work to the processor using a channel instead. - */ - - // AccountCreate processes the given form for creating a new account, returning an oauth token for that account if successful. - AccountCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountCreateRequest) (*apimodel.Token, gtserror.WithCode) - // AccountDeleteLocal processes the delete of a LOCAL account using the given form. - AccountDeleteLocal(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountDeleteRequest) gtserror.WithCode - // AccountGet processes the given request for account information. - AccountGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Account, gtserror.WithCode) - // AccountGet processes the given request for account information. - AccountGetLocalByUsername(ctx context.Context, authed *oauth.Auth, username string) (*apimodel.Account, gtserror.WithCode) - AccountGetCustomCSSForUsername(ctx context.Context, username string) (string, gtserror.WithCode) - // AccountGetRSSFeedForUsername returns a function to get the RSS feed of latest posts for given local account username. - // This function should only be called if necessary: the given lastModified time can be used to check this. - // Will return 404 if an rss feed for that user is not available, or a different error if something else goes wrong. - AccountGetRSSFeedForUsername(ctx context.Context, username string) (func() (string, gtserror.WithCode), time.Time, gtserror.WithCode) - // AccountUpdate processes the update of an account with the given form - AccountUpdate(ctx context.Context, authed *oauth.Auth, form *apimodel.UpdateCredentialsRequest) (*apimodel.Account, gtserror.WithCode) - // AccountStatusesGet fetches a number of statuses (in time descending order) from the given account, filtered by visibility for - // the account given in authed. - AccountStatusesGet(ctx context.Context, authed *oauth.Auth, targetAccountID string, limit int, excludeReplies bool, excludeReblogs bool, maxID string, minID string, pinned bool, mediaOnly bool, publicOnly bool) (*apimodel.PageableResponse, gtserror.WithCode) - // AccountWebStatusesGet fetches a number of statuses (in descending order) from the given account. It selects only - // statuses which are suitable for showing on the public web profile of an account. - AccountWebStatusesGet(ctx context.Context, targetAccountID string, maxID string) (*apimodel.PageableResponse, gtserror.WithCode) - // AccountFollowersGet fetches a list of the target account's followers. - AccountFollowersGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) - // AccountFollowingGet fetches a list of the accounts that target account is following. - AccountFollowingGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) ([]apimodel.Account, gtserror.WithCode) - // AccountRelationshipGet returns a relationship model describing the relationship of the targetAccount to the Authed account. - AccountRelationshipGet(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // AccountFollowCreate handles a follow request to an account, either remote or local. - AccountFollowCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AccountFollowRequest) (*apimodel.Relationship, gtserror.WithCode) - // AccountFollowRemove handles the removal of a follow/follow request to an account, either remote or local. - AccountFollowRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // AccountBlockCreate handles the creation of a block from authed account to target account, either remote or local. - AccountBlockCreate(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - // AccountBlockRemove handles the removal of a block from authed account to target account, either remote or local. - AccountBlockRemove(ctx context.Context, authed *oauth.Auth, targetAccountID string) (*apimodel.Relationship, gtserror.WithCode) - - // AdminAccountAction handles the creation/execution of an action on an account. - AdminAccountAction(ctx context.Context, authed *oauth.Auth, form *apimodel.AdminAccountActionRequest) gtserror.WithCode - // AdminEmojiCreate handles the creation of a new instance emoji by an admin, using the given form. - AdminEmojiCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.EmojiCreateRequest) (*apimodel.Emoji, gtserror.WithCode) - // AdminEmojisGet allows admins to view emojis based on various filters. - AdminEmojisGet(ctx context.Context, authed *oauth.Auth, domain string, includeDisabled bool, includeEnabled bool, shortcode string, maxShortcodeDomain string, minShortcodeDomain string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - // AdminEmojiGet returns the admin view of an emoji with the given ID - AdminEmojiGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) - // AdminEmojiDelete deletes one *local* emoji with the given key. Remote emojis will not be deleted this way. - // Only admin users in good standing should be allowed to access this function -- check this before calling it. - AdminEmojiDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminEmoji, gtserror.WithCode) - // AdminEmojiUpdate updates one local or remote emoji with the given key. - // Only admin users in good standing should be allowed to access this function -- check this before calling it. - AdminEmojiUpdate(ctx context.Context, id string, form *apimodel.EmojiUpdateRequest) (*apimodel.AdminEmoji, gtserror.WithCode) - // AdminEmojiCategoriesGet gets a list of all existing emoji categories. - AdminEmojiCategoriesGet(ctx context.Context) ([]*apimodel.EmojiCategory, gtserror.WithCode) - // AdminDomainBlockCreate handles the creation of a new domain block by an admin, using the given form. - AdminDomainBlockCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) (*apimodel.DomainBlock, gtserror.WithCode) - // AdminDomainBlocksImport handles the import of multiple domain blocks by an admin, using the given form. - AdminDomainBlocksImport(ctx context.Context, authed *oauth.Auth, form *apimodel.DomainBlockCreateRequest) ([]*apimodel.DomainBlock, gtserror.WithCode) - // AdminDomainBlocksGet returns a list of currently blocked domains. - AdminDomainBlocksGet(ctx context.Context, authed *oauth.Auth, export bool) ([]*apimodel.DomainBlock, gtserror.WithCode) - // AdminDomainBlockGet returns one domain block, specified by ID. - AdminDomainBlockGet(ctx context.Context, authed *oauth.Auth, id string, export bool) (*apimodel.DomainBlock, gtserror.WithCode) - // AdminDomainBlockDelete deletes one domain block, specified by ID, returning the deleted domain block. - AdminDomainBlockDelete(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.DomainBlock, gtserror.WithCode) - // AdminMediaRemotePrune triggers a prune of remote media according to the given number of mediaRemoteCacheDays - AdminMediaPrune(ctx context.Context, mediaRemoteCacheDays int) gtserror.WithCode - // AdminMediaRefetch triggers a refetch of remote media for the given domain (or all if domain is empty). - AdminMediaRefetch(ctx context.Context, authed *oauth.Auth, domain string) gtserror.WithCode - // AdminReportsGet returns a list of user moderation reports. - AdminReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, accountID string, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - // AdminReportGet returns a single user moderation report, specified by id. - AdminReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.AdminReport, gtserror.WithCode) - // AdminReportResolve marks a single user moderation report as resolved, with the given id. - // actionTakenComment is optional: if set, this will be stored as a comment on the action taken. - AdminReportResolve(ctx context.Context, authed *oauth.Auth, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) - - // AppCreate processes the creation of a new API application - AppCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ApplicationCreateRequest) (*apimodel.Application, gtserror.WithCode) - - // BlocksGet returns a list of accounts blocked by the requesting account. - BlocksGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, limit int) (*apimodel.BlocksResponse, gtserror.WithCode) - - // CustomEmojisGet returns an array of info about the custom emojis on this server - CustomEmojisGet(ctx context.Context) ([]*apimodel.Emoji, gtserror.WithCode) - - // BookmarksGet returns a pageable response of statuses that have been bookmarked - BookmarksGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - - // FileGet handles the fetching of a media attachment file via the fileserver. - FileGet(ctx context.Context, authed *oauth.Auth, form *apimodel.GetContentRequestForm) (*apimodel.Content, gtserror.WithCode) - - // FollowRequestsGet handles the getting of the authed account's incoming follow requests - FollowRequestsGet(ctx context.Context, auth *oauth.Auth) ([]apimodel.Account, gtserror.WithCode) - // FollowRequestAccept handles the acceptance of a follow request from the given account ID. - FollowRequestAccept(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) - // FollowRequestReject handles the rejection of a follow request from the given account ID. - FollowRequestReject(ctx context.Context, auth *oauth.Auth, accountID string) (*apimodel.Relationship, gtserror.WithCode) - - // InstanceGetV1 retrieves instance information for serving at api/v1/instance - InstanceGetV1(ctx context.Context) (*apimodel.InstanceV1, gtserror.WithCode) - // InstanceGetV1 retrieves instance information for serving at api/v2/instance - InstanceGetV2(ctx context.Context) (*apimodel.InstanceV2, gtserror.WithCode) - InstancePeersGet(ctx context.Context, includeSuspended bool, includeOpen bool, flat bool) (interface{}, gtserror.WithCode) - // InstancePatch updates this instance according to the given form. - // - // It should already be ascertained that the requesting account is authenticated and an admin. - InstancePatch(ctx context.Context, form *apimodel.InstanceSettingsUpdateRequest) (*apimodel.InstanceV1, gtserror.WithCode) - - // MediaCreate handles the creation of a media attachment, using the given form. - MediaCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AttachmentRequest) (*apimodel.Attachment, gtserror.WithCode) - // MediaGet handles the GET of a media attachment with the given ID - MediaGet(ctx context.Context, authed *oauth.Auth, attachmentID string) (*apimodel.Attachment, gtserror.WithCode) - // MediaUpdate handles the PUT of a media attachment with the given ID and form - MediaUpdate(ctx context.Context, authed *oauth.Auth, attachmentID string, form *apimodel.AttachmentUpdateRequest) (*apimodel.Attachment, gtserror.WithCode) - - // NotificationsGet - NotificationsGet(ctx context.Context, authed *oauth.Auth, excludeTypes []string, limit int, maxID string, sinceID string) (*apimodel.PageableResponse, gtserror.WithCode) - // NotificationsClear - NotificationsClear(ctx context.Context, authed *oauth.Auth) gtserror.WithCode - - OAuthHandleTokenRequest(r *http.Request) (map[string]interface{}, gtserror.WithCode) - OAuthHandleAuthorizeRequest(w http.ResponseWriter, r *http.Request) gtserror.WithCode - OAuthValidateBearerToken(r *http.Request) (oauth2.TokenInfo, error) - - // SearchGet performs a search with the given params, resolving/dereferencing remotely as desired - SearchGet(ctx context.Context, authed *oauth.Auth, searchQuery *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) - - // StatusCreate processes the given form to create a new status, returning the api model representation of that status if it's OK. - StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) - // StatusDelete processes the delete of a given status, returning the deleted status if the delete goes through. - StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusFave processes the faving of a given status, returning the updated status if the fave goes through. - StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusBoost processes the boost/reblog of a given status, returning the newly-created boost if all is well. - StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusUnboost processes the unboost/unreblog of a given status, returning the status if all is well. - StatusUnboost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. - StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) - // StatusFavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. - StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) - // StatusGet gets the given status, taking account of privacy settings and blocks etc. - StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusUnfave processes the unfaving of a given status, returning the updated status if the fave goes through. - StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusGetContext returns the context (previous and following posts) from the given status ID - StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) - // StatusBookmark process a bookmark for a status - StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // StatusUnbookmark removes a bookmark for a status - StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - - // HomeTimelineGet returns statuses from the home timeline, with the given filters/parameters. - HomeTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) - // PublicTimelineGet returns statuses from the public/local timeline, with the given filters/parameters. - PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) - // FavedTimelineGet returns faved statuses, with the given filters/parameters. - FavedTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - - // AuthorizeStreamingRequest returns a gotosocial account in exchange for an access token, or an error if the given token is not valid. - AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) - // OpenStreamForAccount opens a new stream for the given account, with the given stream type. - OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode) - - // UserChangePassword changes the password for the given user, with the given form. - UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode - // UserConfirmEmail confirms an email address using the given token. - // The user belonging to the confirmed email is also returned. - UserConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) - - // ReportsGet returns reports created by the given user. - ReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - // ReportGet returns one report created by the given user. - ReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.Report, gtserror.WithCode) - // ReportCreate creates a new report using the given account and form. - ReportCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) - - /* - FEDERATION API-FACING PROCESSING FUNCTIONS - These functions are intended to be called when the federating client needs an immediate (ie., synchronous) reply - to an HTTP request. As such, they will only do the bare-minimum of work necessary to give a properly - formed reply. For more intensive (and time-consuming) calls, where you don't require an immediate - response, pass work to the processor using a channel instead. - */ - - // GetFediUser handles the getting of a fedi/activitypub representation of a user/account, performing appropriate authentication - // before returning a JSON serializable interface to the caller. - GetFediUser(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediFollowers handles the getting of a fedi/activitypub representation of a user/account's followers, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFediFollowers(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediFollowing handles the getting of a fedi/activitypub representation of a user/account's following, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFediFollowing(ctx context.Context, requestedUsername string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediStatus handles the getting of a fedi/activitypub representation of a particular status, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFediStatus(ctx context.Context, requestedUsername string, requestedStatusID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediStatus handles the getting of a fedi/activitypub representation of replies to a status, performing appropriate - // authentication before returning a JSON serializable interface to the caller. - GetFediStatusReplies(ctx context.Context, requestedUsername string, requestedStatusID string, page bool, onlyOtherAccounts bool, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediOutbox returns the public outbox of the requested user, with the given parameters. - GetFediOutbox(ctx context.Context, requestedUsername string, page bool, maxID string, minID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetFediEmoji returns the AP representation of an emoji on this instance. - GetFediEmoji(ctx context.Context, requestedEmojiID string, requestURL *url.URL) (interface{}, gtserror.WithCode) - // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. - GetWebfingerAccount(ctx context.Context, requestedUsername string) (*apimodel.WellKnownResponse, gtserror.WithCode) - // GetNodeInfoRel returns a well known response giving the path to node info. - GetNodeInfoRel(ctx context.Context) (*apimodel.WellKnownResponse, gtserror.WithCode) - // GetNodeInfo returns a node info struct in response to a node info request. - GetNodeInfo(ctx context.Context) (*apimodel.Nodeinfo, gtserror.WithCode) - // InboxPost handles POST requests to a user's inbox for new activitypub messages. - // - // InboxPost returns true if the request was handled as an ActivityPub POST to an actor's inbox. - // If false, the request was not an ActivityPub request and may still be handled by the caller in another way, such as serving a web page. - // - // If the error is nil, then the ResponseWriter's headers and response has already been written. If a non-nil error is returned, then no response has been written. - // - // If the Actor was constructed with the Federated Protocol enabled, side effects will occur. - // - // If the Federated Protocol is not enabled, writes the http.StatusMethodNotAllowed status code in the response. No side effects occur. - InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) -} - -// processor just implements the Processor interface -type processor struct { +type Processor struct { clientWorker *concurrency.WorkerPool[messages.FromClientAPI] fedWorker *concurrency.WorkerPool[messages.FromFederator] federator federation.Federator tc typeutils.TypeConverter oauthServer oauth.Server - mediaManager media.Manager + mediaManager mm.Manager storage *storage.Driver statusTimelines timeline.Manager db db.DB @@ -313,14 +57,46 @@ type processor struct { SUB-PROCESSORS */ - accountProcessor account.Processor - adminProcessor admin.Processor - statusProcessor status.Processor - streamingProcessor streaming.Processor - mediaProcessor mediaProcessor.Processor - userProcessor user.Processor - federationProcessor federationProcessor.Processor - reportProcessor report.Processor + account account.Processor + admin admin.Processor + fedi fedi.Processor + media media.Processor + report report.Processor + status status.Processor + stream stream.Processor + user user.Processor +} + +func (p *Processor) Account() *account.Processor { + return &p.account +} + +func (p *Processor) Admin() *admin.Processor { + return &p.admin +} + +func (p *Processor) Fedi() *fedi.Processor { + return &p.fedi +} + +func (p *Processor) Media() *media.Processor { + return &p.media +} + +func (p *Processor) Report() *report.Processor { + return &p.report +} + +func (p *Processor) Status() *status.Processor { + return &p.status +} + +func (p *Processor) Stream() *stream.Processor { + return &p.stream +} + +func (p *Processor) User() *user.Processor { + return &p.user } // NewProcessor returns a new Processor. @@ -328,26 +104,18 @@ func NewProcessor( tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, - mediaManager media.Manager, + mediaManager mm.Manager, storage *storage.Driver, db db.DB, emailSender email.Sender, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], fedWorker *concurrency.WorkerPool[messages.FromFederator], -) Processor { +) *Processor { parseMentionFunc := GetParseMentionFunc(db, federator) - statusProcessor := status.New(db, tc, clientWorker, parseMentionFunc) - streamingProcessor := streaming.New(db, oauthServer) - accountProcessor := account.New(db, tc, mediaManager, oauthServer, clientWorker, federator, parseMentionFunc) - adminProcessor := admin.New(db, tc, mediaManager, federator.TransportController(), storage, clientWorker) - mediaProcessor := mediaProcessor.New(db, tc, mediaManager, federator.TransportController(), storage) - userProcessor := user.New(db, emailSender) - federationProcessor := federationProcessor.New(db, tc, federator) - reportProcessor := report.New(db, tc, clientWorker) filter := visibility.NewFilter(db) - return &processor{ + return &Processor{ clientWorker: clientWorker, fedWorker: fedWorker, @@ -358,21 +126,22 @@ func NewProcessor( storage: storage, statusTimelines: timeline.NewManager(StatusGrabFunction(db), StatusFilterFunction(db, filter), StatusPrepareFunction(db, tc), StatusSkipInsertFunction()), db: db, - filter: visibility.NewFilter(db), - - accountProcessor: accountProcessor, - adminProcessor: adminProcessor, - statusProcessor: statusProcessor, - streamingProcessor: streamingProcessor, - mediaProcessor: mediaProcessor, - userProcessor: userProcessor, - federationProcessor: federationProcessor, - reportProcessor: reportProcessor, + filter: filter, + + // sub processors + account: account.New(db, tc, mediaManager, oauthServer, clientWorker, federator, parseMentionFunc), + admin: admin.New(db, tc, mediaManager, federator.TransportController(), storage, clientWorker), + fedi: fedi.New(db, tc, federator), + media: media.New(db, tc, mediaManager, federator.TransportController(), storage), + report: report.New(db, tc, clientWorker), + status: status.New(db, tc, clientWorker, parseMentionFunc), + stream: stream.New(db, oauthServer), + user: user.New(db, emailSender), } } // Start starts the Processor, reading from its channels and passing messages back and forth. -func (p *processor) Start() error { +func (p *Processor) Start() error { // Setup and start the client API worker pool p.clientWorker.SetProcessor(p.ProcessFromClientAPI) if err := p.clientWorker.Start(); err != nil { @@ -394,7 +163,7 @@ func (p *processor) Start() error { } // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. -func (p *processor) Stop() error { +func (p *Processor) Stop() error { if err := p.clientWorker.Stop(); err != nil { return err } diff --git a/internal/processing/processor_test.go b/internal/processing/processor_test.go @@ -62,7 +62,7 @@ type ProcessingStandardTestSuite struct { testBlocks map[string]*gtsmodel.Block testActivities map[string]testrig.ActivityWithSignature - processor processing.Processor + processor *processing.Processor } func (suite *ProcessingStandardTestSuite) SetupSuite() { diff --git a/internal/processing/report.go b/internal/processing/report.go @@ -1,39 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) ReportsGet(ctx context.Context, authed *oauth.Auth, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - return p.reportProcessor.ReportsGet(ctx, authed.Account, resolved, targetAccountID, maxID, sinceID, minID, limit) -} - -func (p *processor) ReportGet(ctx context.Context, authed *oauth.Auth, id string) (*apimodel.Report, gtserror.WithCode) { - return p.reportProcessor.ReportGet(ctx, authed.Account, id) -} - -func (p *processor) ReportCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) { - return p.reportProcessor.Create(ctx, authed.Account, form) -} diff --git a/internal/processing/report/create.go b/internal/processing/report/create.go @@ -33,7 +33,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/uris" ) -func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) { +// Create creates one user report / flag, using the provided form parameters. +func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) { if account.ID == form.AccountID { err := errors.New("cannot report your own account") return nil, gtserror.NewErrorBadRequest(err, err.Error()) diff --git a/internal/processing/report/get.go b/internal/processing/report/get.go @@ -0,0 +1,112 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 report + +import ( + "context" + "fmt" + "strconv" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// Get returns the user view of a moderation report, with the given id. +func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode) { + report, err := p.db.GetReportByID(ctx, id) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + if report.AccountID != account.ID { + err = fmt.Errorf("report with id %s does not belong to account %s", report.ID, account.ID) + return nil, gtserror.NewErrorNotFound(err) + } + + apiReport, err := p.tc.ReportToAPIReport(ctx, report) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) + } + + return apiReport, nil +} + +// GetMultiple returns multiple reports created by the given account, filtered according to the provided parameters. +func (p *Processor) GetMultiple( + ctx context.Context, + account *gtsmodel.Account, + resolved *bool, + targetAccountID string, + maxID string, + sinceID string, + minID string, + limit int, +) (*apimodel.PageableResponse, gtserror.WithCode) { + reports, err := p.db.GetReports(ctx, resolved, account.ID, targetAccountID, maxID, sinceID, minID, limit) + if err != nil { + if err == db.ErrNoEntries { + return util.EmptyPageableResponse(), nil + } + return nil, gtserror.NewErrorInternalError(err) + } + + count := len(reports) + items := make([]interface{}, 0, count) + nextMaxIDValue := "" + prevMinIDValue := "" + for i, r := range reports { + item, err := p.tc.ReportToAPIReport(ctx, r) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) + } + + if i == count-1 { + nextMaxIDValue = item.ID + } + + if i == 0 { + prevMinIDValue = item.ID + } + + items = append(items, item) + } + + extraQueryParams := []string{} + if resolved != nil { + extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) + } + if targetAccountID != "" { + extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) + } + + return util.PackagePageableResponse(util.PageableResponseParams{ + Items: items, + Path: "/api/v1/reports", + NextMaxIDValue: nextMaxIDValue, + PrevMinIDValue: prevMinIDValue, + Limit: limit, + ExtraQueryParams: extraQueryParams, + }) +} diff --git a/internal/processing/report/getreport.go b/internal/processing/report/getreport.go @@ -1,51 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 report - -import ( - "context" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode) { - report, err := p.db.GetReportByID(ctx, id) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - if report.AccountID != account.ID { - err = fmt.Errorf("report with id %s does not belong to account %s", report.ID, account.ID) - return nil, gtserror.NewErrorNotFound(err) - } - - apiReport, err := p.tc.ReportToAPIReport(ctx, report) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) - } - - return apiReport, nil -} diff --git a/internal/processing/report/getreports.go b/internal/processing/report/getreports.go @@ -1,79 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 report - -import ( - "context" - "fmt" - "strconv" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -func (p *processor) ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { - reports, err := p.db.GetReports(ctx, resolved, account.ID, targetAccountID, maxID, sinceID, minID, limit) - if err != nil { - if err == db.ErrNoEntries { - return util.EmptyPageableResponse(), nil - } - return nil, gtserror.NewErrorInternalError(err) - } - - count := len(reports) - items := make([]interface{}, 0, count) - nextMaxIDValue := "" - prevMinIDValue := "" - for i, r := range reports { - item, err := p.tc.ReportToAPIReport(ctx, r) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting report to api: %s", err)) - } - - if i == count-1 { - nextMaxIDValue = item.ID - } - - if i == 0 { - prevMinIDValue = item.ID - } - - items = append(items, item) - } - - extraQueryParams := []string{} - if resolved != nil { - extraQueryParams = append(extraQueryParams, "resolved="+strconv.FormatBool(*resolved)) - } - if targetAccountID != "" { - extraQueryParams = append(extraQueryParams, "target_account_id="+targetAccountID) - } - - return util.PackagePageableResponse(util.PageableResponseParams{ - Items: items, - Path: "/api/v1/reports", - NextMaxIDValue: nextMaxIDValue, - PrevMinIDValue: prevMinIDValue, - Limit: limit, - ExtraQueryParams: extraQueryParams, - }) -} diff --git a/internal/processing/report/report.go b/internal/processing/report/report.go @@ -19,31 +19,20 @@ package report import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) -type Processor interface { - ReportsGet(ctx context.Context, account *gtsmodel.Account, resolved *bool, targetAccountID string, maxID string, sinceID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) - ReportGet(ctx context.Context, account *gtsmodel.Account, id string) (*apimodel.Report, gtserror.WithCode) - Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.ReportCreateRequest) (*apimodel.Report, gtserror.WithCode) -} - -type processor struct { +type Processor struct { db db.DB tc typeutils.TypeConverter clientWorker *concurrency.WorkerPool[messages.FromClientAPI] } func New(db db.DB, tc typeutils.TypeConverter, clientWorker *concurrency.WorkerPool[messages.FromClientAPI]) Processor { - return &processor{ + return Processor{ tc: tc, db: db, clientWorker: clientWorker, diff --git a/internal/processing/search.go b/internal/processing/search.go @@ -49,7 +49,7 @@ import ( // The only exception to this is when we get a malformed query, in // which case we return a bad request error so the user knows they // did something funky. -func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) { +func (p *Processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *apimodel.SearchQuery) (*apimodel.SearchResult, gtserror.WithCode) { // tidy up the query and make sure it wasn't just spaces query := strings.TrimSpace(search.Query) if query == "" { @@ -223,7 +223,7 @@ func (p *processor) SearchGet(ctx context.Context, authed *oauth.Auth, search *a return searchResult, nil } -func (p *processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL) (*gtsmodel.Status, error) { +func (p *Processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL) (*gtsmodel.Status, error) { status, statusable, err := p.federator.GetStatus(transport.WithFastfail(ctx), authed.Account.Username, uri, true, true) if err != nil { return nil, err @@ -237,7 +237,7 @@ func (p *processor) searchStatusByURI(ctx context.Context, authed *oauth.Auth, u return status, nil } -func (p *processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) { +func (p *Processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, uri *url.URL, resolve bool) (*gtsmodel.Account, error) { if !resolve { var ( account *gtsmodel.Account @@ -272,7 +272,7 @@ func (p *processor) searchAccountByURI(ctx context.Context, authed *oauth.Auth, ) } -func (p *processor) searchAccountByUsernameDomain(ctx context.Context, authed *oauth.Auth, username string, domain string, resolve bool) (*gtsmodel.Account, error) { +func (p *Processor) searchAccountByUsernameDomain(ctx context.Context, authed *oauth.Auth, username string, domain string, resolve bool) (*gtsmodel.Account, error) { if !resolve { if domain == config.GetHost() || domain == config.GetAccountDomain() { // We do local lookups using an empty domain, diff --git a/internal/processing/status.go b/internal/processing/status.go @@ -1,75 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) StatusCreate(ctx context.Context, authed *oauth.Auth, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Create(ctx, authed.Account, authed.Application, form) -} - -func (p *processor) StatusDelete(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Delete(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusFave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Fave(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusBoost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Boost(ctx, authed.Account, authed.Application, targetStatusID) -} - -func (p *processor) StatusUnboost(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Unboost(ctx, authed.Account, authed.Application, targetStatusID) -} - -func (p *processor) StatusBoostedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - return p.statusProcessor.BoostedBy(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusFavedBy(ctx context.Context, authed *oauth.Auth, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - return p.statusProcessor.FavedBy(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusGet(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Get(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusUnfave(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Unfave(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusGetContext(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { - return p.statusProcessor.Context(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusBookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Bookmark(ctx, authed.Account, targetStatusID) -} - -func (p *processor) StatusUnbookmark(ctx context.Context, authed *oauth.Auth, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - return p.statusProcessor.Unbookmark(ctx, authed.Account, targetStatusID) -} diff --git a/internal/processing/status/bookmark.go b/internal/processing/status/bookmark.go @@ -30,7 +30,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/id" ) -func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists). +func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) @@ -79,3 +80,43 @@ func (p *processor) Bookmark(ctx context.Context, requestingAccount *gtsmodel.Ac return apiStatus, nil } + +// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist). +func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } + + // first check if the status is actually bookmarked + toUnbookmark := false + gtsBookmark := >smodel.StatusBookmark{} + if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil { + // we have a bookmark for this status + toUnbookmark = true + } + + if toUnbookmark { + if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) + } + } + + // return the api representation of the target status + apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) + } + + return apiStatus, nil +} diff --git a/internal/processing/status/bookmark_test.go b/internal/processing/status/bookmark_test.go @@ -36,13 +36,33 @@ func (suite *StatusBookmarkTestSuite) TestBookmark() { bookmarkingAccount1 := suite.testAccounts["local_account_1"] targetStatus1 := suite.testStatuses["admin_account_status_1"] - bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID) + bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID) suite.NoError(err) suite.NotNil(bookmark1) suite.True(bookmark1.Bookmarked) suite.Equal(targetStatus1.ID, bookmark1.ID) } +func (suite *StatusBookmarkTestSuite) TestUnbookmark() { + ctx := context.Background() + + // bookmark a status + bookmarkingAccount1 := suite.testAccounts["local_account_1"] + targetStatus1 := suite.testStatuses["admin_account_status_1"] + + bookmark1, err := suite.status.BookmarkCreate(ctx, bookmarkingAccount1, targetStatus1.ID) + suite.NoError(err) + suite.NotNil(bookmark1) + suite.True(bookmark1.Bookmarked) + suite.Equal(targetStatus1.ID, bookmark1.ID) + + bookmark2, err := suite.status.BookmarkRemove(ctx, bookmarkingAccount1, targetStatus1.ID) + suite.NoError(err) + suite.NotNil(bookmark2) + suite.False(bookmark2.Bookmarked) + suite.Equal(targetStatus1.ID, bookmark1.ID) +} + func TestStatusBookmarkTestSuite(t *testing.T) { suite.Run(t, new(StatusBookmarkTestSuite)) } diff --git a/internal/processing/status/boost.go b/internal/processing/status/boost.go @@ -25,12 +25,14 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" ) -func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +// BoostCreate processes the boost/reblog of a given status, returning the newly-created boost if all is well. +func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) @@ -93,3 +95,153 @@ func (p *processor) Boost(ctx context.Context, requestingAccount *gtsmodel.Accou return apiStatus, nil } + +// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well. +func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } + + // check if we actually have a boost for this status + var toUnboost bool + + gtsBoost := >smodel.Status{} + where := []db.Where{ + { + Key: "boost_of_id", + Value: targetStatusID, + }, + { + Key: "account_id", + Value: requestingAccount.ID, + }, + } + err = p.db.GetWhere(ctx, where, gtsBoost) + if err == nil { + // we have a boost + toUnboost = true + } + + if err != nil { + // something went wrong in the db finding the boost + if err != db.ErrNoEntries { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err)) + } + // we just don't have a boost + toUnboost = false + } + + if toUnboost { + // pin some stuff onto the boost while we have it out of the db + gtsBoost.Account = requestingAccount + gtsBoost.BoostOf = targetStatus + gtsBoost.BoostOfAccount = targetStatus.Account + gtsBoost.BoostOf.Account = targetStatus.Account + + // send it back to the processor for async processing + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityAnnounce, + APActivityType: ap.ActivityUndo, + GTSModel: gtsBoost, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, + }) + } + + apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) + } + + return apiStatus, nil +} + +// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. +func (p *Processor) StatusBoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err) + if !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.NewErrorInternalError(wrapped) + } + return nil, gtserror.NewErrorNotFound(wrapped) + } + + if boostOfID := targetStatus.BoostOfID; boostOfID != "" { + // the target status is a boost wrapper, redirect this request to the status it boosts + boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID) + if err != nil { + wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err) + if !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.NewErrorInternalError(wrapped) + } + return nil, gtserror.NewErrorNotFound(wrapped) + } + targetStatus = boostedStatus + } + + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err) + return nil, gtserror.NewErrorNotFound(err) + } + if !visible { + err = errors.New("BoostedBy: status is not visible") + return nil, gtserror.NewErrorNotFound(err) + } + + statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus) + if err != nil { + err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err) + return nil, gtserror.NewErrorNotFound(err) + } + + // filter account IDs so the user doesn't see accounts they blocked or which blocked them + accountIDs := make([]string, 0, len(statusReblogs)) + for _, s := range statusReblogs { + blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true) + if err != nil { + err = fmt.Errorf("BoostedBy: error checking blocks: %s", err) + return nil, gtserror.NewErrorNotFound(err) + } + if !blocked { + accountIDs = append(accountIDs, s.AccountID) + } + } + + // TODO: filter other things here? suspended? muted? silenced? + + // fetch accounts + create their API representations + apiAccounts := make([]*apimodel.Account, 0, len(accountIDs)) + for _, accountID := range accountIDs { + account, err := p.db.GetAccountByID(ctx, accountID) + if err != nil { + wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err) + if !errors.Is(err, db.ErrNoEntries) { + return nil, gtserror.NewErrorInternalError(wrapped) + } + return nil, gtserror.NewErrorNotFound(wrapped) + } + + apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account) + if err != nil { + err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err) + return nil, gtserror.NewErrorInternalError(err) + } + apiAccounts = append(apiAccounts, apiAccount) + } + + return apiAccounts, nil +} diff --git a/internal/processing/status/boost_test.go b/internal/processing/status/boost_test.go @@ -37,7 +37,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() { application1 := suite.testApplications["application_1"] targetStatus1 := suite.testStatuses["admin_account_status_1"] - boost1, err := suite.status.Boost(ctx, boostingAccount1, application1, targetStatus1.ID) + boost1, err := suite.status.BoostCreate(ctx, boostingAccount1, application1, targetStatus1.ID) suite.NoError(err) suite.NotNil(boost1) suite.Equal(targetStatus1.ID, boost1.Reblog.ID) @@ -47,7 +47,7 @@ func (suite *StatusBoostTestSuite) TestBoostOfBoost() { application2 := suite.testApplications["application_2"] targetStatus2ID := boost1.ID - boost2, err := suite.status.Boost(ctx, boostingAccount2, application2, targetStatus2ID) + boost2, err := suite.status.BoostCreate(ctx, boostingAccount2, application2, targetStatus2ID) suite.NoError(err) suite.NotNil(boost2) // the boosted status should not be the boost, diff --git a/internal/processing/status/boostedby.go b/internal/processing/status/boostedby.go @@ -1,107 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) BoostedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", targetStatusID, err) - if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(wrapped) - } - return nil, gtserror.NewErrorNotFound(wrapped) - } - - if boostOfID := targetStatus.BoostOfID; boostOfID != "" { - // the target status is a boost wrapper, redirect this request to the status it boosts - boostedStatus, err := p.db.GetStatusByID(ctx, boostOfID) - if err != nil { - wrapped := fmt.Errorf("BoostedBy: error fetching status %s: %s", boostOfID, err) - if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(wrapped) - } - return nil, gtserror.NewErrorNotFound(wrapped) - } - targetStatus = boostedStatus - } - - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - err = fmt.Errorf("BoostedBy: error seeing if status %s is visible: %s", targetStatus.ID, err) - return nil, gtserror.NewErrorNotFound(err) - } - if !visible { - err = errors.New("BoostedBy: status is not visible") - return nil, gtserror.NewErrorNotFound(err) - } - - statusReblogs, err := p.db.GetStatusReblogs(ctx, targetStatus) - if err != nil { - err = fmt.Errorf("BoostedBy: error seeing who boosted status: %s", err) - return nil, gtserror.NewErrorNotFound(err) - } - - // filter account IDs so the user doesn't see accounts they blocked or which blocked them - accountIDs := make([]string, 0, len(statusReblogs)) - for _, s := range statusReblogs { - blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, s.AccountID, true) - if err != nil { - err = fmt.Errorf("BoostedBy: error checking blocks: %s", err) - return nil, gtserror.NewErrorNotFound(err) - } - if !blocked { - accountIDs = append(accountIDs, s.AccountID) - } - } - - // TODO: filter other things here? suspended? muted? silenced? - - // fetch accounts + create their API representations - apiAccounts := make([]*apimodel.Account, 0, len(accountIDs)) - for _, accountID := range accountIDs { - account, err := p.db.GetAccountByID(ctx, accountID) - if err != nil { - wrapped := fmt.Errorf("BoostedBy: error fetching account %s: %s", accountID, err) - if !errors.Is(err, db.ErrNoEntries) { - return nil, gtserror.NewErrorInternalError(wrapped) - } - return nil, gtserror.NewErrorNotFound(wrapped) - } - - apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, account) - if err != nil { - err = fmt.Errorf("BoostedBy: error converting account to api model: %s", err) - return nil, gtserror.NewErrorInternalError(err) - } - apiAccounts = append(apiAccounts, apiAccount) - } - - return apiAccounts, nil -} diff --git a/internal/processing/status/context.go b/internal/processing/status/context.go @@ -1,87 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - "sort" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) Context(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - if targetStatus.Account == nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) - } - - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) - } - - context := &apimodel.Context{ - Ancestors: []apimodel.Status{}, - Descendants: []apimodel.Status{}, - } - - parents, err := p.db.GetStatusParents(ctx, targetStatus, false) - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - for _, status := range parents { - if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v { - apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount) - if err == nil { - context.Ancestors = append(context.Ancestors, *apiStatus) - } - } - } - - sort.Slice(context.Ancestors, func(i int, j int) bool { - return context.Ancestors[i].ID < context.Ancestors[j].ID - }) - - children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "") - if err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - for _, status := range children { - if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v { - apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount) - if err == nil { - context.Descendants = append(context.Descendants, *apiStatus) - } - } - } - - return context, nil -} diff --git a/internal/processing/status/create.go b/internal/processing/status/create.go @@ -20,20 +20,25 @@ package status import ( "context" + "errors" "fmt" "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/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" + "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/uris" ) -func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) { +// Create processes the given form to create a new status, returning the api model representation of that status if it's OK. +func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) { accountURIs := uris.GenerateURIsForAccount(account.Username) thisStatusID := id.NewULID() local := true @@ -56,23 +61,23 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli Text: form.Status, } - if errWithCode := p.ProcessReplyToID(ctx, form, account.ID, newStatus); errWithCode != nil { + if errWithCode := processReplyToID(ctx, p.db, form, account.ID, newStatus); errWithCode != nil { return nil, errWithCode } - if errWithCode := p.ProcessMediaIDs(ctx, form, account.ID, newStatus); errWithCode != nil { + if errWithCode := processMediaIDs(ctx, p.db, form, account.ID, newStatus); errWithCode != nil { return nil, errWithCode } - if err := p.ProcessVisibility(ctx, form, account.Privacy, newStatus); err != nil { + if err := processVisibility(ctx, form, account.Privacy, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - if err := p.ProcessLanguage(ctx, form, account.Language, newStatus); err != nil { + if err := processLanguage(ctx, form, account.Language, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } - if err := p.ProcessContent(ctx, form, account.ID, newStatus); err != nil { + if err := processContent(ctx, p.db, p.formatter, p.parseMention, form, account.ID, newStatus); err != nil { return nil, gtserror.NewErrorInternalError(err) } @@ -97,3 +102,249 @@ func (p *processor) Create(ctx context.Context, account *gtsmodel.Account, appli return apiStatus, nil } + +func processReplyToID(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { + if form.InReplyToID == "" { + return nil + } + + // 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 := >smodel.Status{} + repliedAccount := >smodel.Account{} + + if err := dbService.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err) + return gtserror.NewErrorInternalError(err) + } + if !*repliedStatus.Replyable { + err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) + return gtserror.NewErrorForbidden(err, err.Error()) + } + + if err := dbService.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err) + return gtserror.NewErrorInternalError(err) + } + + if blocked, err := dbService.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil { + err := fmt.Errorf("db error checking block: %s", err) + return gtserror.NewErrorInternalError(err) + } else if blocked { + err := fmt.Errorf("status with id %s not replyable", form.InReplyToID) + return gtserror.NewErrorNotFound(err) + } + + status.InReplyToID = repliedStatus.ID + status.InReplyToURI = repliedStatus.URI + status.InReplyToAccountID = repliedAccount.ID + + return nil +} + +func processMediaIDs(ctx context.Context, dbService db.DB, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { + if form.MediaIDs == nil { + return nil + } + + attachments := []*gtsmodel.MediaAttachment{} + attachmentIDs := []string{} + for _, mediaID := range form.MediaIDs { + attachment, err := dbService.GetAttachmentByID(ctx, mediaID) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID) + return gtserror.NewErrorInternalError(err) + } + + if attachment.AccountID != thisAccountID { + err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + if attachment.StatusID != "" || attachment.ScheduledStatusID != "" { + err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + minDescriptionChars := config.GetMediaDescriptionMinChars() + if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars { + err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID) + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + attachments = append(attachments, attachment) + attachmentIDs = append(attachmentIDs, attachment.ID) + } + + status.Attachments = attachments + status.AttachmentIDs = attachmentIDs + return nil +} + +func processVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { + // by default all flags are set to true + federated := true + boostable := true + replyable := true + likeable := true + + // If visibility isn't set on the form, then just take the account default. + // If that's also not set, take the default for the whole instance. + var vis gtsmodel.Visibility + switch { + case form.Visibility != "": + vis = typeutils.APIVisToVis(form.Visibility) + case accountDefaultVis != "": + vis = accountDefaultVis + default: + vis = gtsmodel.VisibilityDefault + } + + switch vis { + case gtsmodel.VisibilityPublic: + // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out + break + case gtsmodel.VisibilityUnlocked: + // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them + if form.Federated != nil { + federated = *form.Federated + } + + if form.Boostable != nil { + boostable = *form.Boostable + } + + if form.Replyable != nil { + replyable = *form.Replyable + } + + if form.Likeable != nil { + likeable = *form.Likeable + } + + case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: + // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them + boostable = false + + if form.Federated != nil { + federated = *form.Federated + } + + if form.Replyable != nil { + replyable = *form.Replyable + } + + if form.Likeable != nil { + likeable = *form.Likeable + } + + case gtsmodel.VisibilityDirect: + // direct is pretty easy: there's only one possible setting so return it + federated = true + boostable = false + replyable = true + likeable = true + } + + status.Visibility = vis + status.Federated = &federated + status.Boostable = &boostable + status.Replyable = &replyable + status.Likeable = &likeable + return nil +} + +func processLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error { + if form.Language != "" { + status.Language = form.Language + } else { + status.Language = accountDefaultLanguage + } + if status.Language == "" { + return errors.New("no language given either in status create form or account default") + } + return nil +} + +func processContent(ctx context.Context, dbService db.DB, formatter text.Formatter, parseMention gtsmodel.ParseMentionFunc, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { + // if there's nothing in the status at all we can just return early + if form.Status == "" { + status.Content = "" + return nil + } + + // if format wasn't specified we should try to figure out what format this user prefers + if form.Format == "" { + acct, err := dbService.GetAccountByID(ctx, accountID) + if err != nil { + return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err) + } + + switch acct.StatusFormat { + case "plain": + form.Format = apimodel.StatusFormatPlain + case "markdown": + form.Format = apimodel.StatusFormatMarkdown + default: + form.Format = apimodel.StatusFormatDefault + } + } + + // parse content out of the status depending on what format has been submitted + var f text.FormatFunc + switch form.Format { + case apimodel.StatusFormatPlain: + f = formatter.FromPlain + case apimodel.StatusFormatMarkdown: + f = formatter.FromMarkdown + default: + return fmt.Errorf("format %s not recognised as a valid status format", form.Format) + } + formatted := f(ctx, parseMention, accountID, status.ID, form.Status) + + // add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently + // add just their ids to the status for putting in the db + status.Mentions = formatted.Mentions + status.MentionIDs = make([]string, 0, len(formatted.Mentions)) + for _, gtsmention := range formatted.Mentions { + status.MentionIDs = append(status.MentionIDs, gtsmention.ID) + } + + status.Tags = formatted.Tags + status.TagIDs = make([]string, 0, len(formatted.Tags)) + for _, gtstag := range formatted.Tags { + status.TagIDs = append(status.TagIDs, gtstag.ID) + } + + status.Emojis = formatted.Emojis + status.EmojiIDs = make([]string, 0, len(formatted.Emojis)) + for _, gtsemoji := range formatted.Emojis { + status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID) + } + + spoilerformatted := formatter.FromPlainEmojiOnly(ctx, parseMention, accountID, status.ID, form.SpoilerText) + for _, gtsemoji := range spoilerformatted.Emojis { + status.Emojis = append(status.Emojis, gtsemoji) + status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID) + } + + status.Content = formatted.HTML + return nil +} diff --git a/internal/processing/status/delete.go b/internal/processing/status/delete.go @@ -30,7 +30,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/messages" ) -func (p *processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +// Delete processes the delete of a given status, returning the deleted status if the delete goes through. +func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) diff --git a/internal/processing/status/fave.go b/internal/processing/status/fave.go @@ -33,7 +33,8 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/uris" ) -func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +// FaveCreate processes the faving of a given status, returning the updated status if the fave goes through. +func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) @@ -98,3 +99,111 @@ func (p *processor) Fave(ctx context.Context, requestingAccount *gtsmodel.Accoun return apiStatus, nil } + +// FaveRemove processes the unfaving of a given status, returning the updated status if the fave goes through. +func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } + + // check if we actually have a fave for this status + var toUnfave bool + + gtsFave := >smodel.StatusFave{} + err = p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave) + if err == nil { + // we have a fave + toUnfave = true + } + if err != nil { + // something went wrong in the db finding the fave + if err != db.ErrNoEntries { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err)) + } + // we just don't have a fave + toUnfave = false + } + + if toUnfave { + // we had a fave, so take some action to get rid of it + if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) + } + + // send it back to the processor for async processing + p.clientWorker.Queue(messages.FromClientAPI{ + APObjectType: ap.ActivityLike, + APActivityType: ap.ActivityUndo, + GTSModel: gtsFave, + OriginAccount: requestingAccount, + TargetAccount: targetStatus.Account, + }) + } + + apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) + } + + return apiStatus, nil +} + +// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. +func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } + + statusFaves, err := p.db.GetStatusFaves(ctx, targetStatus) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err)) + } + + // filter the list so the user doesn't see accounts they blocked or which blocked them + filteredAccounts := []*gtsmodel.Account{} + for _, fave := range statusFaves { + blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err)) + } + if !blocked { + filteredAccounts = append(filteredAccounts, fave.Account) + } + } + + // now we can return the api representation of those accounts + apiAccounts := []*apimodel.Account{} + for _, acc := range filteredAccounts { + apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc) + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) + } + apiAccounts = append(apiAccounts, apiAccount) + } + + return apiAccounts, nil +} diff --git a/internal/processing/status/favedby.go b/internal/processing/status/favedby.go @@ -1,76 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - if targetStatus.Account == nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) - } - - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) - } - - statusFaves, err := p.db.GetStatusFaves(ctx, targetStatus) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing who faved status: %s", err)) - } - - // filter the list so the user doesn't see accounts they blocked or which blocked them - filteredAccounts := []*gtsmodel.Account{} - for _, fave := range statusFaves { - blocked, err := p.db.IsBlocked(ctx, requestingAccount.ID, fave.AccountID, true) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error checking blocks: %s", err)) - } - if !blocked { - filteredAccounts = append(filteredAccounts, fave.Account) - } - } - - // now we can return the api representation of those accounts - apiAccounts := []*apimodel.Account{} - for _, acc := range filteredAccounts { - apiAccount, err := p.tc.AccountToAPIAccountPublic(ctx, acc) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) - } - apiAccounts = append(apiAccounts, apiAccount) - } - - return apiAccounts, nil -} diff --git a/internal/processing/status/get.go b/internal/processing/status/get.go @@ -22,13 +22,15 @@ import ( "context" "errors" "fmt" + "sort" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { +// Get gets the given status, taking account of privacy settings and blocks etc. +func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) if err != nil { return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) @@ -52,3 +54,61 @@ func (p *processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account return apiStatus, nil } + +// ContextGet returns the context (previous and following posts) from the given status ID. +func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { + targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) + } + if targetStatus.Account == nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) + } + + visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) + if err != nil { + return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) + } + if !visible { + return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) + } + + context := &apimodel.Context{ + Ancestors: []apimodel.Status{}, + Descendants: []apimodel.Status{}, + } + + parents, err := p.db.GetStatusParents(ctx, targetStatus, false) + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + for _, status := range parents { + if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v { + apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount) + if err == nil { + context.Ancestors = append(context.Ancestors, *apiStatus) + } + } + } + + sort.Slice(context.Ancestors, func(i int, j int) bool { + return context.Ancestors[i].ID < context.Ancestors[j].ID + }) + + children, err := p.db.GetStatusChildren(ctx, targetStatus, false, "") + if err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + for _, status := range children { + if v, err := p.filter.StatusVisible(ctx, status, requestingAccount); err == nil && v { + apiStatus, err := p.tc.StatusToAPIStatus(ctx, status, requestingAccount) + if err == nil { + context.Descendants = append(context.Descendants, *apiStatus) + } + } + } + + return context, nil +} diff --git a/internal/processing/status/status.go b/internal/processing/status/status.go @@ -19,12 +19,8 @@ package status import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/concurrency" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/text" @@ -32,45 +28,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/visibility" ) -// Processor wraps a bunch of functions for processing statuses. -type Processor interface { - // Create processes the given form to create a new status, returning the api model representation of that status if it's OK. - Create(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, form *apimodel.AdvancedStatusCreateForm) (*apimodel.Status, gtserror.WithCode) - // Delete processes the delete of a given status, returning the deleted status if the delete goes through. - Delete(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Fave processes the faving of a given status, returning the updated status if the fave goes through. - Fave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Boost processes the boost/reblog of a given status, returning the newly-created boost if all is well. - Boost(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Unboost processes the unboost/unreblog of a given status, returning the status if all is well. - Unboost(ctx context.Context, account *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // BoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings. - BoostedBy(ctx context.Context, account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) - // FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings. - FavedBy(ctx context.Context, account *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) - // Get gets the given status, taking account of privacy settings and blocks etc. - Get(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Unfave processes the unfaving of a given status, returning the updated status if the fave goes through. - Unfave(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Context returns the context (previous and following posts) from the given status ID - Context(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) - // Bookmarks a status - Bookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - // Removes a bookmark for a status - Unbookmark(ctx context.Context, account *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) - - /* - PROCESSING UTILS - */ - - ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error - ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode - ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode - ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error - ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error -} - -type processor struct { +type Processor struct { tc typeutils.TypeConverter db db.DB filter visibility.Filter @@ -81,7 +39,7 @@ type processor struct { // New returns a new status processor. func New(db db.DB, tc typeutils.TypeConverter, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], parseMention gtsmodel.ParseMentionFunc) Processor { - return &processor{ + return Processor{ tc: tc, db: db, filter: visibility.NewFilter(db), diff --git a/internal/processing/status/unbookmark.go b/internal/processing/status/unbookmark.go @@ -1,69 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) Unbookmark(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - if targetStatus.Account == nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) - } - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) - } - - // first check if the status is already bookmarked - toUnbookmark := false - gtsBookmark := >smodel.StatusBookmark{} - if err := p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err == nil { - // we already have a bookmark for this status - toUnbookmark = true - } - - if toUnbookmark { - if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsBookmark); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) - } - } - - // return the apidon representation of the target status - apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) - } - - return apiStatus, nil -} diff --git a/internal/processing/status/unbookmark_test.go b/internal/processing/status/unbookmark_test.go @@ -1,54 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" -) - -type StatusUnbookmarkTestSuite struct { - StatusStandardTestSuite -} - -func (suite *StatusUnbookmarkTestSuite) TestUnbookmark() { - ctx := context.Background() - - // bookmark a status - bookmarkingAccount1 := suite.testAccounts["local_account_1"] - targetStatus1 := suite.testStatuses["admin_account_status_1"] - - bookmark1, err := suite.status.Bookmark(ctx, bookmarkingAccount1, targetStatus1.ID) - suite.NoError(err) - suite.NotNil(bookmark1) - suite.True(bookmark1.Bookmarked) - suite.Equal(targetStatus1.ID, bookmark1.ID) - - bookmark2, err := suite.status.Unbookmark(ctx, bookmarkingAccount1, targetStatus1.ID) - suite.NoError(err) - suite.NotNil(bookmark2) - suite.False(bookmark2.Bookmarked) - suite.Equal(targetStatus1.ID, bookmark1.ID) -} - -func TestStatusUnbookmarkTestSuite(t *testing.T) { - suite.Run(t, new(StatusUnbookmarkTestSuite)) -} diff --git a/internal/processing/status/unboost.go b/internal/processing/status/unboost.go @@ -1,103 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/messages" -) - -func (p *processor) Unboost(ctx context.Context, requestingAccount *gtsmodel.Account, application *gtsmodel.Application, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - if targetStatus.Account == nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) - } - - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) - } - - // check if we actually have a boost for this status - var toUnboost bool - - gtsBoost := >smodel.Status{} - where := []db.Where{ - { - Key: "boost_of_id", - Value: targetStatusID, - }, - { - Key: "account_id", - Value: requestingAccount.ID, - }, - } - err = p.db.GetWhere(ctx, where, gtsBoost) - if err == nil { - // we have a boost - toUnboost = true - } - - if err != nil { - // something went wrong in the db finding the boost - if err != db.ErrNoEntries { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing boost from database: %s", err)) - } - // we just don't have a boost - toUnboost = false - } - - if toUnboost { - // pin some stuff onto the boost while we have it out of the db - gtsBoost.Account = requestingAccount - gtsBoost.BoostOf = targetStatus - gtsBoost.BoostOfAccount = targetStatus.Account - gtsBoost.BoostOf.Account = targetStatus.Account - - // send it back to the processor for async processing - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityAnnounce, - APActivityType: ap.ActivityUndo, - GTSModel: gtsBoost, - OriginAccount: requestingAccount, - TargetAccount: targetStatus.Account, - }) - } - - apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) - } - - return apiStatus, nil -} diff --git a/internal/processing/status/unfave.go b/internal/processing/status/unfave.go @@ -1,91 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/ap" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/messages" -) - -func (p *processor) Unfave(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) { - targetStatus, err := p.db.GetStatusByID(ctx, targetStatusID) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error fetching status %s: %s", targetStatusID, err)) - } - if targetStatus.Account == nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("no status owner for status %s", targetStatusID)) - } - - visible, err := p.filter.StatusVisible(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorNotFound(fmt.Errorf("error seeing if status %s is visible: %s", targetStatus.ID, err)) - } - if !visible { - return nil, gtserror.NewErrorNotFound(errors.New("status is not visible")) - } - - // check if we actually have a fave for this status - var toUnfave bool - - gtsFave := >smodel.StatusFave{} - err = p.db.GetWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave) - if err == nil { - // we have a fave - toUnfave = true - } - if err != nil { - // something went wrong in the db finding the fave - if err != db.ErrNoEntries { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error fetching existing fave from database: %s", err)) - } - // we just don't have a fave - toUnfave = false - } - - if toUnfave { - // we had a fave, so take some action to get rid of it - if err := p.db.DeleteWhere(ctx, []db.Where{{Key: "status_id", Value: targetStatus.ID}, {Key: "account_id", Value: requestingAccount.ID}}, gtsFave); err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error unfaveing status: %s", err)) - } - - // send it back to the processor for async processing - p.clientWorker.Queue(messages.FromClientAPI{ - APObjectType: ap.ActivityLike, - APActivityType: ap.ActivityUndo, - GTSModel: gtsFave, - OriginAccount: requestingAccount, - TargetAccount: targetStatus.Account, - }) - } - - apiStatus, err := p.tc.StatusToAPIStatus(ctx, targetStatus, requestingAccount) - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting status %s to frontend representation: %s", targetStatus.ID, err)) - } - - return apiStatus, nil -} diff --git a/internal/processing/status/util.go b/internal/processing/status/util.go @@ -1,278 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status - -import ( - "context" - "errors" - "fmt" - - 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/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/text" -) - -func (p *processor) ProcessVisibility(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultVis gtsmodel.Visibility, status *gtsmodel.Status) error { - // by default all flags are set to true - federated := true - boostable := true - replyable := true - likeable := true - - // If visibility isn't set on the form, then just take the account default. - // If that's also not set, take the default for the whole instance. - var vis gtsmodel.Visibility - switch { - case form.Visibility != "": - vis = p.tc.APIVisToVis(form.Visibility) - case accountDefaultVis != "": - vis = accountDefaultVis - default: - vis = gtsmodel.VisibilityDefault - } - - switch vis { - case gtsmodel.VisibilityPublic: - // for public, there's no need to change any of the advanced flags from true regardless of what the user filled out - break - case gtsmodel.VisibilityUnlocked: - // for unlocked the user can set any combination of flags they like so look at them all to see if they're set and then apply them - if form.Federated != nil { - federated = *form.Federated - } - - if form.Boostable != nil { - boostable = *form.Boostable - } - - if form.Replyable != nil { - replyable = *form.Replyable - } - - if form.Likeable != nil { - likeable = *form.Likeable - } - - case gtsmodel.VisibilityFollowersOnly, gtsmodel.VisibilityMutualsOnly: - // for followers or mutuals only, boostable will *always* be false, but the other fields can be set so check and apply them - boostable = false - - if form.Federated != nil { - federated = *form.Federated - } - - if form.Replyable != nil { - replyable = *form.Replyable - } - - if form.Likeable != nil { - likeable = *form.Likeable - } - - case gtsmodel.VisibilityDirect: - // direct is pretty easy: there's only one possible setting so return it - federated = true - boostable = false - replyable = true - likeable = true - } - - status.Visibility = vis - status.Federated = &federated - status.Boostable = &boostable - status.Replyable = &replyable - status.Likeable = &likeable - return nil -} - -func (p *processor) ProcessReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { - if form.InReplyToID == "" { - return nil - } - - // 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 := >smodel.Status{} - repliedAccount := >smodel.Account{} - - if err := p.db.GetByID(ctx, form.InReplyToID, repliedStatus); err != nil { - if err == db.ErrNoEntries { - err := fmt.Errorf("status with id %s not replyable because it doesn't exist", form.InReplyToID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - err := fmt.Errorf("db error fetching status with id %s: %s", form.InReplyToID, err) - return gtserror.NewErrorInternalError(err) - } - if !*repliedStatus.Replyable { - err := fmt.Errorf("status with id %s is marked as not replyable", form.InReplyToID) - return gtserror.NewErrorForbidden(err, err.Error()) - } - - if err := p.db.GetByID(ctx, repliedStatus.AccountID, repliedAccount); err != nil { - if err == db.ErrNoEntries { - err := fmt.Errorf("status with id %s not replyable because account id %s is not known", form.InReplyToID, repliedStatus.AccountID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - err := fmt.Errorf("db error fetching account with id %s: %s", repliedStatus.AccountID, err) - return gtserror.NewErrorInternalError(err) - } - - if blocked, err := p.db.IsBlocked(ctx, thisAccountID, repliedAccount.ID, true); err != nil { - err := fmt.Errorf("db error checking block: %s", err) - return gtserror.NewErrorInternalError(err) - } else if blocked { - err := fmt.Errorf("status with id %s not replyable", form.InReplyToID) - return gtserror.NewErrorNotFound(err) - } - - status.InReplyToID = repliedStatus.ID - status.InReplyToURI = repliedStatus.URI - status.InReplyToAccountID = repliedAccount.ID - - return nil -} - -func (p *processor) ProcessMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode { - if form.MediaIDs == nil { - return nil - } - - attachments := []*gtsmodel.MediaAttachment{} - attachmentIDs := []string{} - for _, mediaID := range form.MediaIDs { - attachment, err := p.db.GetAttachmentByID(ctx, mediaID) - if err != nil { - if errors.Is(err, db.ErrNoEntries) { - err = fmt.Errorf("ProcessMediaIDs: media not found for media id %s", mediaID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - err = fmt.Errorf("ProcessMediaIDs: db error for media id %s", mediaID) - return gtserror.NewErrorInternalError(err) - } - - if attachment.AccountID != thisAccountID { - err = fmt.Errorf("ProcessMediaIDs: media with id %s does not belong to account %s", mediaID, thisAccountID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - if attachment.StatusID != "" || attachment.ScheduledStatusID != "" { - err = fmt.Errorf("ProcessMediaIDs: media with id %s is already attached to a status", mediaID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - minDescriptionChars := config.GetMediaDescriptionMinChars() - if descriptionLength := len([]rune(attachment.Description)); descriptionLength < minDescriptionChars { - err = fmt.Errorf("ProcessMediaIDs: description too short! media description of at least %d chararacters is required but %d was provided for media with id %s", minDescriptionChars, descriptionLength, mediaID) - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - attachments = append(attachments, attachment) - attachmentIDs = append(attachmentIDs, attachment.ID) - } - - status.Attachments = attachments - status.AttachmentIDs = attachmentIDs - return nil -} - -func (p *processor) ProcessLanguage(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountDefaultLanguage string, status *gtsmodel.Status) error { - if form.Language != "" { - status.Language = form.Language - } else { - status.Language = accountDefaultLanguage - } - if status.Language == "" { - return errors.New("no language given either in status create form or account default") - } - return nil -} - -func (p *processor) ProcessContent(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, accountID string, status *gtsmodel.Status) error { - // if there's nothing in the status at all we can just return early - if form.Status == "" { - status.Content = "" - return nil - } - - // if format wasn't specified we should try to figure out what format this user prefers - if form.Format == "" { - acct, err := p.db.GetAccountByID(ctx, accountID) - if err != nil { - return fmt.Errorf("error processing new content: couldn't retrieve account from db to check post format: %s", err) - } - - switch acct.StatusFormat { - case "plain": - form.Format = apimodel.StatusFormatPlain - case "markdown": - form.Format = apimodel.StatusFormatMarkdown - default: - form.Format = apimodel.StatusFormatDefault - } - } - - // parse content out of the status depending on what format has been submitted - var f text.FormatFunc - switch form.Format { - case apimodel.StatusFormatPlain: - f = p.formatter.FromPlain - case apimodel.StatusFormatMarkdown: - f = p.formatter.FromMarkdown - default: - return fmt.Errorf("format %s not recognised as a valid status format", form.Format) - } - formatted := f(ctx, p.parseMention, accountID, status.ID, form.Status) - - // add full populated gts {mentions, tags, emojis} to the status for passing them around conveniently - // add just their ids to the status for putting in the db - status.Mentions = formatted.Mentions - status.MentionIDs = make([]string, 0, len(formatted.Mentions)) - for _, gtsmention := range formatted.Mentions { - status.MentionIDs = append(status.MentionIDs, gtsmention.ID) - } - - status.Tags = formatted.Tags - status.TagIDs = make([]string, 0, len(formatted.Tags)) - for _, gtstag := range formatted.Tags { - status.TagIDs = append(status.TagIDs, gtstag.ID) - } - - status.Emojis = formatted.Emojis - status.EmojiIDs = make([]string, 0, len(formatted.Emojis)) - for _, gtsemoji := range formatted.Emojis { - status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID) - } - - spoilerformatted := p.formatter.FromPlainEmojiOnly(ctx, p.parseMention, accountID, status.ID, form.SpoilerText) - for _, gtsemoji := range spoilerformatted.Emojis { - status.Emojis = append(status.Emojis, gtsemoji) - status.EmojiIDs = append(status.EmojiIDs, gtsemoji.ID) - } - - status.Content = formatted.HTML - return nil -} diff --git a/internal/processing/status/util_test.go b/internal/processing/status/util_test.go @@ -1,155 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 status_test - -import ( - "context" - "fmt" - "testing" - - "github.com/stretchr/testify/suite" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -const ( - statusText1 = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\nText" - statusText1Expected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br>Text</p>" - statusText2 = "Another test @foss_satan@fossbros-anonymous.io\n\n#Hashtag\n\n#hashTAG" - status2TextExpected = "<p>Another test <span class=\"h-card\"><a href=\"http://fossbros-anonymous.io/@foss_satan\" class=\"u-url mention\" rel=\"nofollow noreferrer noopener\" target=\"_blank\">@<span>foss_satan</span></a></span><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>Hashtag</span></a><br><br><a href=\"http://localhost:8080/tags/Hashtag\" class=\"mention hashtag\" rel=\"tag nofollow noreferrer noopener\" target=\"_blank\">#<span>hashTAG</span></a></p>" -) - -type UtilTestSuite struct { - StatusStandardTestSuite -} - -func (suite *UtilTestSuite) TestProcessContent1() { - /* - TEST PREPARATION - */ - // we need to partially process the status first since processContent expects a status with some stuff already set on it - creatingAccount := suite.testAccounts["local_account_1"] - mentionedAccount := suite.testAccounts["remote_account_1"] - form := &apimodel.AdvancedStatusCreateForm{ - StatusCreateRequest: apimodel.StatusCreateRequest{ - Status: statusText1, - MediaIDs: []string{}, - Poll: nil, - InReplyToID: "", - Sensitive: false, - SpoilerText: "", - Visibility: apimodel.VisibilityPublic, - ScheduledAt: "", - Language: "en", - Format: apimodel.StatusFormatPlain, - }, - AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ - Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, - }, - } - - status := >smodel.Status{ - ID: "01FCTDD78JJMX3K9KPXQ7ZQ8BJ", - } - - /* - ACTUAL TEST - */ - - err := suite.status.ProcessContent(context.Background(), form, creatingAccount.ID, status) - suite.NoError(err) - suite.Equal(statusText1Expected, status.Content) - - suite.Len(status.Mentions, 1) - newMention := status.Mentions[0] - suite.Equal(mentionedAccount.ID, newMention.TargetAccountID) - suite.Equal(creatingAccount.ID, newMention.OriginAccountID) - suite.Equal(creatingAccount.URI, newMention.OriginAccountURI) - suite.Equal(status.ID, newMention.StatusID) - suite.Equal(fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) - suite.Equal(mentionedAccount.URI, newMention.TargetAccountURI) - suite.Equal(mentionedAccount.URL, newMention.TargetAccountURL) - suite.NotNil(newMention.OriginAccount) - - suite.Len(status.MentionIDs, 1) - suite.Equal(newMention.ID, status.MentionIDs[0]) -} - -func (suite *UtilTestSuite) TestProcessContent2() { - /* - TEST PREPARATION - */ - // we need to partially process the status first since processContent expects a status with some stuff already set on it - creatingAccount := suite.testAccounts["local_account_1"] - mentionedAccount := suite.testAccounts["remote_account_1"] - form := &apimodel.AdvancedStatusCreateForm{ - StatusCreateRequest: apimodel.StatusCreateRequest{ - Status: statusText2, - MediaIDs: []string{}, - Poll: nil, - InReplyToID: "", - Sensitive: false, - SpoilerText: "", - Visibility: apimodel.VisibilityPublic, - ScheduledAt: "", - Language: "en", - Format: apimodel.StatusFormatPlain, - }, - AdvancedVisibilityFlagsForm: apimodel.AdvancedVisibilityFlagsForm{ - Federated: nil, - Boostable: nil, - Replyable: nil, - Likeable: nil, - }, - } - - status := >smodel.Status{ - ID: "01FCTDD78JJMX3K9KPXQ7ZQ8BJ", - } - - /* - ACTUAL TEST - */ - - err := suite.status.ProcessContent(context.Background(), form, creatingAccount.ID, status) - suite.NoError(err) - - suite.Equal(status2TextExpected, status.Content) - - suite.Len(status.Mentions, 1) - newMention := status.Mentions[0] - suite.Equal(mentionedAccount.ID, newMention.TargetAccountID) - suite.Equal(creatingAccount.ID, newMention.OriginAccountID) - suite.Equal(creatingAccount.URI, newMention.OriginAccountURI) - suite.Equal(status.ID, newMention.StatusID) - suite.Equal(fmt.Sprintf("@%s@%s", mentionedAccount.Username, mentionedAccount.Domain), newMention.NameString) - suite.Equal(mentionedAccount.URI, newMention.TargetAccountURI) - suite.Equal(mentionedAccount.URL, newMention.TargetAccountURL) - suite.NotNil(newMention.OriginAccount) - - suite.Len(status.MentionIDs, 1) - suite.Equal(newMention.ID, status.MentionIDs[0]) -} - -func TestUtilTestSuite(t *testing.T) { - suite.Run(t, new(UtilTestSuite)) -} diff --git a/internal/processing/statustimeline.go b/internal/processing/statustimeline.go @@ -137,7 +137,7 @@ func StatusSkipInsertFunction() timeline.SkipInsertFunction { } } -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 *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) { preparedItems, err := p.statusTimelines.GetTimeline(ctx, authed.Account.ID, maxID, sinceID, minID, limit, local) if err != nil { return nil, gtserror.NewErrorInternalError(err) @@ -172,7 +172,7 @@ func (p *processor) HomeTimelineGet(ctx context.Context, authed *oauth.Auth, max }) } -func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) { +func (p *Processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, maxID string, sinceID string, minID string, limit int, local bool) (*apimodel.PageableResponse, gtserror.WithCode) { statuses, err := p.db.GetPublicTimeline(ctx, maxID, sinceID, minID, limit, local) if err != nil { if err == db.ErrNoEntries { @@ -217,7 +217,7 @@ func (p *processor) PublicTimelineGet(ctx context.Context, authed *oauth.Auth, m }) } -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 *oauth.Auth, maxID string, minID string, limit int) (*apimodel.PageableResponse, gtserror.WithCode) { statuses, nextMaxID, prevMinID, err := p.db.GetFavedTimeline(ctx, authed.Account.ID, maxID, minID, limit) if err != nil { if err == db.ErrNoEntries { @@ -251,7 +251,7 @@ func (p *processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma }) } -func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { +func (p *Processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { apiStatuses := []*apimodel.Status{} for _, s := range statuses { targetAccount := >smodel.Account{} @@ -284,7 +284,7 @@ func (p *processor) filterPublicStatuses(ctx context.Context, authed *oauth.Auth return apiStatuses, nil } -func (p *processor) filterFavedStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { +func (p *Processor) filterFavedStatuses(ctx context.Context, authed *oauth.Auth, statuses []*gtsmodel.Status) ([]*apimodel.Status, error) { apiStatuses := []*apimodel.Status{} for _, s := range statuses { targetAccount := >smodel.Account{} diff --git a/internal/processing/stream/authorize.go b/internal/processing/stream/authorize.go @@ -0,0 +1,63 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "context" + "fmt" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +) + +// Authorize returns an oauth2 token info in response to an access token query from the streaming API +func (p *Processor) Authorize(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) { + ti, err := p.oauthServer.LoadAccessToken(ctx, accessToken) + if err != nil { + err := fmt.Errorf("could not load access token: %s", err) + return nil, gtserror.NewErrorUnauthorized(err) + } + + uid := ti.GetUserID() + if uid == "" { + err := fmt.Errorf("no userid in token") + return nil, gtserror.NewErrorUnauthorized(err) + } + + user, err := p.db.GetUserByID(ctx, uid) + if err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("no user found for validated uid %s", uid) + return nil, gtserror.NewErrorUnauthorized(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + acct, err := p.db.GetAccountByID(ctx, user.AccountID) + if err != nil { + if err == db.ErrNoEntries { + err := fmt.Errorf("no account found for validated uid %s", uid) + return nil, gtserror.NewErrorUnauthorized(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + return acct, nil +} diff --git a/internal/processing/stream/authorize_test.go b/internal/processing/stream/authorize_test.go @@ -0,0 +1,48 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" +) + +type AuthorizeTestSuite struct { + StreamTestSuite +} + +func (suite *AuthorizeTestSuite) TestAuthorize() { + account1, err := suite.streamProcessor.Authorize(context.Background(), suite.testTokens["local_account_1"].Access) + suite.NoError(err) + suite.Equal(suite.testAccounts["local_account_1"].ID, account1.ID) + + account2, err := suite.streamProcessor.Authorize(context.Background(), suite.testTokens["local_account_2"].Access) + suite.NoError(err) + suite.Equal(suite.testAccounts["local_account_2"].ID, account2.ID) + + noAccount, err := suite.streamProcessor.Authorize(context.Background(), "aaaaaaaaaaaaaaaaaaaaa!!") + suite.EqualError(err, "could not load access token: no entries") + suite.Nil(noAccount) +} + +func TestAuthorizeTestSuite(t *testing.T) { + suite.Run(t, &AuthorizeTestSuite{}) +} diff --git a/internal/processing/stream/delete.go b/internal/processing/stream/delete.go @@ -0,0 +1,56 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "fmt" + "strings" + + "github.com/superseriousbusiness/gotosocial/internal/stream" +) + +// Delete streams the delete of the given statusID to *ALL* open streams. +func (p *Processor) Delete(statusID string) error { + errs := []string{} + + // get all account IDs with open streams + accountIDs := []string{} + p.streamMap.Range(func(k interface{}, _ interface{}) bool { + key, ok := k.(string) + if !ok { + panic("streamMap key was not a string (account id)") + } + + accountIDs = append(accountIDs, key) + return true + }) + + // stream the delete to every account + for _, accountID := range accountIDs { + if err := p.toAccount(statusID, stream.EventTypeDelete, stream.AllStatusTimelines, accountID); err != nil { + errs = append(errs, err.Error()) + } + } + + if len(errs) != 0 { + return fmt.Errorf("one or more errors streaming status delete: %s", strings.Join(errs, ";")) + } + + return nil +} diff --git a/internal/processing/stream/notification.go b/internal/processing/stream/notification.go @@ -0,0 +1,38 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "encoding/json" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/stream" +) + +// Notify streams the given notification to any open, appropriate streams belonging to the given account. +func (p *Processor) Notify(n *apimodel.Notification, account *gtsmodel.Account) error { + bytes, err := json.Marshal(n) + if err != nil { + return fmt.Errorf("error marshalling notification to json: %s", err) + } + + return p.toAccount(string(bytes), stream.EventTypeNotification, []string{stream.TimelineNotifications, stream.TimelineHome}, account.ID) +} diff --git a/internal/processing/stream/notification_test.go b/internal/processing/stream/notification_test.go @@ -0,0 +1,91 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream_test + +import ( + "bytes" + "context" + "encoding/json" + "testing" + + "github.com/stretchr/testify/suite" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type NotificationTestSuite struct { + StreamTestSuite +} + +func (suite *NotificationTestSuite) TestStreamNotification() { + account := suite.testAccounts["local_account_1"] + + openStream, errWithCode := suite.streamProcessor.Open(context.Background(), account, "user") + suite.NoError(errWithCode) + + followAccount := suite.testAccounts["remote_account_1"] + followAccountAPIModel, err := testrig.NewTestTypeConverter(suite.db).AccountToAPIAccountPublic(context.Background(), followAccount) + suite.NoError(err) + + notification := &apimodel.Notification{ + ID: "01FH57SJCMDWQGEAJ0X08CE3WV", + Type: "follow", + CreatedAt: "2021-10-04T08:52:36.000Z", + Account: followAccountAPIModel, + } + + err = suite.streamProcessor.Notify(notification, account) + suite.NoError(err) + + msg := <-openStream.Messages + dst := new(bytes.Buffer) + err = json.Indent(dst, []byte(msg.Payload), "", " ") + suite.NoError(err) + suite.Equal(`{ + "id": "01FH57SJCMDWQGEAJ0X08CE3WV", + "type": "follow", + "created_at": "2021-10-04T08:52:36.000Z", + "account": { + "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", + "username": "foss_satan", + "acct": "foss_satan@fossbros-anonymous.io", + "display_name": "big gerald", + "locked": false, + "discoverable": true, + "bot": false, + "created_at": "2021-09-26T10:52:36.000Z", + "note": "i post about like, i dunno, stuff, or whatever!!!!", + "url": "http://fossbros-anonymous.io/@foss_satan", + "avatar": "", + "avatar_static": "", + "header": "http://localhost:8080/assets/default_header.png", + "header_static": "http://localhost:8080/assets/default_header.png", + "followers_count": 0, + "following_count": 0, + "statuses_count": 1, + "last_status_at": "2021-09-20T10:40:37.000Z", + "emojis": [], + "fields": [] + } +}`, dst.String()) +} + +func TestNotificationTestSuite(t *testing.T) { + suite.Run(t, &NotificationTestSuite{}) +} diff --git a/internal/processing/stream/open.go b/internal/processing/stream/open.go @@ -0,0 +1,121 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "context" + "errors" + "fmt" + + "codeberg.org/gruf/go-kv" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/id" + "github.com/superseriousbusiness/gotosocial/internal/log" + "github.com/superseriousbusiness/gotosocial/internal/stream" +) + +// Open returns a new Stream for the given account, which will contain a channel for passing messages back to the caller. +func (p *Processor) Open(ctx context.Context, account *gtsmodel.Account, streamTimeline string) (*stream.Stream, gtserror.WithCode) { + l := log.WithContext(ctx).WithFields(kv.Fields{ + {"account", account.ID}, + {"streamType", streamTimeline}, + }...) + l.Debug("received open stream request") + + // each stream needs a unique ID so we know to close it + streamID, err := id.NewRandomULID() + if err != nil { + return nil, gtserror.NewErrorInternalError(fmt.Errorf("error generating stream id: %s", err)) + } + + thisStream := &stream.Stream{ + ID: streamID, + Timeline: streamTimeline, + Messages: make(chan *stream.Message, 100), + Hangup: make(chan interface{}, 1), + Connected: true, + } + go p.waitToCloseStream(account, thisStream) + + v, ok := p.streamMap.Load(account.ID) + if !ok || v == nil { + // there is no entry in the streamMap for this account yet, so make one and store it + streamsForAccount := &stream.StreamsForAccount{ + Streams: []*stream.Stream{ + thisStream, + }, + } + p.streamMap.Store(account.ID, streamsForAccount) + } else { + // there is an entry in the streamMap for this account + // parse the interface as a streamsForAccount + streamsForAccount, ok := v.(*stream.StreamsForAccount) + if !ok { + return nil, gtserror.NewErrorInternalError(errors.New("stream map error")) + } + + // append this stream to it + streamsForAccount.Lock() + streamsForAccount.Streams = append(streamsForAccount.Streams, thisStream) + streamsForAccount.Unlock() + } + + return thisStream, nil +} + +// waitToCloseStream waits until the hangup channel is closed for the given stream. +// It then iterates through the map of streams stored by the processor, removes the stream from it, +// and then closes the messages channel of the stream to indicate that the channel should no longer be read from. +func (p *Processor) waitToCloseStream(account *gtsmodel.Account, thisStream *stream.Stream) { + <-thisStream.Hangup // wait for a hangup message + + // lock the stream to prevent more messages being put in it while we work + thisStream.Lock() + defer thisStream.Unlock() + + // indicate the stream is no longer connected + thisStream.Connected = false + + // load and parse the entry for this account from the stream map + v, ok := p.streamMap.Load(account.ID) + if !ok || v == nil { + return + } + streamsForAccount, ok := v.(*stream.StreamsForAccount) + if !ok { + return + } + + // lock the streams for account while we remove this stream from its slice + streamsForAccount.Lock() + defer streamsForAccount.Unlock() + + // put everything into modified streams *except* the stream we're removing + modifiedStreams := []*stream.Stream{} + for _, s := range streamsForAccount.Streams { + if s.ID != thisStream.ID { + modifiedStreams = append(modifiedStreams, s) + } + } + streamsForAccount.Streams = modifiedStreams + + // finally close the messages channel so no more messages can be read from it + close(thisStream.Messages) +} diff --git a/internal/processing/stream/open_test.go b/internal/processing/stream/open_test.go @@ -0,0 +1,41 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream_test + +import ( + "context" + "testing" + + "github.com/stretchr/testify/suite" +) + +type OpenStreamTestSuite struct { + StreamTestSuite +} + +func (suite *OpenStreamTestSuite) TestOpenStream() { + account := suite.testAccounts["local_account_1"] + + _, errWithCode := suite.streamProcessor.Open(context.Background(), account, "user") + suite.NoError(errWithCode) +} + +func TestOpenStreamTestSuite(t *testing.T) { + suite.Run(t, &OpenStreamTestSuite{}) +} diff --git a/internal/processing/stream/stream.go b/internal/processing/stream/stream.go @@ -0,0 +1,78 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "errors" + "sync" + + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/stream" +) + +type Processor struct { + db db.DB + oauthServer oauth.Server + streamMap *sync.Map +} + +func New(db db.DB, oauthServer oauth.Server) Processor { + return Processor{ + db: db, + oauthServer: oauthServer, + streamMap: &sync.Map{}, + } +} + +// toAccount streams the given payload with the given event type to any streams currently open for the given account ID. +func (p *Processor) toAccount(payload string, event string, timelines []string, accountID string) error { + v, ok := p.streamMap.Load(accountID) + if !ok { + // no open connections so nothing to stream + return nil + } + + streamsForAccount, ok := v.(*stream.StreamsForAccount) + if !ok { + return errors.New("stream map error") + } + + streamsForAccount.Lock() + defer streamsForAccount.Unlock() + for _, s := range streamsForAccount.Streams { + s.Lock() + defer s.Unlock() + if !s.Connected { + continue + } + + for _, t := range timelines { + if s.Timeline == string(t) { + s.Messages <- &stream.Message{ + Stream: []string{string(t)}, + Event: string(event), + Payload: payload, + } + } + } + } + + return nil +} diff --git a/internal/processing/stream/stream_test.go b/internal/processing/stream/stream_test.go @@ -0,0 +1,55 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream_test + +import ( + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/oauth" + "github.com/superseriousbusiness/gotosocial/internal/processing/stream" + "github.com/superseriousbusiness/gotosocial/testrig" +) + +type StreamTestSuite struct { + suite.Suite + testAccounts map[string]*gtsmodel.Account + testTokens map[string]*gtsmodel.Token + db db.DB + oauthServer oauth.Server + + streamProcessor stream.Processor +} + +func (suite *StreamTestSuite) SetupTest() { + testrig.InitTestLog() + testrig.InitTestConfig() + + suite.testAccounts = testrig.NewTestAccounts() + suite.testTokens = testrig.NewTestTokens() + suite.db = testrig.NewTestDB() + suite.oauthServer = testrig.NewTestOauthServer(suite.db) + suite.streamProcessor = stream.New(suite.db, suite.oauthServer) + + testrig.StandardDBSetup(suite.db, suite.testAccounts) +} + +func (suite *StreamTestSuite) TearDownTest() { + testrig.StandardDBTeardown(suite.db) +} diff --git a/internal/processing/stream/update.go b/internal/processing/stream/update.go @@ -0,0 +1,38 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 stream + +import ( + "encoding/json" + "fmt" + + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/stream" +) + +// Update streams the given update to any open, appropriate streams belonging to the given account. +func (p *Processor) Update(s *apimodel.Status, account *gtsmodel.Account, timeline string) error { + bytes, err := json.Marshal(s) + if err != nil { + return fmt.Errorf("error marshalling status to json: %s", err) + } + + return p.toAccount(string(bytes), stream.EventTypeUpdate, []string{timeline}, account.ID) +} diff --git a/internal/processing/streaming.go b/internal/processing/streaming.go @@ -1,35 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) { - return p.streamingProcessor.AuthorizeStreamingRequest(ctx, accessToken) -} - -func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamType string) (*stream.Stream, gtserror.WithCode) { - return p.streamingProcessor.OpenStreamForAccount(ctx, account, streamType) -} diff --git a/internal/processing/streaming/authorize.go b/internal/processing/streaming/authorize.go @@ -1,62 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "context" - "fmt" - - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" -) - -func (p *processor) AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) { - ti, err := p.oauthServer.LoadAccessToken(ctx, accessToken) - if err != nil { - err := fmt.Errorf("could not load access token: %s", err) - return nil, gtserror.NewErrorUnauthorized(err) - } - - uid := ti.GetUserID() - if uid == "" { - err := fmt.Errorf("no userid in token") - return nil, gtserror.NewErrorUnauthorized(err) - } - - user, err := p.db.GetUserByID(ctx, uid) - if err != nil { - if err == db.ErrNoEntries { - err := fmt.Errorf("no user found for validated uid %s", uid) - return nil, gtserror.NewErrorUnauthorized(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - acct, err := p.db.GetAccountByID(ctx, user.AccountID) - if err != nil { - if err == db.ErrNoEntries { - err := fmt.Errorf("no account found for validated uid %s", uid) - return nil, gtserror.NewErrorUnauthorized(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - return acct, nil -} diff --git a/internal/processing/streaming/authorize_test.go b/internal/processing/streaming/authorize_test.go @@ -1,48 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" -) - -type AuthorizeTestSuite struct { - StreamingTestSuite -} - -func (suite *AuthorizeTestSuite) TestAuthorize() { - account1, err := suite.streamingProcessor.AuthorizeStreamingRequest(context.Background(), suite.testTokens["local_account_1"].Access) - suite.NoError(err) - suite.Equal(suite.testAccounts["local_account_1"].ID, account1.ID) - - account2, err := suite.streamingProcessor.AuthorizeStreamingRequest(context.Background(), suite.testTokens["local_account_2"].Access) - suite.NoError(err) - suite.Equal(suite.testAccounts["local_account_2"].ID, account2.ID) - - noAccount, err := suite.streamingProcessor.AuthorizeStreamingRequest(context.Background(), "aaaaaaaaaaaaaaaaaaaaa!!") - suite.EqualError(err, "could not load access token: no entries") - suite.Nil(noAccount) -} - -func TestAuthorizeTestSuite(t *testing.T) { - suite.Run(t, &AuthorizeTestSuite{}) -} diff --git a/internal/processing/streaming/notification.go b/internal/processing/streaming/notification.go @@ -1,37 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "encoding/json" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -func (p *processor) StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error { - bytes, err := json.Marshal(n) - if err != nil { - return fmt.Errorf("error marshalling notification to json: %s", err) - } - - return p.streamToAccount(string(bytes), stream.EventTypeNotification, []string{stream.TimelineNotifications, stream.TimelineHome}, account.ID) -} diff --git a/internal/processing/streaming/notification_test.go b/internal/processing/streaming/notification_test.go @@ -1,91 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming_test - -import ( - "bytes" - "context" - "encoding/json" - "testing" - - "github.com/stretchr/testify/suite" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type NotificationTestSuite struct { - StreamingTestSuite -} - -func (suite *NotificationTestSuite) TestStreamNotification() { - account := suite.testAccounts["local_account_1"] - - openStream, errWithCode := suite.streamingProcessor.OpenStreamForAccount(context.Background(), account, "user") - suite.NoError(errWithCode) - - followAccount := suite.testAccounts["remote_account_1"] - followAccountAPIModel, err := testrig.NewTestTypeConverter(suite.db).AccountToAPIAccountPublic(context.Background(), followAccount) - suite.NoError(err) - - notification := &apimodel.Notification{ - ID: "01FH57SJCMDWQGEAJ0X08CE3WV", - Type: "follow", - CreatedAt: "2021-10-04T08:52:36.000Z", - Account: followAccountAPIModel, - } - - err = suite.streamingProcessor.StreamNotificationToAccount(notification, account) - suite.NoError(err) - - msg := <-openStream.Messages - dst := new(bytes.Buffer) - err = json.Indent(dst, []byte(msg.Payload), "", " ") - suite.NoError(err) - suite.Equal(`{ - "id": "01FH57SJCMDWQGEAJ0X08CE3WV", - "type": "follow", - "created_at": "2021-10-04T08:52:36.000Z", - "account": { - "id": "01F8MH5ZK5VRH73AKHQM6Y9VNX", - "username": "foss_satan", - "acct": "foss_satan@fossbros-anonymous.io", - "display_name": "big gerald", - "locked": false, - "discoverable": true, - "bot": false, - "created_at": "2021-09-26T10:52:36.000Z", - "note": "i post about like, i dunno, stuff, or whatever!!!!", - "url": "http://fossbros-anonymous.io/@foss_satan", - "avatar": "", - "avatar_static": "", - "header": "http://localhost:8080/assets/default_header.png", - "header_static": "http://localhost:8080/assets/default_header.png", - "followers_count": 0, - "following_count": 0, - "statuses_count": 1, - "last_status_at": "2021-09-20T10:40:37.000Z", - "emojis": [], - "fields": [] - } -}`, dst.String()) -} - -func TestNotificationTestSuite(t *testing.T) { - suite.Run(t, &NotificationTestSuite{}) -} diff --git a/internal/processing/streaming/openstream.go b/internal/processing/streaming/openstream.go @@ -1,121 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "context" - "errors" - "fmt" - - "codeberg.org/gruf/go-kv" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -func (p *processor) OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, streamTimeline string) (*stream.Stream, gtserror.WithCode) { - l := log.WithContext(ctx). - WithFields(kv.Fields{ - {"account", account.ID}, - {"streamType", streamTimeline}, - }...) - l.Debug("received open stream request") - - // each stream needs a unique ID so we know to close it - streamID, err := id.NewRandomULID() - if err != nil { - return nil, gtserror.NewErrorInternalError(fmt.Errorf("error generating stream id: %s", err)) - } - - thisStream := &stream.Stream{ - ID: streamID, - Timeline: streamTimeline, - Messages: make(chan *stream.Message, 100), - Hangup: make(chan interface{}, 1), - Connected: true, - } - go p.waitToCloseStream(account, thisStream) - - v, ok := p.streamMap.Load(account.ID) - if !ok || v == nil { - // there is no entry in the streamMap for this account yet, so make one and store it - streamsForAccount := &stream.StreamsForAccount{ - Streams: []*stream.Stream{ - thisStream, - }, - } - p.streamMap.Store(account.ID, streamsForAccount) - } else { - // there is an entry in the streamMap for this account - // parse the interface as a streamsForAccount - streamsForAccount, ok := v.(*stream.StreamsForAccount) - if !ok { - return nil, gtserror.NewErrorInternalError(errors.New("stream map error")) - } - - // append this stream to it - streamsForAccount.Lock() - streamsForAccount.Streams = append(streamsForAccount.Streams, thisStream) - streamsForAccount.Unlock() - } - - return thisStream, nil -} - -// waitToCloseStream waits until the hangup channel is closed for the given stream. -// It then iterates through the map of streams stored by the processor, removes the stream from it, -// and then closes the messages channel of the stream to indicate that the channel should no longer be read from. -func (p *processor) waitToCloseStream(account *gtsmodel.Account, thisStream *stream.Stream) { - <-thisStream.Hangup // wait for a hangup message - - // lock the stream to prevent more messages being put in it while we work - thisStream.Lock() - defer thisStream.Unlock() - - // indicate the stream is no longer connected - thisStream.Connected = false - - // load and parse the entry for this account from the stream map - v, ok := p.streamMap.Load(account.ID) - if !ok || v == nil { - return - } - streamsForAccount, ok := v.(*stream.StreamsForAccount) - if !ok { - return - } - - // lock the streams for account while we remove this stream from its slice - streamsForAccount.Lock() - defer streamsForAccount.Unlock() - - // put everything into modified streams *except* the stream we're removing - modifiedStreams := []*stream.Stream{} - for _, s := range streamsForAccount.Streams { - if s.ID != thisStream.ID { - modifiedStreams = append(modifiedStreams, s) - } - } - streamsForAccount.Streams = modifiedStreams - - // finally close the messages channel so no more messages can be read from it - close(thisStream.Messages) -} diff --git a/internal/processing/streaming/openstream_test.go b/internal/processing/streaming/openstream_test.go @@ -1,41 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/suite" -) - -type OpenStreamTestSuite struct { - StreamingTestSuite -} - -func (suite *OpenStreamTestSuite) TestOpenStream() { - account := suite.testAccounts["local_account_1"] - - _, errWithCode := suite.streamingProcessor.OpenStreamForAccount(context.Background(), account, "user") - suite.NoError(errWithCode) -} - -func TestOpenStreamTestSuite(t *testing.T) { - suite.Run(t, &OpenStreamTestSuite{}) -} diff --git a/internal/processing/streaming/streamdelete.go b/internal/processing/streaming/streamdelete.go @@ -1,55 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "fmt" - "strings" - - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -func (p *processor) StreamDelete(statusID string) error { - errs := []string{} - - // get all account IDs with open streams - accountIDs := []string{} - p.streamMap.Range(func(k interface{}, _ interface{}) bool { - key, ok := k.(string) - if !ok { - panic("streamMap key was not a string (account id)") - } - - accountIDs = append(accountIDs, key) - return true - }) - - // stream the delete to every account - for _, accountID := range accountIDs { - if err := p.streamToAccount(statusID, stream.EventTypeDelete, stream.AllStatusTimelines, accountID); err != nil { - errs = append(errs, err.Error()) - } - } - - if len(errs) != 0 { - return fmt.Errorf("one or more errors streaming status delete: %s", strings.Join(errs, ";")) - } - - return nil -} diff --git a/internal/processing/streaming/streaming.go b/internal/processing/streaming/streaming.go @@ -1,60 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "context" - "sync" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -// Processor wraps a bunch of functions for processing streaming. -type Processor interface { - // AuthorizeStreamingRequest returns an oauth2 token info in response to an access token query from the streaming API - AuthorizeStreamingRequest(ctx context.Context, accessToken string) (*gtsmodel.Account, gtserror.WithCode) - // OpenStreamForAccount returns a new Stream for the given account, which will contain a channel for passing messages back to the caller. - OpenStreamForAccount(ctx context.Context, account *gtsmodel.Account, timeline string) (*stream.Stream, gtserror.WithCode) - // StreamUpdateToAccount streams the given update to any open, appropriate streams belonging to the given account. - StreamUpdateToAccount(s *apimodel.Status, account *gtsmodel.Account, timeline string) error - // StreamNotificationToAccount streams the given notification to any open, appropriate streams belonging to the given account. - StreamNotificationToAccount(n *apimodel.Notification, account *gtsmodel.Account) error - // StreamDelete streams the delete of the given statusID to *ALL* open streams. - StreamDelete(statusID string) error -} - -type processor struct { - db db.DB - oauthServer oauth.Server - streamMap *sync.Map -} - -// New returns a new status processor. -func New(db db.DB, oauthServer oauth.Server) Processor { - return &processor{ - db: db, - oauthServer: oauthServer, - streamMap: &sync.Map{}, - } -} diff --git a/internal/processing/streaming/streaming_test.go b/internal/processing/streaming/streaming_test.go @@ -1,55 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming_test - -import ( - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/processing/streaming" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type StreamingTestSuite struct { - suite.Suite - testAccounts map[string]*gtsmodel.Account - testTokens map[string]*gtsmodel.Token - db db.DB - oauthServer oauth.Server - - streamingProcessor streaming.Processor -} - -func (suite *StreamingTestSuite) SetupTest() { - testrig.InitTestLog() - testrig.InitTestConfig() - - suite.testAccounts = testrig.NewTestAccounts() - suite.testTokens = testrig.NewTestTokens() - suite.db = testrig.NewTestDB() - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.streamingProcessor = streaming.New(suite.db, suite.oauthServer) - - testrig.StandardDBSetup(suite.db, suite.testAccounts) -} - -func (suite *StreamingTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) -} diff --git a/internal/processing/streaming/streamtoaccount.go b/internal/processing/streaming/streamtoaccount.go @@ -1,61 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "errors" - - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -// streamToAccount streams the given payload with the given event type to any streams currently open for the given account ID. -func (p *processor) streamToAccount(payload string, event string, timelines []string, accountID string) error { - v, ok := p.streamMap.Load(accountID) - if !ok { - // no open connections so nothing to stream - return nil - } - - streamsForAccount, ok := v.(*stream.StreamsForAccount) - if !ok { - return errors.New("stream map error") - } - - streamsForAccount.Lock() - defer streamsForAccount.Unlock() - for _, s := range streamsForAccount.Streams { - s.Lock() - defer s.Unlock() - if !s.Connected { - continue - } - - for _, t := range timelines { - if s.Timeline == string(t) { - s.Messages <- &stream.Message{ - Stream: []string{string(t)}, - Event: string(event), - Payload: payload, - } - } - } - } - - return nil -} diff --git a/internal/processing/streaming/update.go b/internal/processing/streaming/update.go @@ -1,37 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 streaming - -import ( - "encoding/json" - "fmt" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/stream" -) - -func (p *processor) StreamUpdateToAccount(s *apimodel.Status, account *gtsmodel.Account, timeline string) error { - bytes, err := json.Marshal(s) - if err != nil { - return fmt.Errorf("error marshalling status to json: %s", err) - } - - return p.streamToAccount(string(bytes), stream.EventTypeUpdate, []string{timeline}, account.ID) -} diff --git a/internal/processing/user.go b/internal/processing/user.go @@ -1,36 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 processing - -import ( - "context" - - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/oauth" -) - -func (p *processor) UserChangePassword(ctx context.Context, authed *oauth.Auth, form *apimodel.PasswordChangeRequest) gtserror.WithCode { - return p.userProcessor.ChangePassword(ctx, authed.User, form.OldPassword, form.NewPassword) -} - -func (p *processor) UserConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) { - return p.userProcessor.ConfirmEmail(ctx, token) -} diff --git a/internal/processing/user/changepassword.go b/internal/processing/user/changepassword.go @@ -1,51 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 user - -import ( - "context" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/validate" - "golang.org/x/crypto/bcrypt" -) - -func (p *processor) ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode { - if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil { - return gtserror.NewErrorUnauthorized(err, "old password was incorrect") - } - - if err := validate.NewPassword(newPassword); err != nil { - return gtserror.NewErrorBadRequest(err, err.Error()) - } - - newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) - if err != nil { - return gtserror.NewErrorInternalError(err, "error hashing password") - } - - user.EncryptedPassword = string(newPasswordHash) - - if err := p.db.UpdateUser(ctx, user, "encrypted_password"); err != nil { - return gtserror.NewErrorInternalError(err) - } - - return nil -} diff --git a/internal/processing/user/changepassword_test.go b/internal/processing/user/changepassword_test.go @@ -1,92 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 user_test - -import ( - "context" - "net/http" - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "golang.org/x/crypto/bcrypt" -) - -type ChangePasswordTestSuite struct { - UserStandardTestSuite -} - -func (suite *ChangePasswordTestSuite) TestChangePasswordOK() { - user := suite.testUsers["local_account_1"] - - errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "verygoodnewpassword") - suite.NoError(errWithCode) - - err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword")) - suite.NoError(err) - - // get user from the db again - dbUser := >smodel.User{} - err = suite.db.GetByID(context.Background(), user.ID, dbUser) - suite.NoError(err) - - // check the password has changed - err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword")) - suite.NoError(err) -} - -func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() { - user := suite.testUsers["local_account_1"] - - errWithCode := suite.user.ChangePassword(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword") - suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password") - suite.Equal(http.StatusUnauthorized, errWithCode.Code()) - suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe()) - - // get user from the db again - dbUser := >smodel.User{} - err := suite.db.GetByID(context.Background(), user.ID, dbUser) - suite.NoError(err) - - // check the password has not changed - err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password")) - suite.NoError(err) -} - -func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() { - user := suite.testUsers["local_account_1"] - - errWithCode := suite.user.ChangePassword(context.Background(), user, "password", "1234") - suite.EqualError(errWithCode, "password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password") - suite.Equal(http.StatusBadRequest, errWithCode.Code()) - suite.Equal("Bad Request: password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe()) - - // get user from the db again - dbUser := >smodel.User{} - err := suite.db.GetByID(context.Background(), user.ID, dbUser) - suite.NoError(err) - - // check the password has not changed - err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password")) - suite.NoError(err) -} - -func TestChangePasswordTestSuite(t *testing.T) { - suite.Run(t, &ChangePasswordTestSuite{}) -} diff --git a/internal/processing/user/email.go b/internal/processing/user/email.go @@ -0,0 +1,137 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 user + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/google/uuid" + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/email" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/uris" +) + +var oneWeek = 168 * time.Hour + +// EmailSendConfirmation sends an email address confirmation request email to the given user. +func (p *Processor) EmailSendConfirmation(ctx context.Context, user *gtsmodel.User, username string) error { + if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { + // user has already confirmed this email address, so there's nothing to do + return nil + } + + // We need a token and a link for the user to click on. + // We'll use a uuid as our token since it's basically impossible to guess. + // From the uuid package we use (which uses crypto/rand under the hood): + // Randomly generated UUIDs have 122 random bits. One's annual risk of being + // hit by a meteorite is estimated to be one chance in 17 billion, that + // means the probability is about 0.00000000006 (6 × 10−11), + // equivalent to the odds of creating a few tens of trillions of UUIDs in a + // year and having one duplicate. + confirmationToken := uuid.NewString() + confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken) + + // pull our instance entry from the database so we can greet the user nicely in the email + instance := >smodel.Instance{} + host := config.GetHost() + if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil { + return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err) + } + + // assemble the email contents and send the email + confirmData := email.ConfirmData{ + Username: username, + InstanceURL: instance.URI, + InstanceName: instance.Title, + ConfirmLink: confirmationLink, + } + if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil { + return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err) + } + + // email sent, now we need to update the user entry with the token we just sent them + updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"} + user.ConfirmationSentAt = time.Now() + user.ConfirmationToken = confirmationToken + user.LastEmailedAt = time.Now() + user.UpdatedAt = time.Now() + + if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { + return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err) + } + + return nil +} + +// EmailConfirm processes an email confirmation request, usually initiated as a result of clicking on a link +// in a 'confirm your email address' type email. +func (p *Processor) EmailConfirm(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) { + if token == "" { + return nil, gtserror.NewErrorNotFound(errors.New("no token provided")) + } + + user, err := p.db.GetUserByConfirmationToken(ctx, token) + if err != nil { + if err == db.ErrNoEntries { + return nil, gtserror.NewErrorNotFound(err) + } + return nil, gtserror.NewErrorInternalError(err) + } + + if user.Account == nil { + a, err := p.db.GetAccountByID(ctx, user.AccountID) + if err != nil { + return nil, gtserror.NewErrorNotFound(err) + } + user.Account = a + } + + if !user.Account.SuspendedAt.IsZero() { + return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID)) + } + + if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { + // no pending email confirmations so just return OK + return user, nil + } + + if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) { + return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired")) + } + + // mark the user's email address as confirmed + remove the unconfirmed address and the token + updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"} + user.Email = user.UnconfirmedEmail + user.UnconfirmedEmail = "" + user.ConfirmedAt = time.Now() + user.ConfirmationToken = "" + user.UpdatedAt = time.Now() + + if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { + return nil, gtserror.NewErrorInternalError(err) + } + + return user, nil +} diff --git a/internal/processing/user/email_test.go b/internal/processing/user/email_test.go @@ -0,0 +1,116 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 user_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/suite" +) + +type EmailConfirmTestSuite struct { + UserStandardTestSuite +} + +func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() { + user := suite.testUsers["local_account_1"] + + // set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought) + user.UnconfirmedEmail = "some.email@example.org" + user.Email = "" + user.ConfirmedAt = time.Time{} + user.ConfirmationSentAt = time.Time{} + user.ConfirmationToken = "" + + err := suite.user.EmailSendConfirmation(context.Background(), user, "the_mighty_zork") + suite.NoError(err) + + // zork should have an email now + suite.Len(suite.sentEmails, 1) + email, ok := suite.sentEmails["some.email@example.org"] + suite.True(ok) + + // a token should be set on zork + token := user.ConfirmationToken + suite.NotEmpty(token) + + // email should contain the token + emailShould := fmt.Sprintf("To: some.email@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token) + suite.Equal(emailShould, email) + + // confirmationSentAt should be recent + suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute) +} + +func (suite *EmailConfirmTestSuite) TestConfirmEmail() { + ctx := context.Background() + + user := suite.testUsers["local_account_1"] + + // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 5 minutes ago + updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"} + user.UnconfirmedEmail = "some.email@example.org" + user.Email = "" + user.ConfirmedAt = time.Time{} + user.ConfirmationSentAt = time.Now().Add(-5 * time.Minute) + user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6" + + err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...) + suite.NoError(err) + + // confirm with the token set above + updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6") + suite.NoError(errWithCode) + + // email should now be confirmed and token cleared + suite.Equal("some.email@example.org", updatedUser.Email) + suite.Empty(updatedUser.UnconfirmedEmail) + suite.Empty(updatedUser.ConfirmationToken) + suite.WithinDuration(updatedUser.ConfirmedAt, time.Now(), 1*time.Minute) + suite.WithinDuration(updatedUser.UpdatedAt, time.Now(), 1*time.Minute) +} + +func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() { + ctx := context.Background() + + user := suite.testUsers["local_account_1"] + + // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 8 days ago + updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"} + user.UnconfirmedEmail = "some.email@example.org" + user.Email = "" + user.ConfirmedAt = time.Time{} + user.ConfirmationSentAt = time.Now().Add(-192 * time.Hour) + user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6" + + err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...) + suite.NoError(err) + + // confirm with the token set above + updatedUser, errWithCode := suite.user.EmailConfirm(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6") + suite.Nil(updatedUser) + suite.EqualError(errWithCode, "ConfirmEmail: confirmation token expired") +} + +func TestEmailConfirmTestSuite(t *testing.T) { + suite.Run(t, &EmailConfirmTestSuite{}) +} diff --git a/internal/processing/user/emailconfirm.go b/internal/processing/user/emailconfirm.go @@ -1,134 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 user - -import ( - "context" - "errors" - "fmt" - "time" - - "github.com/google/uuid" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/email" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -var oneWeek = 168 * time.Hour - -func (p *processor) SendConfirmEmail(ctx context.Context, user *gtsmodel.User, username string) error { - if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { - // user has already confirmed this email address, so there's nothing to do - return nil - } - - // We need a token and a link for the user to click on. - // We'll use a uuid as our token since it's basically impossible to guess. - // From the uuid package we use (which uses crypto/rand under the hood): - // Randomly generated UUIDs have 122 random bits. One's annual risk of being - // hit by a meteorite is estimated to be one chance in 17 billion, that - // means the probability is about 0.00000000006 (6 × 10−11), - // equivalent to the odds of creating a few tens of trillions of UUIDs in a - // year and having one duplicate. - confirmationToken := uuid.NewString() - confirmationLink := uris.GenerateURIForEmailConfirm(confirmationToken) - - // pull our instance entry from the database so we can greet the user nicely in the email - instance := >smodel.Instance{} - host := config.GetHost() - if err := p.db.GetWhere(ctx, []db.Where{{Key: "domain", Value: host}}, instance); err != nil { - return fmt.Errorf("SendConfirmEmail: error getting instance: %s", err) - } - - // assemble the email contents and send the email - confirmData := email.ConfirmData{ - Username: username, - InstanceURL: instance.URI, - InstanceName: instance.Title, - ConfirmLink: confirmationLink, - } - if err := p.emailSender.SendConfirmEmail(user.UnconfirmedEmail, confirmData); err != nil { - return fmt.Errorf("SendConfirmEmail: error sending to email address %s belonging to user %s: %s", user.UnconfirmedEmail, username, err) - } - - // email sent, now we need to update the user entry with the token we just sent them - updatingColumns := []string{"confirmation_sent_at", "confirmation_token", "last_emailed_at", "updated_at"} - user.ConfirmationSentAt = time.Now() - user.ConfirmationToken = confirmationToken - user.LastEmailedAt = time.Now() - user.UpdatedAt = time.Now() - - if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { - return fmt.Errorf("SendConfirmEmail: error updating user entry after email sent: %s", err) - } - - return nil -} - -func (p *processor) ConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) { - if token == "" { - return nil, gtserror.NewErrorNotFound(errors.New("no token provided")) - } - - user, err := p.db.GetUserByConfirmationToken(ctx, token) - if err != nil { - if err == db.ErrNoEntries { - return nil, gtserror.NewErrorNotFound(err) - } - return nil, gtserror.NewErrorInternalError(err) - } - - if user.Account == nil { - a, err := p.db.GetAccountByID(ctx, user.AccountID) - if err != nil { - return nil, gtserror.NewErrorNotFound(err) - } - user.Account = a - } - - if !user.Account.SuspendedAt.IsZero() { - return nil, gtserror.NewErrorForbidden(fmt.Errorf("ConfirmEmail: account %s is suspended", user.AccountID)) - } - - if user.UnconfirmedEmail == "" || user.UnconfirmedEmail == user.Email { - // no pending email confirmations so just return OK - return user, nil - } - - if user.ConfirmationSentAt.Before(time.Now().Add(-oneWeek)) { - return nil, gtserror.NewErrorForbidden(errors.New("ConfirmEmail: confirmation token expired")) - } - - // mark the user's email address as confirmed + remove the unconfirmed address and the token - updatingColumns := []string{"email", "unconfirmed_email", "confirmed_at", "confirmation_token", "updated_at"} - user.Email = user.UnconfirmedEmail - user.UnconfirmedEmail = "" - user.ConfirmedAt = time.Now() - user.ConfirmationToken = "" - user.UpdatedAt = time.Now() - - if err := p.db.UpdateByID(ctx, user, user.ID, updatingColumns...); err != nil { - return nil, gtserror.NewErrorInternalError(err) - } - - return user, nil -} diff --git a/internal/processing/user/emailconfirm_test.go b/internal/processing/user/emailconfirm_test.go @@ -1,116 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2023 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 user_test - -import ( - "context" - "fmt" - "testing" - "time" - - "github.com/stretchr/testify/suite" -) - -type EmailConfirmTestSuite struct { - UserStandardTestSuite -} - -func (suite *EmailConfirmTestSuite) TestSendConfirmEmail() { - user := suite.testUsers["local_account_1"] - - // set a bunch of stuff on the user as though zork hasn't been confirmed (perish the thought) - user.UnconfirmedEmail = "some.email@example.org" - user.Email = "" - user.ConfirmedAt = time.Time{} - user.ConfirmationSentAt = time.Time{} - user.ConfirmationToken = "" - - err := suite.user.SendConfirmEmail(context.Background(), user, "the_mighty_zork") - suite.NoError(err) - - // zork should have an email now - suite.Len(suite.sentEmails, 1) - email, ok := suite.sentEmails["some.email@example.org"] - suite.True(ok) - - // a token should be set on zork - token := user.ConfirmationToken - suite.NotEmpty(token) - - // email should contain the token - emailShould := fmt.Sprintf("To: some.email@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello the_mighty_zork!\r\n\r\nYou are receiving this mail because you've requested an account on http://localhost:8080.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttp://localhost:8080/confirm_email?token=%s\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of http://localhost:8080\r\n\r\n", token) - suite.Equal(emailShould, email) - - // confirmationSentAt should be recent - suite.WithinDuration(time.Now(), user.ConfirmationSentAt, 1*time.Minute) -} - -func (suite *EmailConfirmTestSuite) TestConfirmEmail() { - ctx := context.Background() - - user := suite.testUsers["local_account_1"] - - // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 5 minutes ago - updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"} - user.UnconfirmedEmail = "some.email@example.org" - user.Email = "" - user.ConfirmedAt = time.Time{} - user.ConfirmationSentAt = time.Now().Add(-5 * time.Minute) - user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6" - - err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...) - suite.NoError(err) - - // confirm with the token set above - updatedUser, errWithCode := suite.user.ConfirmEmail(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6") - suite.NoError(errWithCode) - - // email should now be confirmed and token cleared - suite.Equal("some.email@example.org", updatedUser.Email) - suite.Empty(updatedUser.UnconfirmedEmail) - suite.Empty(updatedUser.ConfirmationToken) - suite.WithinDuration(updatedUser.ConfirmedAt, time.Now(), 1*time.Minute) - suite.WithinDuration(updatedUser.UpdatedAt, time.Now(), 1*time.Minute) -} - -func (suite *EmailConfirmTestSuite) TestConfirmEmailOldToken() { - ctx := context.Background() - - user := suite.testUsers["local_account_1"] - - // set a bunch of stuff on the user as though zork hasn't been confirmed yet, but has had an email sent 8 days ago - updatingColumns := []string{"unconfirmed_email", "email", "confirmed_at", "confirmation_sent_at", "confirmation_token"} - user.UnconfirmedEmail = "some.email@example.org" - user.Email = "" - user.ConfirmedAt = time.Time{} - user.ConfirmationSentAt = time.Now().Add(-192 * time.Hour) - user.ConfirmationToken = "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6" - - err := suite.db.UpdateByID(ctx, user, user.ID, updatingColumns...) - suite.NoError(err) - - // confirm with the token set above - updatedUser, errWithCode := suite.user.ConfirmEmail(ctx, "1d1aa44b-afa4-49c8-ac4b-eceb61715cc6") - suite.Nil(updatedUser) - suite.EqualError(errWithCode, "ConfirmEmail: confirmation token expired") -} - -func TestEmailConfirmTestSuite(t *testing.T) { - suite.Run(t, &EmailConfirmTestSuite{}) -} diff --git a/internal/processing/user/password.go b/internal/processing/user/password.go @@ -0,0 +1,52 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 user + +import ( + "context" + + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/validate" + "golang.org/x/crypto/bcrypt" +) + +// PasswordChange processes a password change request for the given user. +func (p *Processor) PasswordChange(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode { + if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(oldPassword)); err != nil { + return gtserror.NewErrorUnauthorized(err, "old password was incorrect") + } + + if err := validate.NewPassword(newPassword); err != nil { + return gtserror.NewErrorBadRequest(err, err.Error()) + } + + newPasswordHash, err := bcrypt.GenerateFromPassword([]byte(newPassword), bcrypt.DefaultCost) + if err != nil { + return gtserror.NewErrorInternalError(err, "error hashing password") + } + + user.EncryptedPassword = string(newPasswordHash) + + if err := p.db.UpdateUser(ctx, user, "encrypted_password"); err != nil { + return gtserror.NewErrorInternalError(err) + } + + return nil +} diff --git a/internal/processing/user/password_test.go b/internal/processing/user/password_test.go @@ -0,0 +1,92 @@ +/* + GoToSocial + Copyright (C) 2021-2023 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 user_test + +import ( + "context" + "net/http" + "testing" + + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "golang.org/x/crypto/bcrypt" +) + +type ChangePasswordTestSuite struct { + UserStandardTestSuite +} + +func (suite *ChangePasswordTestSuite) TestChangePasswordOK() { + user := suite.testUsers["local_account_1"] + + errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "verygoodnewpassword") + suite.NoError(errWithCode) + + err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte("verygoodnewpassword")) + suite.NoError(err) + + // get user from the db again + dbUser := >smodel.User{} + err = suite.db.GetByID(context.Background(), user.ID, dbUser) + suite.NoError(err) + + // check the password has changed + err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("verygoodnewpassword")) + suite.NoError(err) +} + +func (suite *ChangePasswordTestSuite) TestChangePasswordIncorrectOld() { + user := suite.testUsers["local_account_1"] + + errWithCode := suite.user.PasswordChange(context.Background(), user, "ooooopsydoooopsy", "verygoodnewpassword") + suite.EqualError(errWithCode, "crypto/bcrypt: hashedPassword is not the hash of the given password") + suite.Equal(http.StatusUnauthorized, errWithCode.Code()) + suite.Equal("Unauthorized: old password was incorrect", errWithCode.Safe()) + + // get user from the db again + dbUser := >smodel.User{} + err := suite.db.GetByID(context.Background(), user.ID, dbUser) + suite.NoError(err) + + // check the password has not changed + err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password")) + suite.NoError(err) +} + +func (suite *ChangePasswordTestSuite) TestChangePasswordWeakNew() { + user := suite.testUsers["local_account_1"] + + errWithCode := suite.user.PasswordChange(context.Background(), user, "password", "1234") + suite.EqualError(errWithCode, "password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password") + suite.Equal(http.StatusBadRequest, errWithCode.Code()) + suite.Equal("Bad Request: password is only 11% strength, try including more special characters, using lowercase letters, using uppercase letters or using a longer password", errWithCode.Safe()) + + // get user from the db again + dbUser := >smodel.User{} + err := suite.db.GetByID(context.Background(), user.ID, dbUser) + suite.NoError(err) + + // check the password has not changed + err = bcrypt.CompareHashAndPassword([]byte(dbUser.EncryptedPassword), []byte("password")) + suite.NoError(err) +} + +func TestChangePasswordTestSuite(t *testing.T) { + suite.Run(t, &ChangePasswordTestSuite{}) +} diff --git a/internal/processing/user/user.go b/internal/processing/user/user.go @@ -19,33 +19,18 @@ package user import ( - "context" - "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/email" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -// Processor wraps a bunch of functions for processing user-level actions. -type Processor interface { - // ChangePassword changes the specified user's password from old => new, - // or returns an error if the new password is too weak, or the old password is incorrect. - ChangePassword(ctx context.Context, user *gtsmodel.User, oldPassword string, newPassword string) gtserror.WithCode - // SendConfirmEmail sends a 'confirm-your-email-address' type email to a user. - SendConfirmEmail(ctx context.Context, user *gtsmodel.User, username string) error - // ConfirmEmail confirms an email address using the given token. - ConfirmEmail(ctx context.Context, token string) (*gtsmodel.User, gtserror.WithCode) -} - -type processor struct { +type Processor struct { emailSender email.Sender db db.DB } // New returns a new user processor func New(db db.DB, emailSender email.Sender) Processor { - return &processor{ + return Processor{ emailSender: emailSender, db: db, } diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go @@ -101,13 +101,6 @@ type TypeConverter interface { StatusToRSSItem(ctx context.Context, s *gtsmodel.Status) (*feeds.Item, error) /* - FRONTEND (api) MODEL TO INTERNAL (gts) MODEL - */ - - // APIVisToVis converts an API model visibility into its internal gts equivalent. - APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility - - /* ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL */ diff --git a/internal/typeutils/frontendtointernal.go b/internal/typeutils/frontendtointernal.go @@ -23,7 +23,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -func (c *converter) APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility { +func APIVisToVis(m apimodel.Visibility) gtsmodel.Visibility { switch m { case apimodel.VisibilityPublic: return gtsmodel.VisibilityPublic diff --git a/internal/web/confirmemail.go b/internal/web/confirmemail.go @@ -37,7 +37,7 @@ func (m *Module) confirmEmailGETHandler(c *gin.Context) { return } - user, errWithCode := m.processor.UserConfirmEmail(ctx, token) + user, errWithCode := m.processor.User().EmailConfirm(ctx, token) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/web/customcss.go b/internal/web/customcss.go @@ -51,7 +51,7 @@ func (m *Module) customCSSGETHandler(c *gin.Context) { return } - customCSS, errWithCode := m.processor.AccountGetCustomCSSForUsername(c.Request.Context(), username) + customCSS, errWithCode := m.processor.Account().GetCustomCSSForUsername(c.Request.Context(), username) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/web/profile.go b/internal/web/profile.go @@ -66,7 +66,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { return instance, nil } - account, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username) + account, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, username) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, instanceGet) return @@ -102,7 +102,7 @@ func (m *Module) profileGETHandler(c *gin.Context) { showBackToTop = true } - statusResp, errWithCode := m.processor.AccountWebStatusesGet(ctx, account.ID, maxStatusID) + statusResp, errWithCode := m.processor.Account().WebStatusesGet(ctx, account.ID, maxStatusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, instanceGet) return @@ -142,7 +142,7 @@ func (m *Module) returnAPProfile(ctx context.Context, c *gin.Context, username s ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) } - user, errWithCode := m.processor.GetFediUser(ctx, username, c.Request.URL) + user, errWithCode := m.processor.Fedi().UserGet(ctx, username, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) //nolint:contextcheck return diff --git a/internal/web/rss.go b/internal/web/rss.go @@ -99,7 +99,7 @@ func (m *Module) rssFeedGETHandler(c *gin.Context) { ifNoneMatch := c.Request.Header.Get(ifNoneMatchHeader) ifModifiedSince := extractIfModifiedSince(c.Request) - getRssFeed, accountLastPostedPublic, errWithCode := m.processor.AccountGetRSSFeedForUsername(ctx, username) + getRssFeed, accountLastPostedPublic, errWithCode := m.processor.Account().GetRSSFeedForUsername(ctx, username) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/web/thread.go b/internal/web/thread.go @@ -72,12 +72,12 @@ func (m *Module) threadGETHandler(c *gin.Context) { // do this check to make sure the status is actually from a local account, // we shouldn't render threads from statuses that don't belong to us! - if _, errWithCode := m.processor.AccountGetLocalByUsername(ctx, authed, username); errWithCode != nil { + if _, errWithCode := m.processor.Account().GetLocalByUsername(ctx, authed.Account, username); errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, instanceGet) return } - status, errWithCode := m.processor.StatusGet(ctx, authed, statusID) + status, errWithCode := m.processor.Status().Get(ctx, authed.Account, statusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, instanceGet) return @@ -97,7 +97,7 @@ func (m *Module) threadGETHandler(c *gin.Context) { return } - context, errWithCode := m.processor.StatusGetContext(ctx, authed, statusID) + context, errWithCode := m.processor.Status().ContextGet(ctx, authed.Account, statusID) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, instanceGet) return @@ -132,7 +132,7 @@ func (m *Module) returnAPStatus(ctx context.Context, c *gin.Context, username st ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) } - status, errWithCode := m.processor.GetFediStatus(ctx, username, statusID, c.Request.URL) + status, errWithCode := m.processor.Fedi().StatusGet(ctx, username, statusID, c.Request.URL) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) //nolint:contextcheck return diff --git a/internal/web/web.go b/internal/web/web.go @@ -61,12 +61,12 @@ const ( ) type Module struct { - processor processing.Processor + processor *processing.Processor eTagCache cache.Cache[string, eTagCacheEntry] isURIBlocked func(context.Context, *url.URL) (bool, db.Error) } -func New(db db.DB, processor processing.Processor) *Module { +func New(db db.DB, processor *processing.Processor) *Module { return &Module{ processor: processor, eTagCache: newETagCache(), diff --git a/testrig/processor.go b/testrig/processor.go @@ -30,6 +30,6 @@ import ( ) // NewTestProcessor returns a Processor suitable for testing purposes -func NewTestProcessor(db db.DB, storage *storage.Driver, federator federation.Federator, emailSender email.Sender, mediaManager media.Manager, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], fedWorker *concurrency.WorkerPool[messages.FromFederator]) processing.Processor { +func NewTestProcessor(db db.DB, storage *storage.Driver, federator federation.Federator, emailSender email.Sender, mediaManager media.Manager, clientWorker *concurrency.WorkerPool[messages.FromClientAPI], fedWorker *concurrency.WorkerPool[messages.FromFederator]) *processing.Processor { return processing.NewProcessor(NewTestTypeConverter(db), federator, NewTestOauthServer(db), mediaManager, storage, db, emailSender, clientWorker, fedWorker) }