summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2024-08-24 11:49:37 +0200
committerLibravatar GitHub <noreply@github.com>2024-08-24 11:49:37 +0200
commitf23f04e0b1d117be714bf91d5266dab219ed741e (patch)
tree0b3ddd60d51c8729949c3669993910a7f8f32a7b /internal
parent[performance] ffmpeg ffprobe wrapper improvements (#3225) (diff)
downloadgotosocial-f23f04e0b1d117be714bf91d5266dab219ed741e.tar.xz
[feature] Interaction requests client api + settings panel (#3215)
* [feature] Interaction requests client api + settings panel * test accept / reject * fmt * don't pin rejected interaction * use single db model for interaction accept, reject, and request * swaggor * env sharting * append errors * remove ErrNoEntries checks * change intReqID to reqID * rename "pend" to "request" * markIntsPending -> mark interactionsPending * use log instead of returning error when rejecting interaction * empty migration * jolly renaming * make interactionURI unique again * swag grr * remove unnecessary locks * invalidate as last step
Diffstat (limited to 'internal')
-rw-r--r--internal/api/activitypub/users/acceptget.go6
-rw-r--r--internal/api/client.go4
-rw-r--r--internal/api/client/interactionrequests/authorize.go104
-rw-r--r--internal/api/client/interactionrequests/get.go96
-rw-r--r--internal/api/client/interactionrequests/getpage.go211
-rw-r--r--internal/api/client/interactionrequests/interactionrequests.go50
-rw-r--r--internal/api/client/interactionrequests/reject.go104
-rw-r--r--internal/api/model/interaction.go46
-rw-r--r--internal/api/util/parsequery.go19
-rw-r--r--internal/cache/cache.go4
-rw-r--r--internal/cache/db.go23
-rw-r--r--internal/cache/size.go11
-rw-r--r--internal/config/config.go2
-rw-r--r--internal/config/defaults.go2
-rw-r--r--internal/config/helpers.gen.go26
-rw-r--r--internal/db/bundb/account.go22
-rw-r--r--internal/db/bundb/basic_test.go2
-rw-r--r--internal/db/bundb/bundb_test.go46
-rw-r--r--internal/db/bundb/instance.go3
-rw-r--r--internal/db/bundb/interaction.go289
-rw-r--r--internal/db/bundb/interaction_test.go261
-rw-r--r--internal/db/bundb/migrations/20240716151327_interaction_policy.go29
-rw-r--r--internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go154
-rw-r--r--internal/db/bundb/timeline.go38
-rw-r--r--internal/db/interaction.go46
-rw-r--r--internal/federation/federatingdb/accept.go4
-rw-r--r--internal/gtsmodel/interaction.go93
-rw-r--r--internal/gtsmodel/interactionapproval.go55
-rw-r--r--internal/processing/fedi/accept.go25
-rw-r--r--internal/processing/interactionrequests/accept.go239
-rw-r--r--internal/processing/interactionrequests/accept_test.go89
-rw-r--r--internal/processing/interactionrequests/get.go141
-rw-r--r--internal/processing/interactionrequests/interactionrequests.go47
-rw-r--r--internal/processing/interactionrequests/interactionrequests_test.go45
-rw-r--r--internal/processing/interactionrequests/reject.go133
-rw-r--r--internal/processing/interactionrequests/reject_test.go78
-rw-r--r--internal/processing/processor.go46
-rw-r--r--internal/processing/workers/federate.go92
-rw-r--r--internal/processing/workers/fromclientapi.go398
-rw-r--r--internal/processing/workers/fromclientapi_test.go72
-rw-r--r--internal/processing/workers/fromfediapi.go170
-rw-r--r--internal/processing/workers/fromfediapi_test.go36
-rw-r--r--internal/processing/workers/surfacenotify_test.go5
-rw-r--r--internal/processing/workers/util.go200
-rw-r--r--internal/processing/workers/workers.go4
-rw-r--r--internal/processing/workers/workers_test.go88
-rw-r--r--internal/timeline/prune_test.go8
-rw-r--r--internal/typeutils/internal.go78
-rw-r--r--internal/typeutils/internaltoas.go16
-rw-r--r--internal/typeutils/internaltoas_test.go14
-rw-r--r--internal/typeutils/internaltofrontend.go71
-rw-r--r--internal/uris/uri.go13
52 files changed, 3213 insertions, 645 deletions
diff --git a/internal/api/activitypub/users/acceptget.go b/internal/api/activitypub/users/acceptget.go
index c2b438330..4d0630df7 100644
--- a/internal/api/activitypub/users/acceptget.go
+++ b/internal/api/activitypub/users/acceptget.go
@@ -25,7 +25,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
)
-// AcceptGETHandler serves an interactionApproval as an ActivityStreams Accept.
+// AcceptGETHandler serves an interaction request as an ActivityStreams Accept.
func (m *Module) AcceptGETHandler(c *gin.Context) {
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
if errWithCode != nil {
@@ -33,7 +33,7 @@ func (m *Module) AcceptGETHandler(c *gin.Context) {
return
}
- acceptID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
@@ -45,7 +45,7 @@ func (m *Module) AcceptGETHandler(c *gin.Context) {
return
}
- resp, errWithCode := m.processor.Fedi().AcceptGet(c.Request.Context(), username, acceptID)
+ resp, errWithCode := m.processor.Fedi().AcceptGet(c.Request.Context(), username, reqID)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return
diff --git a/internal/api/client.go b/internal/api/client.go
index 65d4f29d5..77a63eb89 100644
--- a/internal/api/client.go
+++ b/internal/api/client.go
@@ -38,6 +38,7 @@ import (
importdata "github.com/superseriousbusiness/gotosocial/internal/api/client/import"
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
"github.com/superseriousbusiness/gotosocial/internal/api/client/interactionpolicies"
+ "github.com/superseriousbusiness/gotosocial/internal/api/client/interactionrequests"
"github.com/superseriousbusiness/gotosocial/internal/api/client/lists"
"github.com/superseriousbusiness/gotosocial/internal/api/client/markers"
"github.com/superseriousbusiness/gotosocial/internal/api/client/media"
@@ -80,6 +81,7 @@ type Client struct {
importData *importdata.Module // api/v1/import
instance *instance.Module // api/v1/instance
interactionPolicies *interactionpolicies.Module // api/v1/interaction_policies
+ interactionRequests *interactionrequests.Module // api/v1/interaction_requests
lists *lists.Module // api/v1/lists
markers *markers.Module // api/v1/markers
media *media.Module // api/v1/media, api/v2/media
@@ -130,6 +132,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) {
c.importData.Route(h)
c.instance.Route(h)
c.interactionPolicies.Route(h)
+ c.interactionRequests.Route(h)
c.lists.Route(h)
c.markers.Route(h)
c.media.Route(h)
@@ -168,6 +171,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client {
importData: importdata.New(p),
instance: instance.New(p),
interactionPolicies: interactionpolicies.New(p),
+ interactionRequests: interactionrequests.New(p),
lists: lists.New(p),
markers: markers.New(p),
media: media.New(p),
diff --git a/internal/api/client/interactionrequests/authorize.go b/internal/api/client/interactionrequests/authorize.go
new file mode 100644
index 000000000..1e5589f7e
--- /dev/null
+++ b/internal/api/client/interactionrequests/authorize.go
@@ -0,0 +1,104 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// InteractionRequestAuthorizePOSTHandler swagger:operation POST /api/v1/interaction_requests/{id}/authorize authorizeInteractionRequest
+//
+// Accept/authorize/approve an interaction request with the given ID.
+//
+// ---
+// tags:
+// - interaction_requests
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the interaction request targeting you.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - write:statuses
+//
+// responses:
+// '200':
+// name: Approval.
+// description: The now-approved interaction request.
+// schema:
+// "$ref": "#/definitions/interactionRequest"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) InteractionRequestAuthorizePOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiReq, errWithCode := m.processor.InteractionRequests().Accept(
+ c.Request.Context(),
+ authed.Account,
+ reqID,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, apiReq)
+}
diff --git a/internal/api/client/interactionrequests/get.go b/internal/api/client/interactionrequests/get.go
new file mode 100644
index 000000000..a354a8623
--- /dev/null
+++ b/internal/api/client/interactionrequests/get.go
@@ -0,0 +1,96 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// InteractionRequestGETHandler swagger:operation GET /api/v1/interaction_requests/{id} getInteractionRequest
+//
+// Get interaction request with the given ID.
+//
+// ---
+// tags:
+// - interaction_requests
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the interaction request targeting you.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - read:notifications
+//
+// responses:
+// '200':
+// description: Interaction request.
+// schema:
+// "$ref": "#/definitions/interactionRequest"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) InteractionRequestGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ intReq, errWithCode := m.processor.InteractionRequests().GetOne(
+ c.Request.Context(),
+ authed.Account,
+ id,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, intReq)
+}
diff --git a/internal/api/client/interactionrequests/getpage.go b/internal/api/client/interactionrequests/getpage.go
new file mode 100644
index 000000000..1978a055c
--- /dev/null
+++ b/internal/api/client/interactionrequests/getpage.go
@@ -0,0 +1,211 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+)
+
+// InteractionRequestsGETHandler swagger:operation GET /api/v1/interaction_requests getInteractionRequests
+//
+// Get an array of interactions requested on your statuses by other accounts, and pending your approval.
+//
+// ```
+// <https://example.org/api/v1/interaction_requests?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/interaction_requests?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
+// ````
+//
+// ---
+// tags:
+// - interaction_requests
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: status_id
+// type: string
+// description: >-
+// If set, then only interactions targeting the given status_id will be included in the results.
+// in: query
+// required: false
+// -
+// name: favourites
+// type: boolean
+// description: >-
+// If true or not set, pending favourites will be included in the results.
+// At least one of favourites, replies, and reblogs must be true.
+// in: query
+// required: false
+// default: true
+// -
+// name: replies
+// type: boolean
+// description: >-
+// If true or not set, pending replies will be included in the results.
+// At least one of favourites, replies, and reblogs must be true.
+// in: query
+// required: false
+// default: true
+// -
+// name: reblogs
+// type: boolean
+// description: >-
+// If true or not set, pending reblogs will be included in the results.
+// At least one of favourites, replies, and reblogs must be true.
+// in: query
+// required: false
+// default: true
+// -
+// name: max_id
+// type: string
+// description: >-
+// Return only interaction requests *OLDER* than the given max ID.
+// The interaction with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: since_id
+// type: string
+// description: >-
+// Return only interaction requests *NEWER* than the given since ID.
+// The interaction with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: min_id
+// type: string
+// description: >-
+// Return only interaction requests *IMMEDIATELY NEWER* than the given min ID.
+// The interaction with the specified ID will not be included in the response.
+// in: query
+// required: false
+// -
+// name: limit
+// type: integer
+// description: Number of interaction requests to return.
+// default: 40
+// minimum: 1
+// maximum: 80
+// in: query
+// required: false
+//
+// security:
+// - OAuth2 Bearer:
+// - read:notifications
+//
+// responses:
+// '200':
+// headers:
+// Link:
+// type: string
+// description: Links to the next and previous queries.
+// schema:
+// type: array
+// items:
+// "$ref": "#/definitions/interactionRequest"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) InteractionRequestsGETHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
+ return
+ }
+
+ includeLikes, errWithCode := apiutil.ParseInteractionFavourites(
+ c.Query(apiutil.InteractionFavouritesKey), true,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ includeReplies, errWithCode := apiutil.ParseInteractionReplies(
+ c.Query(apiutil.InteractionRepliesKey), true,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ includeBoosts, errWithCode := apiutil.ParseInteractionReblogs(
+ c.Query(apiutil.InteractionReblogsKey), true,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if !includeLikes && !includeReplies && !includeBoosts {
+ const text = "at least one of favourites, replies, or boosts must be true"
+ errWithCode := gtserror.NewErrorBadRequest(errors.New(text), text)
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ page, errWithCode := paging.ParseIDPage(c,
+ 1, // min limit
+ 80, // max limit
+ 40, // default limit
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ resp, errWithCode := m.processor.InteractionRequests().GetPage(
+ c.Request.Context(),
+ authed.Account,
+ c.Query(apiutil.InteractionStatusIDKey),
+ includeLikes,
+ includeReplies,
+ includeBoosts,
+ page,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if resp.LinkHeader != "" {
+ c.Header("Link", resp.LinkHeader)
+ }
+
+ apiutil.JSON(c, http.StatusOK, resp.Items)
+}
diff --git a/internal/api/client/interactionrequests/interactionrequests.go b/internal/api/client/interactionrequests/interactionrequests.go
new file mode 100644
index 000000000..172951817
--- /dev/null
+++ b/internal/api/client/interactionrequests/interactionrequests.go
@@ -0,0 +1,50 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/processing"
+)
+
+const (
+ BasePath = "/v1/interaction_requests"
+ BasePathWithID = BasePath + "/:" + apiutil.IDKey
+ AuthorizePath = BasePathWithID + "/authorize"
+ RejectPath = BasePathWithID + "/reject"
+)
+
+type Module struct {
+ processor *processing.Processor
+}
+
+func New(processor *processing.Processor) *Module {
+ return &Module{
+ processor: processor,
+ }
+}
+
+func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
+ attachHandler(http.MethodGet, BasePath, m.InteractionRequestsGETHandler)
+ attachHandler(http.MethodGet, BasePathWithID, m.InteractionRequestGETHandler)
+ attachHandler(http.MethodPost, AuthorizePath, m.InteractionRequestAuthorizePOSTHandler)
+ attachHandler(http.MethodPost, RejectPath, m.InteractionRequestRejectPOSTHandler)
+}
diff --git a/internal/api/client/interactionrequests/reject.go b/internal/api/client/interactionrequests/reject.go
new file mode 100644
index 000000000..33c426462
--- /dev/null
+++ b/internal/api/client/interactionrequests/reject.go
@@ -0,0 +1,104 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+)
+
+// InteractionRequestRejectPOSTHandler swagger:operation POST /api/v1/interaction_requests/{id}/reject rejectInteractionRequest
+//
+// Reject an interaction request with the given ID.
+//
+// ---
+// tags:
+// - interaction_requests
+//
+// produces:
+// - application/json
+//
+// parameters:
+// -
+// name: id
+// type: string
+// description: ID of the interaction request targeting you.
+// in: path
+// required: true
+//
+// security:
+// - OAuth2 Bearer:
+// - write:statuses
+//
+// responses:
+// '200':
+// name: Rejection.
+// description: The now-rejected interaction request.
+// schema:
+// "$ref": "#/definitions/interactionRequest"
+// '400':
+// description: bad request
+// '401':
+// description: unauthorized
+// '404':
+// description: not found
+// '406':
+// description: not acceptable
+// '500':
+// description: internal server error
+func (m *Module) InteractionRequestRejectPOSTHandler(c *gin.Context) {
+ authed, err := oauth.Authed(c, true, true, true, true)
+ if err != nil {
+ errWithCode := gtserror.NewErrorUnauthorized(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ if authed.Account.IsMoving() {
+ apiutil.ForbiddenAfterMove(c)
+ return
+ }
+
+ if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
+ errWithCode := gtserror.NewErrorNotAcceptable(err, err.Error())
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiReq, errWithCode := m.processor.InteractionRequests().Reject(
+ c.Request.Context(),
+ authed.Account,
+ reqID,
+ )
+ if errWithCode != nil {
+ apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
+ return
+ }
+
+ apiutil.JSON(c, http.StatusOK, apiReq)
+}
diff --git a/internal/api/model/interaction.go b/internal/api/model/interaction.go
new file mode 100644
index 000000000..b0543ce6b
--- /dev/null
+++ b/internal/api/model/interaction.go
@@ -0,0 +1,46 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package model
+
+// InteractionRequest represents a pending, approved, or rejected interaction of type favourite, reply, or reblog.
+//
+// swagger:model interactionRequest
+type InteractionRequest struct {
+ // The id of the interaction request in the database.
+ ID string `json:"id"`
+ // The type of interaction that this interaction request pertains to.
+ //
+ // `favourite` - Someone favourited a status.
+ // `reply` - Someone replied to a status.
+ // `reblog` - Someone reblogged / boosted a status.
+ Type string `json:"type"`
+ // The timestamp of the interaction request (ISO 8601 Datetime)
+ CreatedAt string `json:"created_at"`
+ // The account that performed the interaction.
+ Account *Account `json:"account"`
+ // Status targeted by the requested interaction.
+ Status *Status `json:"status"`
+ // If type=reply, this field will be set to the reply that is awaiting approval. If type=favourite, or type=reblog, the field will be omitted.
+ Reply *Status `json:"reply,omitempty"`
+ // The timestamp that the interaction request was accepted (ISO 8601 Datetime). Field omitted if request not accepted (yet).
+ AcceptedAt string `json:"accepted_at,omitempty"`
+ // The timestamp that the interaction request was rejected (ISO 8601 Datetime). Field omitted if request not rejected (yet).
+ RejectedAt string `json:"rejected_at,omitempty"`
+ // URI of the Accept or Reject. Only set if accepted_at or rejected_at is set, else omitted.
+ URI string `json:"uri,omitempty"`
+}
diff --git a/internal/api/util/parsequery.go b/internal/api/util/parsequery.go
index 90cc30e6f..024ec028b 100644
--- a/internal/api/util/parsequery.go
+++ b/internal/api/util/parsequery.go
@@ -91,6 +91,13 @@ const (
AdminPermissionsKey = "permissions"
AdminRoleIDsKey = "role_ids[]"
AdminInvitedByKey = "invited_by"
+
+ /* Interaction policy + request keys */
+
+ InteractionStatusIDKey = "status_id"
+ InteractionFavouritesKey = "favourites"
+ InteractionRepliesKey = "replies"
+ InteractionReblogsKey = "reblogs"
)
/*
@@ -194,6 +201,18 @@ func ParseAdminStaff(value string, defaultValue bool) (bool, gtserror.WithCode)
return parseBool(value, defaultValue, AdminStaffKey)
}
+func ParseInteractionFavourites(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ return parseBool(value, defaultValue, InteractionFavouritesKey)
+}
+
+func ParseInteractionReplies(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ return parseBool(value, defaultValue, InteractionRepliesKey)
+}
+
+func ParseInteractionReblogs(value string, defaultValue bool) (bool, gtserror.WithCode) {
+ return parseBool(value, defaultValue, InteractionReblogsKey)
+}
+
/*
Parse functions for *REQUIRED* parameters.
*/
diff --git a/internal/cache/cache.go b/internal/cache/cache.go
index 2949d528a..f1c382d11 100644
--- a/internal/cache/cache.go
+++ b/internal/cache/cache.go
@@ -81,7 +81,7 @@ func (c *Caches) Init() {
c.initFollowRequestIDs()
c.initInReplyToIDs()
c.initInstance()
- c.initInteractionApproval()
+ c.initInteractionRequest()
c.initList()
c.initListEntry()
c.initMarker()
@@ -158,7 +158,7 @@ func (c *Caches) Sweep(threshold float64) {
c.DB.FollowRequestIDs.Trim(threshold)
c.DB.InReplyToIDs.Trim(threshold)
c.DB.Instance.Trim(threshold)
- c.DB.InteractionApproval.Trim(threshold)
+ c.DB.InteractionRequest.Trim(threshold)
c.DB.List.Trim(threshold)
c.DB.ListEntry.Trim(threshold)
c.DB.Marker.Trim(threshold)
diff --git a/internal/cache/db.go b/internal/cache/db.go
index c1b87ef96..5e86c92a2 100644
--- a/internal/cache/db.go
+++ b/internal/cache/db.go
@@ -106,8 +106,8 @@ type DBCaches struct {
// Instance provides access to the gtsmodel Instance database cache.
Instance StructCache[*gtsmodel.Instance]
- // InteractionApproval provides access to the gtsmodel InteractionApproval database cache.
- InteractionApproval StructCache[*gtsmodel.InteractionApproval]
+ // InteractionRequest provides access to the gtsmodel InteractionRequest database cache.
+ InteractionRequest StructCache[*gtsmodel.InteractionRequest]
// InReplyToIDs provides access to the status in reply to IDs list database cache.
InReplyToIDs SliceCache[string]
@@ -802,31 +802,36 @@ func (c *Caches) initInstance() {
})
}
-func (c *Caches) initInteractionApproval() {
+func (c *Caches) initInteractionRequest() {
// Calculate maximum cache size.
cap := calculateResultCacheMax(
- sizeofInteractionApproval(),
- config.GetCacheInteractionApprovalMemRatio(),
+ sizeofInteractionRequest(),
+ config.GetCacheInteractionRequestMemRatio(),
)
log.Infof(nil, "cache size = %d", cap)
- copyF := func(i1 *gtsmodel.InteractionApproval) *gtsmodel.InteractionApproval {
- i2 := new(gtsmodel.InteractionApproval)
+ copyF := func(i1 *gtsmodel.InteractionRequest) *gtsmodel.InteractionRequest {
+ i2 := new(gtsmodel.InteractionRequest)
*i2 = *i1
// Don't include ptr fields that
// will be populated separately.
// See internal/db/bundb/interaction.go.
- i2.Account = nil
+ i2.Status = nil
+ i2.TargetAccount = nil
i2.InteractingAccount = nil
+ i2.Like = nil
+ i2.Reply = nil
+ i2.Announce = nil
return i2
}
- c.DB.InteractionApproval.Init(structr.CacheConfig[*gtsmodel.InteractionApproval]{
+ c.DB.InteractionRequest.Init(structr.CacheConfig[*gtsmodel.InteractionRequest]{
Indices: []structr.IndexConfig{
{Fields: "ID"},
+ {Fields: "InteractionURI"},
{Fields: "URI"},
},
MaxSize: cap,
diff --git a/internal/cache/size.go b/internal/cache/size.go
index 4c474fa28..29ab77fbf 100644
--- a/internal/cache/size.go
+++ b/internal/cache/size.go
@@ -190,7 +190,7 @@ func totalOfRatios() float64 {
config.GetCacheFollowRequestMemRatio() +
config.GetCacheFollowRequestIDsMemRatio() +
config.GetCacheInstanceMemRatio() +
- config.GetCacheInteractionApprovalMemRatio() +
+ config.GetCacheInteractionRequestMemRatio() +
config.GetCacheInReplyToIDsMemRatio() +
config.GetCacheListMemRatio() +
config.GetCacheListEntryMemRatio() +
@@ -441,16 +441,17 @@ func sizeofInstance() uintptr {
}))
}
-func sizeofInteractionApproval() uintptr {
- return uintptr(size.Of(&gtsmodel.InteractionApproval{
+func sizeofInteractionRequest() uintptr {
+ return uintptr(size.Of(&gtsmodel.InteractionRequest{
ID: exampleID,
CreatedAt: exampleTime,
- UpdatedAt: exampleTime,
- AccountID: exampleID,
+ StatusID: exampleID,
+ TargetAccountID: exampleID,
InteractingAccountID: exampleID,
InteractionURI: exampleURI,
InteractionType: gtsmodel.InteractionAnnounce,
URI: exampleURI,
+ AcceptedAt: exampleTime,
}))
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 4bb9be0c4..d6b8f0a54 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -218,7 +218,7 @@ type CacheConfiguration struct {
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
InstanceMemRatio float64 `name:"instance-mem-ratio"`
- InteractionApprovalMemRatio float64 `name:"interaction-approval-mem-ratio"`
+ InteractionRequestMemRatio float64 `name:"interaction-request-mem-ratio"`
ListMemRatio float64 `name:"list-mem-ratio"`
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
MarkerMemRatio float64 `name:"marker-mem-ratio"`
diff --git a/internal/config/defaults.go b/internal/config/defaults.go
index 3ef988f06..e71711cb3 100644
--- a/internal/config/defaults.go
+++ b/internal/config/defaults.go
@@ -181,7 +181,7 @@ var Defaults = Configuration{
FollowRequestIDsMemRatio: 2,
InReplyToIDsMemRatio: 3,
InstanceMemRatio: 1,
- InteractionApprovalMemRatio: 1,
+ InteractionRequestMemRatio: 1,
ListMemRatio: 1,
ListEntryMemRatio: 2,
MarkerMemRatio: 0.5,
diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go
index f176676e5..d19e4e241 100644
--- a/internal/config/helpers.gen.go
+++ b/internal/config/helpers.gen.go
@@ -3412,32 +3412,30 @@ func GetCacheInstanceMemRatio() float64 { return global.GetCacheInstanceMemRatio
// SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field
func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) }
-// GetCacheInteractionApprovalMemRatio safely fetches the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
-func (st *ConfigState) GetCacheInteractionApprovalMemRatio() (v float64) {
+// GetCacheInteractionRequestMemRatio safely fetches the Configuration value for state's 'Cache.InteractionRequestMemRatio' field
+func (st *ConfigState) GetCacheInteractionRequestMemRatio() (v float64) {
st.mutex.RLock()
- v = st.config.Cache.InteractionApprovalMemRatio
+ v = st.config.Cache.InteractionRequestMemRatio
st.mutex.RUnlock()
return
}
-// SetCacheInteractionApprovalMemRatio safely sets the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
-func (st *ConfigState) SetCacheInteractionApprovalMemRatio(v float64) {
+// SetCacheInteractionRequestMemRatio safely sets the Configuration value for state's 'Cache.InteractionRequestMemRatio' field
+func (st *ConfigState) SetCacheInteractionRequestMemRatio(v float64) {
st.mutex.Lock()
defer st.mutex.Unlock()
- st.config.Cache.InteractionApprovalMemRatio = v
+ st.config.Cache.InteractionRequestMemRatio = v
st.reloadToViper()
}
-// CacheInteractionApprovalMemRatioFlag returns the flag name for the 'Cache.InteractionApprovalMemRatio' field
-func CacheInteractionApprovalMemRatioFlag() string { return "cache-interaction-approval-mem-ratio" }
+// CacheInteractionRequestMemRatioFlag returns the flag name for the 'Cache.InteractionRequestMemRatio' field
+func CacheInteractionRequestMemRatioFlag() string { return "cache-interaction-request-mem-ratio" }
-// GetCacheInteractionApprovalMemRatio safely fetches the value for global configuration 'Cache.InteractionApprovalMemRatio' field
-func GetCacheInteractionApprovalMemRatio() float64 {
- return global.GetCacheInteractionApprovalMemRatio()
-}
+// GetCacheInteractionRequestMemRatio safely fetches the value for global configuration 'Cache.InteractionRequestMemRatio' field
+func GetCacheInteractionRequestMemRatio() float64 { return global.GetCacheInteractionRequestMemRatio() }
-// SetCacheInteractionApprovalMemRatio safely sets the value for global configuration 'Cache.InteractionApprovalMemRatio' field
-func SetCacheInteractionApprovalMemRatio(v float64) { global.SetCacheInteractionApprovalMemRatio(v) }
+// SetCacheInteractionRequestMemRatio safely sets the value for global configuration 'Cache.InteractionRequestMemRatio' field
+func SetCacheInteractionRequestMemRatio(v float64) { global.SetCacheInteractionRequestMemRatio(v) }
// GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field
func (st *ConfigState) GetCacheListMemRatio() (v float64) {
diff --git a/internal/db/bundb/account.go b/internal/db/bundb/account.go
index 94fd054bf..b57dcb57b 100644
--- a/internal/db/bundb/account.go
+++ b/internal/db/bundb/account.go
@@ -1285,34 +1285,40 @@ func (a *accountDB) RegenerateAccountStats(ctx context.Context, account *gtsmode
if err := a.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
var err error
- // Scan database for account statuses.
+ // Scan database for account statuses, ignoring
+ // statuses that are currently pending approval.
statusesCount, err := tx.NewSelect().
- Table("statuses").
- Where("? = ?", bun.Ident("account_id"), account.ID).
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Count(ctx)
if err != nil {
return err
}
stats.StatusesCount = &statusesCount
- // Scan database for pinned statuses.
+ // Scan database for pinned statuses, ignoring
+ // statuses that are currently pending approval.
statusesPinnedCount, err := tx.NewSelect().
- Table("statuses").
- Where("? = ?", bun.Ident("account_id"), account.ID).
- Where("? IS NOT NULL", bun.Ident("pinned_at")).
+ TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
+ Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Count(ctx)
if err != nil {
return err
}
stats.StatusesPinnedCount = &statusesPinnedCount
- // Scan database for last status.
+ // Scan database for last status, ignoring
+ // statuses that are currently pending approval.
lastStatusAt := time.Time{}
err = tx.
NewSelect().
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
Column("status.created_at").
Where("? = ?", bun.Ident("status.account_id"), account.ID).
+ Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
Order("status.id DESC").
Limit(1).
Scan(ctx, &lastStatusAt)
diff --git a/internal/db/bundb/basic_test.go b/internal/db/bundb/basic_test.go
index f6647c1f5..56159dc25 100644
--- a/internal/db/bundb/basic_test.go
+++ b/internal/db/bundb/basic_test.go
@@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
s := []*gtsmodel.Status{}
err := suite.db.GetAll(context.Background(), &s)
suite.NoError(err)
- suite.Len(s, 24)
+ suite.Len(s, 25)
}
func (suite *BasicTestSuite) TestGetAllNotNull() {
diff --git a/internal/db/bundb/bundb_test.go b/internal/db/bundb/bundb_test.go
index adfd605a5..e976199e4 100644
--- a/internal/db/bundb/bundb_test.go
+++ b/internal/db/bundb/bundb_test.go
@@ -34,28 +34,29 @@ type BunDBStandardTestSuite struct {
state state.State
// standard suite models
- testTokens map[string]*gtsmodel.Token
- testClients map[string]*gtsmodel.Client
- testApplications map[string]*gtsmodel.Application
- testUsers map[string]*gtsmodel.User
- testAccounts map[string]*gtsmodel.Account
- testAttachments map[string]*gtsmodel.MediaAttachment
- testStatuses map[string]*gtsmodel.Status
- testTags map[string]*gtsmodel.Tag
- testMentions map[string]*gtsmodel.Mention
- testFollows map[string]*gtsmodel.Follow
- testEmojis map[string]*gtsmodel.Emoji
- testReports map[string]*gtsmodel.Report
- testBookmarks map[string]*gtsmodel.StatusBookmark
- testFaves map[string]*gtsmodel.StatusFave
- testLists map[string]*gtsmodel.List
- testListEntries map[string]*gtsmodel.ListEntry
- testAccountNotes map[string]*gtsmodel.AccountNote
- testMarkers map[string]*gtsmodel.Marker
- testRules map[string]*gtsmodel.Rule
- testThreads map[string]*gtsmodel.Thread
- testPolls map[string]*gtsmodel.Poll
- testPollVotes map[string]*gtsmodel.PollVote
+ testTokens map[string]*gtsmodel.Token
+ testClients map[string]*gtsmodel.Client
+ testApplications map[string]*gtsmodel.Application
+ testUsers map[string]*gtsmodel.User
+ testAccounts map[string]*gtsmodel.Account
+ testAttachments map[string]*gtsmodel.MediaAttachment
+ testStatuses map[string]*gtsmodel.Status
+ testTags map[string]*gtsmodel.Tag
+ testMentions map[string]*gtsmodel.Mention
+ testFollows map[string]*gtsmodel.Follow
+ testEmojis map[string]*gtsmodel.Emoji
+ testReports map[string]*gtsmodel.Report
+ testBookmarks map[string]*gtsmodel.StatusBookmark
+ testFaves map[string]*gtsmodel.StatusFave
+ testLists map[string]*gtsmodel.List
+ testListEntries map[string]*gtsmodel.ListEntry
+ testAccountNotes map[string]*gtsmodel.AccountNote
+ testMarkers map[string]*gtsmodel.Marker
+ testRules map[string]*gtsmodel.Rule
+ testThreads map[string]*gtsmodel.Thread
+ testPolls map[string]*gtsmodel.Poll
+ testPollVotes map[string]*gtsmodel.PollVote
+ testInteractionRequests map[string]*gtsmodel.InteractionRequest
}
func (suite *BunDBStandardTestSuite) SetupSuite() {
@@ -81,6 +82,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
suite.testThreads = testrig.NewTestThreads()
suite.testPolls = testrig.NewTestPolls()
suite.testPollVotes = testrig.NewTestPollVotes()
+ suite.testInteractionRequests = testrig.NewTestInteractionRequests()
}
func (suite *BunDBStandardTestSuite) SetupTest() {
diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go
index 6d98c8db7..008b6c8f3 100644
--- a/internal/db/bundb/instance.go
+++ b/internal/db/bundb/instance.go
@@ -76,6 +76,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
Where("? = ?", bun.Ident("account.domain"), domain)
}
+ // Ignore statuses that are currently pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
count, err := q.Count(ctx)
if err != nil {
return 0, err
diff --git a/internal/db/bundb/interaction.go b/internal/db/bundb/interaction.go
index 2ded70311..78abcc763 100644
--- a/internal/db/bundb/interaction.go
+++ b/internal/db/bundb/interaction.go
@@ -19,10 +19,14 @@ package bundb
import (
"context"
+ "errors"
+ "slices"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/uptrace/bun"
)
@@ -32,56 +36,70 @@ type interactionDB struct {
state *state.State
}
-func (r *interactionDB) newInteractionApprovalQ(approval interface{}) *bun.SelectQuery {
- return r.db.
+func (i *interactionDB) newInteractionRequestQ(request interface{}) *bun.SelectQuery {
+ return i.db.
NewSelect().
- Model(approval)
+ Model(request)
}
-func (r *interactionDB) GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) {
- return r.getInteractionApproval(
+func (i *interactionDB) GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
ctx,
"ID",
- func(approval *gtsmodel.InteractionApproval) error {
- return r.
- newInteractionApprovalQ(approval).
- Where("? = ?", bun.Ident("interaction_approval.id"), id).
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.id"), id).
Scan(ctx)
},
id,
)
}
-func (r *interactionDB) GetInteractionApprovalByURI(ctx context.Context, uri string) (*gtsmodel.InteractionApproval, error) {
- return r.getInteractionApproval(
+func (i *interactionDB) GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
+ ctx,
+ "InteractionURI",
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.interaction_uri"), uri).
+ Scan(ctx)
+ },
+ uri,
+ )
+}
+
+func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
+ return i.getInteractionRequest(
ctx,
"URI",
- func(approval *gtsmodel.InteractionApproval) error {
- return r.
- newInteractionApprovalQ(approval).
- Where("? = ?", bun.Ident("interaction_approval.uri"), uri).
+ func(request *gtsmodel.InteractionRequest) error {
+ return i.
+ newInteractionRequestQ(request).
+ Where("? = ?", bun.Ident("interaction_request.uri"), uri).
Scan(ctx)
},
uri,
)
}
-func (r *interactionDB) getInteractionApproval(
+func (i *interactionDB) getInteractionRequest(
ctx context.Context,
lookup string,
- dbQuery func(*gtsmodel.InteractionApproval) error,
+ dbQuery func(*gtsmodel.InteractionRequest) error,
keyParts ...any,
-) (*gtsmodel.InteractionApproval, error) {
- // Fetch approval from database cache with loader callback
- approval, err := r.state.Caches.DB.InteractionApproval.LoadOne(lookup, func() (*gtsmodel.InteractionApproval, error) {
- var approval gtsmodel.InteractionApproval
+) (*gtsmodel.InteractionRequest, error) {
+ // Fetch request from database cache with loader callback
+ request, err := i.state.Caches.DB.InteractionRequest.LoadOne(lookup, func() (*gtsmodel.InteractionRequest, error) {
+ var request gtsmodel.InteractionRequest
// Not cached! Perform database query
- if err := dbQuery(&approval); err != nil {
+ if err := dbQuery(&request); err != nil {
return nil, err
}
- return &approval, nil
+ return &request, nil
}, keyParts...)
if err != nil {
// Error already processed.
@@ -90,60 +108,241 @@ func (r *interactionDB) getInteractionApproval(
if gtscontext.Barebones(ctx) {
// Only a barebones model was requested.
- return approval, nil
+ return request, nil
}
- if err := r.PopulateInteractionApproval(ctx, approval); err != nil {
+ if err := i.PopulateInteractionRequest(ctx, request); err != nil {
return nil, err
}
- return approval, nil
+ return request, nil
}
-func (r *interactionDB) PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error {
+func (i *interactionDB) PopulateInteractionRequest(ctx context.Context, req *gtsmodel.InteractionRequest) error {
var (
err error
- errs = gtserror.NewMultiError(2)
+ errs = gtserror.NewMultiError(4)
)
- if approval.Account == nil {
- // Account is not set, fetch from the database.
- approval.Account, err = r.state.DB.GetAccountByID(
+ if req.Status == nil {
+ // Target status is not set, fetch from the database.
+ req.Status, err = i.state.DB.GetStatusByID(
+ gtscontext.SetBarebones(ctx),
+ req.StatusID,
+ )
+ if err != nil {
+ errs.Appendf("error populating interactionRequest target: %w", err)
+ }
+ }
+
+ if req.TargetAccount == nil {
+ // Target account is not set, fetch from the database.
+ req.TargetAccount, err = i.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
- approval.AccountID,
+ req.TargetAccountID,
)
if err != nil {
- errs.Appendf("error populating interactionApproval account: %w", err)
+ errs.Appendf("error populating interactionRequest target account: %w", err)
}
}
- if approval.InteractingAccount == nil {
+ if req.InteractingAccount == nil {
// InteractingAccount is not set, fetch from the database.
- approval.InteractingAccount, err = r.state.DB.GetAccountByID(
+ req.InteractingAccount, err = i.state.DB.GetAccountByID(
gtscontext.SetBarebones(ctx),
- approval.InteractingAccountID,
+ req.InteractingAccountID,
)
if err != nil {
- errs.Appendf("error populating interactionApproval interacting account: %w", err)
+ errs.Appendf("error populating interactionRequest interacting account: %w", err)
+ }
+ }
+
+ // Depending on the interaction type, *try* to populate
+ // the related model, but don't error if this is not
+ // possible, as it may have just already been deleted
+ // by its owner and we haven't cleaned up yet.
+ switch req.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ req.Like, err = i.state.DB.GetStatusFaveByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Like: %w", err)
+ }
+
+ case gtsmodel.InteractionReply:
+ req.Reply, err = i.state.DB.GetStatusByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Reply: %w", err)
+ }
+
+ case gtsmodel.InteractionAnnounce:
+ req.Announce, err = i.state.DB.GetStatusByURI(ctx, req.InteractionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ errs.Appendf("error populating interactionRequest Announce: %w", err)
}
}
return errs.Combine()
}
-func (r *interactionDB) PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error {
- return r.state.Caches.DB.InteractionApproval.Store(approval, func() error {
- _, err := r.db.NewInsert().Model(approval).Exec(ctx)
+func (i *interactionDB) PutInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error {
+ return i.state.Caches.DB.InteractionRequest.Store(request, func() error {
+ _, err := i.db.NewInsert().Model(request).Exec(ctx)
return err
})
}
-func (r *interactionDB) DeleteInteractionApprovalByID(ctx context.Context, id string) error {
- defer r.state.Caches.DB.InteractionApproval.Invalidate("ID", id)
+func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest, columns ...string) error {
+ return i.state.Caches.DB.InteractionRequest.Store(request, func() error {
+ _, err := i.db.
+ NewUpdate().
+ Model(request).
+ Where("? = ?", bun.Ident("interaction_request.id"), request.ID).
+ Column(columns...).
+ Exec(ctx)
+ return err
+ })
+}
+
+func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
+ defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
- _, err := r.db.NewDelete().
- TableExpr("? AS ?", bun.Ident("interaction_approvals"), bun.Ident("interaction_approval")).
- Where("? = ?", bun.Ident("interaction_approval.id"), id).
+ _, err := i.db.NewDelete().
+ TableExpr("? AS ?", bun.Ident("interaction_requests"), bun.Ident("interaction_request")).
+ Where("? = ?", bun.Ident("interaction_request.id"), id).
Exec(ctx)
return err
}
+
+func (i *interactionDB) GetInteractionsRequestsForAcct(
+ ctx context.Context,
+ acctID string,
+ statusID string,
+ likes bool,
+ replies bool,
+ boosts bool,
+ page *paging.Page,
+) ([]*gtsmodel.InteractionRequest, error) {
+ if !likes && !replies && !boosts {
+ return nil, gtserror.New("at least one of likes, replies, or boosts must be true")
+ }
+
+ var (
+ // Get paging params.
+ minID = page.GetMin()
+ maxID = page.GetMax()
+ limit = page.GetLimit()
+ order = page.GetOrder()
+
+ // Make educated guess for slice size
+ reqIDs = make([]string, 0, limit)
+ )
+
+ // Create the basic select query.
+ q := i.db.
+ NewSelect().
+ Column("id").
+ TableExpr(
+ "? AS ?",
+ bun.Ident("interaction_requests"),
+ bun.Ident("interaction_request"),
+ ).
+ // Select only interaction requests that
+ // are neither accepted or rejected yet,
+ // ie., without an Accept or Reject URI.
+ Where("? IS NULL", bun.Ident("uri"))
+
+ // Select interactions targeting status.
+ if statusID != "" {
+ q = q.Where("? = ?", bun.Ident("status_id"), statusID)
+ }
+
+ // Select interactions targeting account.
+ if acctID != "" {
+ q = q.Where("? = ?", bun.Ident("target_account_id"), acctID)
+ }
+
+ // Figure out which types of interaction are
+ // being sought, and add them to the query.
+ wantTypes := make([]gtsmodel.InteractionType, 0, 3)
+ if likes {
+ wantTypes = append(wantTypes, gtsmodel.InteractionLike)
+ }
+ if replies {
+ wantTypes = append(wantTypes, gtsmodel.InteractionReply)
+ }
+ if boosts {
+ wantTypes = append(wantTypes, gtsmodel.InteractionAnnounce)
+ }
+ q = q.Where("? IN (?)", bun.Ident("interaction_type"), bun.In(wantTypes))
+
+ // Add paging param max ID.
+ if maxID != "" {
+ q = q.Where("? < ?", bun.Ident("id"), maxID)
+ }
+
+ // Add paging param min ID.
+ if minID != "" {
+ q = q.Where("? > ?", bun.Ident("id"), minID)
+ }
+
+ // Add paging param order.
+ if order == paging.OrderAscending {
+ // Page up.
+ q = q.OrderExpr("? ASC", bun.Ident("id"))
+ } else {
+ // Page down.
+ q = q.OrderExpr("? DESC", bun.Ident("id"))
+ }
+
+ // Add paging param limit.
+ if limit > 0 {
+ q = q.Limit(limit)
+ }
+
+ // Execute the query and scan into IDs.
+ err := q.Scan(ctx, &reqIDs)
+ if err != nil {
+ return nil, err
+ }
+
+ // Catch case of no items early
+ if len(reqIDs) == 0 {
+ return nil, db.ErrNoEntries
+ }
+
+ // If we're paging up, we still want interactions
+ // to be sorted by ID desc, so reverse ids slice.
+ if order == paging.OrderAscending {
+ slices.Reverse(reqIDs)
+ }
+
+ // For each interaction request ID,
+ // select the interaction request.
+ reqs := make([]*gtsmodel.InteractionRequest, 0, len(reqIDs))
+ for _, id := range reqIDs {
+ req, err := i.GetInteractionRequestByID(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ reqs = append(reqs, req)
+ }
+
+ return reqs, nil
+}
+
+func (i *interactionDB) IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error) {
+ req, err := i.GetInteractionRequestByInteractionURI(ctx, interactionURI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return false, gtserror.Newf("db error getting interaction request: %w", err)
+ }
+
+ if req == nil {
+ // No interaction req at all with this
+ // interactionURI so it can't be rejected.
+ return false, nil
+ }
+
+ return req.IsRejected(), nil
+}
diff --git a/internal/db/bundb/interaction_test.go b/internal/db/bundb/interaction_test.go
new file mode 100644
index 000000000..37684f18c
--- /dev/null
+++ b/internal/db/bundb/interaction_test.go
@@ -0,0 +1,261 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package bundb_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+type InteractionTestSuite struct {
+ BunDBStandardTestSuite
+}
+
+func (suite *InteractionTestSuite) markInteractionsPending(
+ ctx context.Context,
+ statusID string,
+) (pendingCount int) {
+ // Get replies of given status.
+ replies, err := suite.state.DB.GetStatusReplies(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each reply as pending approval.
+ for _, reply := range replies {
+ reply.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatus(
+ ctx,
+ reply,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this reply.
+ req, err := typeutils.StatusToInteractionRequest(ctx, reply)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ // Get boosts of given status.
+ boosts, err := suite.state.DB.GetStatusBoosts(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each boost as pending approval.
+ for _, boost := range boosts {
+ boost.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatus(
+ ctx,
+ boost,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this boost.
+ req, err := typeutils.StatusToInteractionRequest(ctx, boost)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ // Get faves of given status.
+ faves, err := suite.state.DB.GetStatusFaves(ctx, statusID)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ suite.FailNow(err.Error())
+ }
+
+ // Mark each fave as pending approval.
+ for _, fave := range faves {
+ fave.PendingApproval = util.Ptr(true)
+ if err := suite.state.DB.UpdateStatusFave(
+ ctx,
+ fave,
+ "pending_approval",
+ ); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Put an interaction request
+ // in the DB for this fave.
+ req, err := typeutils.StatusFaveToInteractionRequest(ctx, fave)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ if err := suite.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ pendingCount++
+ }
+
+ return pendingCount
+}
+
+func (suite *InteractionTestSuite) TestGetPending() {
+ var (
+ testStatus = suite.testStatuses["local_account_1_status_1"]
+ ctx = context.Background()
+ acctID = suite.testAccounts["local_account_1"].ID
+ statusID = ""
+ likes = true
+ replies = true
+ boosts = true
+ page = &paging.Page{
+ Max: paging.MaxID(id.Highest),
+ Limit: 20,
+ }
+ )
+
+ // Update target test status to mark
+ // all interactions with it pending.
+ pendingCount := suite.markInteractionsPending(ctx, testStatus.ID)
+
+ // Get pendingInts interactions.
+ pendingInts, err := suite.state.DB.GetInteractionsRequestsForAcct(
+ ctx,
+ acctID,
+ statusID,
+ likes,
+ replies,
+ boosts,
+ page,
+ )
+ suite.NoError(err)
+ suite.Len(pendingInts, pendingCount)
+
+ // Ensure relevant model populated.
+ for _, pendingInt := range pendingInts {
+ switch pendingInt.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ suite.NotNil(pendingInt.Like)
+
+ case gtsmodel.InteractionReply:
+ suite.NotNil(pendingInt.Reply)
+
+ case gtsmodel.InteractionAnnounce:
+ suite.NotNil(pendingInt.Announce)
+ }
+ }
+}
+
+func (suite *InteractionTestSuite) TestGetPendingRepliesOnly() {
+ var (
+ testStatus = suite.testStatuses["local_account_1_status_1"]
+ ctx = context.Background()
+ acctID = suite.testAccounts["local_account_1"].ID
+ statusID = ""
+ likes = false
+ replies = true
+ boosts = false
+ page = &paging.Page{
+ Max: paging.MaxID(id.Highest),
+ Limit: 20,
+ }
+ )
+
+ // Update target test status to mark
+ // all interactions with it pending.
+ suite.markInteractionsPending(ctx, testStatus.ID)
+
+ // Get pendingInts interactions.
+ pendingInts, err := suite.state.DB.GetInteractionsRequestsForAcct(
+ ctx,
+ acctID,
+ statusID,
+ likes,
+ replies,
+ boosts,
+ page,
+ )
+ suite.NoError(err)
+
+ // Ensure only replies returned.
+ for _, pendingInt := range pendingInts {
+ suite.Equal(gtsmodel.InteractionReply, pendingInt.InteractionType)
+ }
+}
+
+func (suite *InteractionTestSuite) TestInteractionRejected() {
+ var (
+ ctx = context.Background()
+ req = new(gtsmodel.InteractionRequest)
+ )
+
+ // Make a copy of the request we'll modify.
+ *req = *suite.testInteractionRequests["admin_account_reply_turtle"]
+
+ // No rejection in the db for this interaction URI so it should be OK.
+ rejected, err := suite.state.DB.IsInteractionRejected(ctx, req.InteractionURI)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ if rejected {
+ suite.FailNow("wanted rejected = false, got true")
+ }
+
+ // Update the interaction request to mark it rejected.
+ req.RejectedAt = time.Now()
+ req.URI = "https://some.reject.uri"
+ if err := suite.state.DB.UpdateInteractionRequest(ctx, req, "uri", "rejected_at"); err != nil {
+ suite.FailNow(err.Error())
+ }
+
+ // Rejection in the db for this interaction URI now so it should be très mauvais.
+ rejected, err = suite.state.DB.IsInteractionRejected(ctx, req.InteractionURI)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ if !rejected {
+ suite.FailNow("wanted rejected = true, got false")
+ }
+}
+
+func TestInteractionTestSuite(t *testing.T) {
+ suite.Run(t, new(InteractionTestSuite))
+}
diff --git a/internal/db/bundb/migrations/20240716151327_interaction_policy.go b/internal/db/bundb/migrations/20240716151327_interaction_policy.go
index fb0d1d752..210d4b2c5 100644
--- a/internal/db/bundb/migrations/20240716151327_interaction_policy.go
+++ b/internal/db/bundb/migrations/20240716151327_interaction_policy.go
@@ -20,41 +20,12 @@ package migrations
import (
"context"
- gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/uptrace/bun"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
- if _, err := tx.
- NewCreateTable().
- Model(&gtsmodel.InteractionApproval{}).
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
- if _, err := tx.
- NewCreateIndex().
- Table("interaction_approvals").
- Index("interaction_approvals_account_id_idx").
- Column("account_id").
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
- if _, err := tx.
- NewCreateIndex().
- Table("interaction_approvals").
- Index("interaction_approvals_interacting_account_id_idx").
- Column("interacting_account_id").
- IfNotExists().
- Exec(ctx); err != nil {
- return err
- }
-
return nil
})
}
diff --git a/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go
new file mode 100644
index 000000000..82c2b4016
--- /dev/null
+++ b/internal/db/bundb/migrations/20240809134448_interaction_requests_client_api.go
@@ -0,0 +1,154 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package migrations
+
+import (
+ "context"
+
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+
+ "github.com/uptrace/bun"
+)
+
+func init() {
+ up := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+
+ // Drop interaction approvals table if it exists,
+ // ie., if instance was running on main between now
+ // and 2024-07-16.
+ //
+ // We might lose some interaction approvals this way,
+ // but since they weren't *really* used much yet this
+ // it's not a big deal, that's the running-on-main life!
+ if _, err := tx.NewDropTable().
+ Table("interaction_approvals").
+ IfExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ // Add `interaction_requests`
+ // table and new indexes.
+ if _, err := tx.
+ NewCreateTable().
+ Model(&gtsmodel.InteractionRequest{}).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+
+ for idx, col := range map[string]string{
+ "interaction_requests_status_id_idx": "status_id",
+ "interaction_requests_target_account_id_idx": "target_account_id",
+ "interaction_requests_interacting_account_id_idx": "interacting_account_id",
+ } {
+ if _, err := tx.
+ NewCreateIndex().
+ Table("interaction_requests").
+ Index(idx).
+ Column(col).
+ IfNotExists().
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ // Select all pending statuses (replies or boosts).
+ pendingStatuses := []*gtsmodel.Status{}
+ err := tx.
+ NewSelect().
+ Model(&pendingStatuses).
+ Column(
+ "created_at",
+ "in_reply_to_id",
+ "boost_of_id",
+ "in_reply_to_account_id",
+ "boost_of_account_id",
+ "account_id",
+ "uri",
+ ).
+ Where("? = ?", bun.Ident("pending_approval"), true).
+ Scan(ctx)
+ if err != nil {
+ return err
+ }
+
+ // For each currently pending status, check whether it's a reply or
+ // a boost, and insert a corresponding interaction request into the db.
+ for _, pendingStatus := range pendingStatuses {
+ req, err := typeutils.StatusToInteractionRequest(ctx, pendingStatus)
+ if err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewInsert().
+ Model(req).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ // Now do the same thing for pending faves.
+ pendingFaves := []*gtsmodel.StatusFave{}
+ err = tx.
+ NewSelect().
+ Model(&pendingFaves).
+ Column(
+ "created_at",
+ "status_id",
+ "target_account_id",
+ "account_id",
+ "uri",
+ ).
+ Where("? = ?", bun.Ident("pending_approval"), true).
+ Scan(ctx)
+ if err != nil {
+ return err
+ }
+
+ for _, pendingFave := range pendingFaves {
+ req, err := typeutils.StatusFaveToInteractionRequest(ctx, pendingFave)
+ if err != nil {
+ return err
+ }
+
+ if _, err := tx.
+ NewInsert().
+ Model(req).
+ Exec(ctx); err != nil {
+ return err
+ }
+ }
+
+ return nil
+ })
+ }
+
+ down := func(ctx context.Context, db *bun.DB) error {
+ return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
+ return nil
+ })
+ }
+
+ if err := Migrations.Register(up, down); err != nil {
+ panic(err)
+ }
+}
diff --git a/internal/db/bundb/timeline.go b/internal/db/bundb/timeline.go
index e6c7e482d..995c4e84f 100644
--- a/internal/db/bundb/timeline.go
+++ b/internal/db/bundb/timeline.go
@@ -89,19 +89,6 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
q = q.Where("? = ?", bun.Ident("status.local"), local)
}
- if limit > 0 {
- // limit amount of statuses returned
- q = q.Limit(limit)
- }
-
- if frontToBack {
- // Page down.
- q = q.Order("status.id DESC")
- } else {
- // Page up.
- q = q.Order("status.id ASC")
- }
-
// As this is the home timeline, it should be
// populated by statuses from accounts followed
// by accountID, and posts from accountID itself.
@@ -137,6 +124,22 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
bun.In(targetAccountIDs),
)
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
+ if limit > 0 {
+ // limit amount of statuses returned
+ q = q.Limit(limit)
+ }
+
+ if frontToBack {
+ // Page down.
+ q = q.Order("status.id DESC")
+ } else {
+ // Page up.
+ q = q.Order("status.id ASC")
+ }
+
if err := q.Scan(ctx, &statusIDs); err != nil {
return nil, err
}
@@ -213,6 +216,9 @@ func (t *timelineDB) GetPublicTimeline(ctx context.Context, maxID string, sinceI
q = q.Where("? = ?", bun.Ident("status.local"), local)
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
@@ -395,6 +401,9 @@ func (t *timelineDB) GetListTimeline(
frontToBack = false
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
@@ -491,6 +500,9 @@ func (t *timelineDB) GetTagTimeline(
frontToBack = false
}
+ // Only include statuses that aren't pending approval.
+ q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
+
if limit > 0 {
// limit amount of statuses returned
q = q.Limit(limit)
diff --git a/internal/db/interaction.go b/internal/db/interaction.go
index 6f595c54e..a3a3afde9 100644
--- a/internal/db/interaction.go
+++ b/internal/db/interaction.go
@@ -21,21 +21,47 @@ import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
)
type Interaction interface {
- // GetInteractionApprovalByID gets one approval with the given id.
- GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
+ // GetInteractionRequestByID gets one request with the given id.
+ GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error)
- // GetInteractionApprovalByID gets one approval with the given uri.
- GetInteractionApprovalByURI(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
+ // GetInteractionRequestByID gets one request with the given interaction uri.
+ GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error)
- // PopulateInteractionApproval ensures that the approval's struct fields are populated.
- PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
+ // GetInteractionRequestByURI returns one accepted or rejected
+ // interaction request with the given URI, if it exists in the db.
+ GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error)
- // PutInteractionApproval puts a new approval in the database.
- PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
+ // PopulateInteractionRequest ensures that the request's struct fields are populated.
+ PopulateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error
- // DeleteInteractionApprovalByID deletes one approval with the given ID.
- DeleteInteractionApprovalByID(ctx context.Context, id string) error
+ // PutInteractionRequest puts a new request in the database.
+ PutInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error
+
+ // UpdateInteractionRequest updates the given interaction request.
+ UpdateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest, columns ...string) error
+
+ // DeleteInteractionRequestByID deletes one request with the given ID.
+ DeleteInteractionRequestByID(ctx context.Context, id string) error
+
+ // GetInteractionsRequestsForAcct returns pending interactions targeting
+ // the given (optional) account ID and the given (optional) status ID.
+ //
+ // At least one of `likes`, `replies`, or `boosts` must be true.
+ GetInteractionsRequestsForAcct(
+ ctx context.Context,
+ acctID string,
+ statusID string,
+ likes bool,
+ replies bool,
+ boosts bool,
+ page *paging.Page,
+ ) ([]*gtsmodel.InteractionRequest, error)
+
+ // IsInteractionRejected returns true if an rejection exists in the database for an
+ // object with the given interactionURI (ie., a status or announce or fave uri).
+ IsInteractionRejected(ctx context.Context, interactionURI string) (bool, error)
}
diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go
index 60b9cfe58..8082e555f 100644
--- a/internal/federation/federatingdb/accept.go
+++ b/internal/federation/federatingdb/accept.go
@@ -38,11 +38,11 @@ func (f *federatingDB) GetAccept(
ctx context.Context,
acceptIRI *url.URL,
) (vocab.ActivityStreamsAccept, error) {
- approval, err := f.state.DB.GetInteractionApprovalByURI(ctx, acceptIRI.String())
+ approval, err := f.state.DB.GetInteractionRequestByURI(ctx, acceptIRI.String())
if err != nil {
return nil, err
}
- return f.converter.InteractionApprovalToASAccept(ctx, approval)
+ return f.converter.InteractionReqToASAccept(ctx, approval)
}
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
diff --git a/internal/gtsmodel/interaction.go b/internal/gtsmodel/interaction.go
new file mode 100644
index 000000000..562b752eb
--- /dev/null
+++ b/internal/gtsmodel/interaction.go
@@ -0,0 +1,93 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package gtsmodel
+
+import "time"
+
+// Like / Reply / Announce
+type InteractionType int
+
+const (
+ // WARNING: DO NOT CHANGE THE ORDER OF THESE,
+ // as this will cause breakage of approvals!
+ //
+ // If you need to add new interaction types,
+ // add them *to the end* of the list.
+
+ InteractionLike InteractionType = iota
+ InteractionReply
+ InteractionAnnounce
+)
+
+// Stringifies this InteractionType in a
+// manner suitable for serving via the API.
+func (i InteractionType) String() string {
+ switch i {
+ case InteractionLike:
+ const text = "favourite"
+ return text
+ case InteractionReply:
+ const text = "reply"
+ return text
+ case InteractionAnnounce:
+ const text = "reblog"
+ return text
+ default:
+ panic("undefined InteractionType")
+ }
+}
+
+// InteractionRequest represents one interaction (like, reply, fave)
+// that is either accepted, rejected, or currently still awaiting
+// acceptance or rejection by the target account.
+type InteractionRequest struct {
+ ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
+ CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
+ StatusID string `bun:"type:CHAR(26),nullzero,notnull"` // ID of the interaction target status.
+ Status *Status `bun:"-"` // Not stored in DB. Status being interacted with.
+ TargetAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account being interacted with
+ TargetAccount *Account `bun:"-"` // Not stored in DB. Account being interacted with.
+ InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account requesting the interaction.
+ InteractingAccount *Account `bun:"-"` // Not stored in DB. Account corresponding to targetAccountID
+ InteractionURI string `bun:",nullzero,notnull,unique"` // URI of the interacting like, reply, or announce. Unique (only one interaction request allowed per interaction URI).
+ InteractionType InteractionType `bun:",notnull"` // One of Like, Reply, or Announce.
+ Like *StatusFave `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionLike.
+ Reply *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionReply.
+ Announce *Status `bun:"-"` // Not stored in DB. Only set if InteractionType = InteractionAnnounce.
+ URI string `bun:",nullzero,unique"` // ActivityPub URI of the Accept (if accepted) or Reject (if rejected). Null/empty if currently neither accepted not rejected.
+ AcceptedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was accepted, time at which this occurred.
+ RejectedAt time.Time `bun:"type:timestamptz,nullzero"` // If interaction request was rejected, time at which this occurred.
+}
+
+// IsHandled returns true if interaction
+// request has been neither accepted or rejected.
+func (ir *InteractionRequest) IsPending() bool {
+ return ir.URI == "" && ir.AcceptedAt.IsZero() && ir.RejectedAt.IsZero()
+}
+
+// IsAccepted returns true if this
+// interaction request has been accepted.
+func (ir *InteractionRequest) IsAccepted() bool {
+ return ir.URI != "" && !ir.AcceptedAt.IsZero()
+}
+
+// IsRejected returns true if this
+// interaction request has been rejected.
+func (ir *InteractionRequest) IsRejected() bool {
+ return ir.URI != "" && !ir.RejectedAt.IsZero()
+}
diff --git a/internal/gtsmodel/interactionapproval.go b/internal/gtsmodel/interactionapproval.go
deleted file mode 100644
index f6a5da83b..000000000
--- a/internal/gtsmodel/interactionapproval.go
+++ /dev/null
@@ -1,55 +0,0 @@
-// GoToSocial
-// Copyright (C) GoToSocial Authors admin@gotosocial.org
-// SPDX-License-Identifier: AGPL-3.0-or-later
-//
-// This program is free software: you can redistribute it and/or modify
-// it under the terms of the GNU Affero General Public License as published by
-// the Free Software Foundation, either version 3 of the License, or
-// (at your option) any later version.
-//
-// This program is distributed in the hope that it will be useful,
-// but WITHOUT ANY WARRANTY; without even the implied warranty of
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-// GNU Affero General Public License for more details.
-//
-// You should have received a copy of the GNU Affero General Public License
-// along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-package gtsmodel
-
-import "time"
-
-// InteractionApproval refers to a single Accept activity sent
-// *from this instance* in response to an interaction request,
-// in order to approve it.
-//
-// Accepts originating from remote instances are not stored
-// using this format; the URI of the remote Accept is instead
-// just added to the *gtsmodel.StatusFave or *gtsmodel.Status.
-type InteractionApproval struct {
- ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
- CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
- UpdatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item last updated
- AccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account that owns this accept/approval
- Account *Account `bun:"-"` // account corresponding to accountID
- InteractingAccountID string `bun:"type:CHAR(26),nullzero,notnull"` // id of the account that did the interaction that this Accept targets.
- InteractingAccount *Account `bun:"-"` // account corresponding to targetAccountID
- InteractionURI string `bun:",nullzero,notnull"` // URI of the target like, reply, or announce
- InteractionType InteractionType `bun:",notnull"` // One of Like, Reply, or Announce.
- URI string `bun:",nullzero,notnull,unique"` // ActivityPub URI of the Accept.
-}
-
-// Like / Reply / Announce
-type InteractionType int
-
-const (
- // WARNING: DO NOT CHANGE THE ORDER OF THESE,
- // as this will cause breakage of approvals!
- //
- // If you need to add new interaction types,
- // add them *to the end* of the list.
-
- InteractionLike InteractionType = iota
- InteractionReply
- InteractionAnnounce
-)
diff --git a/internal/processing/fedi/accept.go b/internal/processing/fedi/accept.go
index 72d810f94..fc699ee08 100644
--- a/internal/processing/fedi/accept.go
+++ b/internal/processing/fedi/accept.go
@@ -27,14 +27,14 @@ import (
)
// AcceptGet handles the getting of a fedi/activitypub
-// representation of a local interaction approval.
+// representation of a local interaction acceptance.
//
// It performs appropriate authentication before
// returning a JSON serializable interface.
func (p *Processor) AcceptGet(
ctx context.Context,
requestedUser string,
- approvalID string,
+ reqID string,
) (interface{}, gtserror.WithCode) {
// Authenticate incoming request, getting related accounts.
auth, errWithCode := p.authenticate(ctx, requestedUser)
@@ -52,25 +52,26 @@ func (p *Processor) AcceptGet(
receivingAcct := auth.receivingAcct
- approval, err := p.state.DB.GetInteractionApprovalByID(ctx, approvalID)
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
- err := gtserror.Newf("db error getting approval %s: %w", approvalID, err)
+ err := gtserror.Newf("db error getting interaction request %s: %w", reqID, err)
return nil, gtserror.NewErrorInternalError(err)
}
- if approval.AccountID != receivingAcct.ID {
- const text = "approval does not belong to receiving account"
- return nil, gtserror.NewErrorNotFound(errors.New(text))
+ if req == nil || !req.IsAccepted() {
+ // Request doesn't exist or hasn't been accepted.
+ err := gtserror.Newf("interaction request %s not found", reqID)
+ return nil, gtserror.NewErrorNotFound(err)
}
- if approval == nil {
- err := gtserror.Newf("approval %s not found", approvalID)
- return nil, gtserror.NewErrorNotFound(err)
+ if req.TargetAccountID != receivingAcct.ID {
+ const text = "interaction request does not belong to receiving account"
+ return nil, gtserror.NewErrorNotFound(errors.New(text))
}
- accept, err := p.converter.InteractionApprovalToASAccept(ctx, approval)
+ accept, err := p.converter.InteractionReqToASAccept(ctx, req)
if err != nil {
- err := gtserror.Newf("error converting approval: %w", err)
+ err := gtserror.Newf("error converting accept: %w", err)
return nil, gtserror.NewErrorInternalError(err)
}
diff --git a/internal/processing/interactionrequests/accept.go b/internal/processing/interactionrequests/accept.go
new file mode 100644
index 000000000..ad86e50d1
--- /dev/null
+++ b/internal/processing/interactionrequests/accept.go
@@ -0,0 +1,239 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "context"
+ "time"
+
+ "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/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+// Accept accepts an interaction request with the given ID,
+// on behalf of the given account (whose post it must target).
+func (p *Processor) Accept(
+ ctx context.Context,
+ acct *gtsmodel.Account,
+ reqID string,
+) (*apimodel.InteractionRequest, gtserror.WithCode) {
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID)
+ if err != nil {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if req.TargetAccountID != acct.ID {
+ err := gtserror.Newf(
+ "interaction request %s does not belong to account %s",
+ reqID, acct.ID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if !req.IsPending() {
+ err := gtserror.Newf(
+ "interaction request %s has already been handled",
+ reqID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Lock on the interaction req URI to
+ // ensure nobody else is modifying it rn.
+ unlock := p.state.ProcessingLocks.Lock(req.InteractionURI)
+ defer unlock()
+
+ // Mark the request as accepted
+ // and generate a URI for it.
+ req.AcceptedAt = time.Now()
+ req.URI = uris.GenerateURIForAccept(acct.Username, req.ID)
+ if err := p.state.DB.UpdateInteractionRequest(
+ ctx,
+ req,
+ "accepted_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ switch req.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ if errWithCode := p.acceptLike(ctx, req); errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ case gtsmodel.InteractionReply:
+ if errWithCode := p.acceptReply(ctx, req); errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ case gtsmodel.InteractionAnnounce:
+ if errWithCode := p.acceptAnnounce(ctx, req); errWithCode != nil {
+ return nil, errWithCode
+ }
+
+ default:
+ err := gtserror.Newf("unknown interaction type for interaction request %s", reqID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Return the now-accepted req to the caller so
+ // they can do something with it if they need to.
+ apiReq, err := p.converter.InteractionReqToAPIInteractionReq(
+ ctx,
+ req,
+ acct,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiReq, nil
+}
+
+// Package-internal convenience
+// function to accept a like.
+func (p *Processor) acceptLike(
+ ctx context.Context,
+ req *gtsmodel.InteractionRequest,
+) gtserror.WithCode {
+ // If the Like is missing, that means it's
+ // probably already been undone by someone,
+ // so there's nothing to actually accept.
+ if req.Like == nil {
+ err := gtserror.Newf("no Like found for interaction request %s", req.ID)
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ // Update the Like.
+ req.Like.PendingApproval = util.Ptr(false)
+ req.Like.PreApproved = false
+ req.Like.ApprovedByURI = req.URI
+ if err := p.state.DB.UpdateStatusFave(
+ ctx,
+ req.Like,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating status fave: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Send the accepted request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityAccept,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ return nil
+}
+
+// Package-internal convenience
+// function to accept a reply.
+func (p *Processor) acceptReply(
+ ctx context.Context,
+ req *gtsmodel.InteractionRequest,
+) gtserror.WithCode {
+ // If the Reply is missing, that means it's
+ // probably already been undone by someone,
+ // so there's nothing to actually accept.
+ if req.Reply == nil {
+ err := gtserror.Newf("no Reply found for interaction request %s", req.ID)
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ // Update the Reply.
+ req.Reply.PendingApproval = util.Ptr(false)
+ req.Reply.PreApproved = false
+ req.Reply.ApprovedByURI = req.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ req.Reply,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating status reply: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Send the accepted request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityAccept,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ return nil
+}
+
+// Package-internal convenience
+// function to accept an announce.
+func (p *Processor) acceptAnnounce(
+ ctx context.Context,
+ req *gtsmodel.InteractionRequest,
+) gtserror.WithCode {
+ // If the Announce is missing, that means it's
+ // probably already been undone by someone,
+ // so there's nothing to actually accept.
+ if req.Reply == nil {
+ err := gtserror.Newf("no Announce found for interaction request %s", req.ID)
+ return gtserror.NewErrorNotFound(err)
+ }
+
+ // Update the Announce.
+ req.Announce.PendingApproval = util.Ptr(false)
+ req.Announce.PreApproved = false
+ req.Announce.ApprovedByURI = req.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ req.Announce,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating status announce: %w", err)
+ return gtserror.NewErrorInternalError(err)
+ }
+
+ // Send the accepted request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityAccept,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ return nil
+}
diff --git a/internal/processing/interactionrequests/accept_test.go b/internal/processing/interactionrequests/accept_test.go
new file mode 100644
index 000000000..75fb1e512
--- /dev/null
+++ b/internal/processing/interactionrequests/accept_test.go
@@ -0,0 +1,89 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests_test
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type AcceptTestSuite struct {
+ InteractionRequestsTestSuite
+}
+
+func (suite *AcceptTestSuite) TestAccept() {
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
+
+ var (
+ ctx = context.Background()
+ state = testStructs.State
+ acct = suite.testAccounts["local_account_2"]
+ intReq = suite.testInteractionRequests["admin_account_reply_turtle"]
+ )
+
+ // Create interaction reqs processor.
+ p := interactionrequests.New(
+ testStructs.Common,
+ testStructs.State,
+ testStructs.TypeConverter,
+ )
+
+ apiReq, errWithCode := p.Accept(ctx, acct, intReq.ID)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ // Get db interaction request.
+ dbReq, err := state.DB.GetInteractionRequestByID(ctx, apiReq.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.True(dbReq.IsAccepted())
+
+ // Interacting status
+ // should now be approved.
+ dbStatus, err := state.DB.GetStatusByURI(ctx, dbReq.InteractionURI)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.False(*dbStatus.PendingApproval)
+ suite.Equal(dbReq.URI, dbStatus.ApprovedByURI)
+
+ // Wait for a notification
+ // for interacting status.
+ testrig.WaitFor(func() bool {
+ notif, err := state.DB.GetNotification(
+ ctx,
+ gtsmodel.NotificationMention,
+ dbStatus.InReplyToAccountID,
+ dbStatus.AccountID,
+ dbStatus.ID,
+ )
+ return notif != nil && err == nil
+ })
+}
+
+func TestAcceptTestSuite(t *testing.T) {
+ suite.Run(t, new(AcceptTestSuite))
+}
diff --git a/internal/processing/interactionrequests/get.go b/internal/processing/interactionrequests/get.go
new file mode 100644
index 000000000..8f8a7f35d
--- /dev/null
+++ b/internal/processing/interactionrequests/get.go
@@ -0,0 +1,141 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "context"
+ "errors"
+ "net/url"
+ "strconv"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/paging"
+)
+
+// GetPage returns a page of interaction requests targeting
+// the requester and (optionally) the given status ID.
+func (p *Processor) GetPage(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ statusID string,
+ likes bool,
+ replies bool,
+ boosts bool,
+ page *paging.Page,
+) (*apimodel.PageableResponse, gtserror.WithCode) {
+ reqs, err := p.state.DB.GetInteractionsRequestsForAcct(
+ ctx,
+ requester.ID,
+ statusID,
+ likes,
+ replies,
+ boosts,
+ page,
+ )
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction requests: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ count := len(reqs)
+ if count == 0 {
+ return paging.EmptyResponse(), nil
+ }
+
+ var (
+ // Get the lowest and highest
+ // ID values, used for paging.
+ lo = reqs[count-1].ID
+ hi = reqs[0].ID
+
+ // Best-guess items length.
+ items = make([]interface{}, 0, count)
+ )
+
+ for _, req := range reqs {
+ apiReq, err := p.converter.InteractionReqToAPIInteractionReq(
+ ctx, req, requester,
+ )
+ if err != nil {
+ log.Errorf(ctx, "error converting interaction req to api req: %v", err)
+ continue
+ }
+
+ // Append req to return items.
+ items = append(items, apiReq)
+ }
+
+ // Build extra query params to return in Link header.
+ extraParams := make(url.Values, 4)
+ extraParams.Set(apiutil.InteractionFavouritesKey, strconv.FormatBool(likes))
+ extraParams.Set(apiutil.InteractionRepliesKey, strconv.FormatBool(replies))
+ extraParams.Set(apiutil.InteractionReblogsKey, strconv.FormatBool(boosts))
+ if statusID != "" {
+ extraParams.Set(apiutil.InteractionStatusIDKey, statusID)
+ }
+
+ return paging.PackageResponse(paging.ResponseParams{
+ Items: items,
+ Path: "/api/v1/interaction_requests",
+ Next: page.Next(lo, hi),
+ Prev: page.Prev(lo, hi),
+ Query: extraParams,
+ }), nil
+}
+
+// GetOne returns one interaction
+// request with the given ID.
+func (p *Processor) GetOne(
+ ctx context.Context,
+ requester *gtsmodel.Account,
+ id string,
+) (*apimodel.InteractionRequest, gtserror.WithCode) {
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, id)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if req == nil {
+ err := gtserror.New("interaction request not found")
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if req.TargetAccountID != requester.ID {
+ err := gtserror.Newf(
+ "interaction request %s does not target account %s",
+ req.ID, requester.ID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ apiReq, err := p.converter.InteractionReqToAPIInteractionReq(
+ ctx, req, requester,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting interaction req to api req: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiReq, nil
+}
diff --git a/internal/processing/interactionrequests/interactionrequests.go b/internal/processing/interactionrequests/interactionrequests.go
new file mode 100644
index 000000000..d56636233
--- /dev/null
+++ b/internal/processing/interactionrequests/interactionrequests.go
@@ -0,0 +1,47 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
+ "github.com/superseriousbusiness/gotosocial/internal/state"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+)
+
+// Processor wraps functionality for getting,
+// accepting, and rejecting interaction requests.
+type Processor struct {
+ // common processor logic
+ c *common.Processor
+
+ state *state.State
+ converter *typeutils.Converter
+}
+
+// New returns a new interaction requests processor.
+func New(
+ common *common.Processor,
+ state *state.State,
+ converter *typeutils.Converter,
+) Processor {
+ return Processor{
+ c: common,
+ state: state,
+ converter: converter,
+ }
+}
diff --git a/internal/processing/interactionrequests/interactionrequests_test.go b/internal/processing/interactionrequests/interactionrequests_test.go
new file mode 100644
index 000000000..ce2aa351a
--- /dev/null
+++ b/internal/processing/interactionrequests/interactionrequests_test.go
@@ -0,0 +1,45 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests_test
+
+import (
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+const (
+ rMediaPath = "../../../testrig/media"
+ rTemplatePath = "../../../web/template"
+)
+
+type InteractionRequestsTestSuite struct {
+ suite.Suite
+
+ testAccounts map[string]*gtsmodel.Account
+ testStatuses map[string]*gtsmodel.Status
+ testInteractionRequests map[string]*gtsmodel.InteractionRequest
+}
+
+func (suite *InteractionRequestsTestSuite) SetupTest() {
+ testrig.InitTestConfig()
+ testrig.InitTestLog()
+ suite.testAccounts = testrig.NewTestAccounts()
+ suite.testStatuses = testrig.NewTestStatuses()
+ suite.testInteractionRequests = testrig.NewTestInteractionRequests()
+}
diff --git a/internal/processing/interactionrequests/reject.go b/internal/processing/interactionrequests/reject.go
new file mode 100644
index 000000000..dcf07a03f
--- /dev/null
+++ b/internal/processing/interactionrequests/reject.go
@@ -0,0 +1,133 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests
+
+import (
+ "context"
+ "time"
+
+ "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/messages"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
+)
+
+// Reject rejects an interaction request with the given ID,
+// on behalf of the given account (whose post it must target).
+func (p *Processor) Reject(
+ ctx context.Context,
+ acct *gtsmodel.Account,
+ reqID string,
+) (*apimodel.InteractionRequest, gtserror.WithCode) {
+ req, err := p.state.DB.GetInteractionRequestByID(ctx, reqID)
+ if err != nil {
+ err := gtserror.Newf("db error getting interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ if req.TargetAccountID != acct.ID {
+ err := gtserror.Newf(
+ "interaction request %s does not belong to account %s",
+ reqID, acct.ID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ if !req.IsPending() {
+ err := gtserror.Newf(
+ "interaction request %s has already been handled",
+ reqID,
+ )
+ return nil, gtserror.NewErrorNotFound(err)
+ }
+
+ // Lock on the interaction req URI to
+ // ensure nobody else is modifying it rn.
+ unlock := p.state.ProcessingLocks.Lock(req.InteractionURI)
+ defer unlock()
+
+ // Mark the request as rejected
+ // and generate a URI for it.
+ req.RejectedAt = time.Now()
+ req.URI = uris.GenerateURIForReject(acct.Username, req.ID)
+ if err := p.state.DB.UpdateInteractionRequest(
+ ctx,
+ req,
+ "rejected_at",
+ "uri",
+ ); err != nil {
+ err := gtserror.Newf("db error updating interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ switch req.InteractionType {
+
+ case gtsmodel.InteractionLike:
+ // Send the rejected request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityLike,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ case gtsmodel.InteractionReply:
+ // Send the rejected request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ObjectNote,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ case gtsmodel.InteractionAnnounce:
+ // Send the rejected request off through the
+ // client API processor to handle side effects.
+ p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
+ APObjectType: ap.ActivityAnnounce,
+ APActivityType: ap.ActivityReject,
+ GTSModel: req,
+ Origin: req.TargetAccount,
+ Target: req.InteractingAccount,
+ })
+
+ default:
+ err := gtserror.Newf("unknown interaction type for interaction request %s", reqID)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ // Return the now-rejected req to the caller so
+ // they can do something with it if they need to.
+ apiReq, err := p.converter.InteractionReqToAPIInteractionReq(
+ ctx,
+ req,
+ acct,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting interaction request: %w", err)
+ return nil, gtserror.NewErrorInternalError(err)
+ }
+
+ return apiReq, nil
+}
diff --git a/internal/processing/interactionrequests/reject_test.go b/internal/processing/interactionrequests/reject_test.go
new file mode 100644
index 000000000..f1f6aed72
--- /dev/null
+++ b/internal/processing/interactionrequests/reject_test.go
@@ -0,0 +1,78 @@
+// GoToSocial
+// Copyright (C) GoToSocial Authors admin@gotosocial.org
+// SPDX-License-Identifier: AGPL-3.0-or-later
+//
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU Affero General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+package interactionrequests_test
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
+ "github.com/superseriousbusiness/gotosocial/testrig"
+)
+
+type RejectTestSuite struct {
+ InteractionRequestsTestSuite
+}
+
+func (suite *RejectTestSuite) TestReject() {
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
+
+ var (
+ ctx = context.Background()
+ state = testStructs.State
+ acct = suite.testAccounts["local_account_2"]
+ intReq = suite.testInteractionRequests["admin_account_reply_turtle"]
+ )
+
+ // Create int reqs processor.
+ p := interactionrequests.New(
+ testStructs.Common,
+ testStructs.State,
+ testStructs.TypeConverter,
+ )
+
+ apiReq, errWithCode := p.Reject(ctx, acct, intReq.ID)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ // Get db interaction rejection.
+ dbReq, err := state.DB.GetInteractionRequestByID(ctx, apiReq.ID)
+ if err != nil {
+ suite.FailNow(err.Error())
+ }
+ suite.True(dbReq.IsRejected())
+
+ // Wait for interacting status to be deleted.
+ testrig.WaitFor(func() bool {
+ status, err := state.DB.GetStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ dbReq.InteractionURI,
+ )
+ return status == nil && errors.Is(err, db.ErrNoEntries)
+ })
+}
+
+func TestRejectTestSuite(t *testing.T) {
+ suite.Run(t, new(RejectTestSuite))
+}
diff --git a/internal/processing/processor.go b/internal/processing/processor.go
index 6d39dc103..2ed13d396 100644
--- a/internal/processing/processor.go
+++ b/internal/processing/processor.go
@@ -34,6 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
filtersv2 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v2"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/interactionrequests"
"github.com/superseriousbusiness/gotosocial/internal/processing/list"
"github.com/superseriousbusiness/gotosocial/internal/processing/markers"
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
@@ -74,25 +75,26 @@ type Processor struct {
SUB-PROCESSORS
*/
- account account.Processor
- admin admin.Processor
- advancedmigrations advancedmigrations.Processor
- conversations conversations.Processor
- fedi fedi.Processor
- filtersv1 filtersv1.Processor
- filtersv2 filtersv2.Processor
- list list.Processor
- markers markers.Processor
- media media.Processor
- polls polls.Processor
- report report.Processor
- search search.Processor
- status status.Processor
- stream stream.Processor
- tags tags.Processor
- timeline timeline.Processor
- user user.Processor
- workers workers.Processor
+ account account.Processor
+ admin admin.Processor
+ advancedmigrations advancedmigrations.Processor
+ conversations conversations.Processor
+ fedi fedi.Processor
+ filtersv1 filtersv1.Processor
+ filtersv2 filtersv2.Processor
+ interactionRequests interactionrequests.Processor
+ list list.Processor
+ markers markers.Processor
+ media media.Processor
+ polls polls.Processor
+ report report.Processor
+ search search.Processor
+ status status.Processor
+ stream stream.Processor
+ tags tags.Processor
+ timeline timeline.Processor
+ user user.Processor
+ workers workers.Processor
}
func (p *Processor) Account() *account.Processor {
@@ -123,6 +125,10 @@ func (p *Processor) FiltersV2() *filtersv2.Processor {
return &p.filtersv2
}
+func (p *Processor) InteractionRequests() *interactionrequests.Processor {
+ return &p.interactionRequests
+}
+
func (p *Processor) List() *list.Processor {
return &p.list
}
@@ -209,6 +215,7 @@ func NewProcessor(
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
processor.filtersv2 = filtersv2.New(state, converter, &processor.stream)
+ processor.interactionRequests = interactionrequests.New(&common, state, converter)
processor.list = list.New(state, converter)
processor.markers = markers.New(state, converter)
processor.polls = polls.New(&common, state, converter)
@@ -227,6 +234,7 @@ func NewProcessor(
// and pass subset of sub processors it needs.
processor.workers = workers.New(
state,
+ &common,
federator,
converter,
visFilter,
diff --git a/internal/processing/workers/federate.go b/internal/processing/workers/federate.go
index 6e8b558c8..d14f38902 100644
--- a/internal/processing/workers/federate.go
+++ b/internal/processing/workers/federate.go
@@ -1127,17 +1127,17 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
func (f *federate) AcceptInteraction(
ctx context.Context,
- approval *gtsmodel.InteractionApproval,
+ req *gtsmodel.InteractionRequest,
) error {
// Populate model.
- if err := f.state.DB.PopulateInteractionApproval(ctx, approval); err != nil {
- return gtserror.Newf("error populating approval: %w", err)
+ if err := f.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
+ return gtserror.Newf("error populating request: %w", err)
}
// Bail if interacting account is ours:
// we've already accepted internally and
// shouldn't send an Accept to ourselves.
- if approval.InteractingAccount.IsLocal() {
+ if req.InteractingAccount.IsLocal() {
return nil
}
@@ -1145,27 +1145,27 @@ func (f *federate) AcceptInteraction(
// we can't Accept on another
// instance's behalf. (This
// should never happen but...)
- if approval.Account.IsRemote() {
+ if req.TargetAccount.IsRemote() {
return nil
}
// Parse relevant URI(s).
- outboxIRI, err := parseURI(approval.Account.OutboxURI)
+ outboxIRI, err := parseURI(req.TargetAccount.OutboxURI)
if err != nil {
return err
}
- acceptingAcctIRI, err := parseURI(approval.Account.URI)
+ acceptingAcctIRI, err := parseURI(req.TargetAccount.URI)
if err != nil {
return err
}
- interactingAcctURI, err := parseURI(approval.InteractingAccount.URI)
+ interactingAcctURI, err := parseURI(req.InteractingAccount.URI)
if err != nil {
return err
}
- interactionURI, err := parseURI(approval.InteractionURI)
+ interactionURI, err := parseURI(req.InteractionURI)
if err != nil {
return err
}
@@ -1190,7 +1190,79 @@ func (f *federate) AcceptInteraction(
); err != nil {
return gtserror.Newf(
"error sending activity %T for %v via outbox %s: %w",
- accept, approval.InteractionType, outboxIRI, err,
+ accept, req.InteractionType, outboxIRI, err,
+ )
+ }
+
+ return nil
+}
+
+func (f *federate) RejectInteraction(
+ ctx context.Context,
+ req *gtsmodel.InteractionRequest,
+) error {
+ // Populate model.
+ if err := f.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
+ return gtserror.Newf("error populating request: %w", err)
+ }
+
+ // Bail if interacting account is ours:
+ // we've already rejected internally and
+ // shouldn't send an Reject to ourselves.
+ if req.InteractingAccount.IsLocal() {
+ return nil
+ }
+
+ // Bail if account isn't ours:
+ // we can't Reject on another
+ // instance's behalf. (This
+ // should never happen but...)
+ if req.TargetAccount.IsRemote() {
+ return nil
+ }
+
+ // Parse relevant URI(s).
+ outboxIRI, err := parseURI(req.TargetAccount.OutboxURI)
+ if err != nil {
+ return err
+ }
+
+ rejectingAcctIRI, err := parseURI(req.TargetAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ interactingAcctURI, err := parseURI(req.InteractingAccount.URI)
+ if err != nil {
+ return err
+ }
+
+ interactionURI, err := parseURI(req.InteractionURI)
+ if err != nil {
+ return err
+ }
+
+ // Create a new Reject.
+ reject := streams.NewActivityStreamsReject()
+
+ // Set interacted-with account
+ // as Actor of the Reject.
+ ap.AppendActorIRIs(reject, rejectingAcctIRI)
+
+ // Set the interacted-with object
+ // as Object of the Reject.
+ ap.AppendObjectIRIs(reject, interactionURI)
+
+ // Address the Reject To the interacting acct.
+ ap.AppendTo(reject, interactingAcctURI)
+
+ // Send the Reject via the Actor's outbox.
+ if _, err := f.FederatingActor().Send(
+ ctx, outboxIRI, reject,
+ ); err != nil {
+ return gtserror.Newf(
+ "error sending activity %T for %v via outbox %s: %w",
+ reject, req.InteractionType, outboxIRI, err,
)
}
diff --git a/internal/processing/workers/fromclientapi.go b/internal/processing/workers/fromclientapi.go
index 1a37341f8..c723a6001 100644
--- a/internal/processing/workers/fromclientapi.go
+++ b/internal/processing/workers/fromclientapi.go
@@ -20,18 +20,23 @@ package workers
import (
"context"
"errors"
+ "time"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -44,6 +49,7 @@ type clientAPI struct {
surface *Surface
federate *federate
account *account.Processor
+ common *common.Processor
utils *utils
}
@@ -160,6 +166,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
// REJECT USER (ie., new user+account sign-up)
case ap.ObjectProfile:
return p.clientAPI.RejectUser(ctx, cMsg)
+
+ // REJECT NOTE/STATUS (ie., reject a reply)
+ case ap.ObjectNote:
+ return p.clientAPI.RejectReply(ctx, cMsg)
+
+ // REJECT LIKE
+ case ap.ActivityLike:
+ return p.clientAPI.RejectLike(ctx, cMsg)
+
+ // REJECT BOOST
+ case ap.ActivityAnnounce:
+ return p.clientAPI.RejectAnnounce(ctx, cMsg)
}
// UNDO SOMETHING
@@ -261,15 +279,13 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending reply.
- if err := p.surface.notifyPendingReply(ctx, status); err != nil {
- log.Errorf(ctx, "error notifying pending reply: %v", err)
+ if err := p.utils.requestReply(ctx, status); err != nil {
+ return gtserror.Newf("error pending reply: %w", err)
}
// Send Create to *remote* account inbox ONLY.
if err := p.federate.CreateStatus(ctx, status); err != nil {
- log.Errorf(ctx, "error federating pending reply: %v", err)
+ return gtserror.Newf("error federating pending reply: %w", err)
}
// Return early.
@@ -285,14 +301,38 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
// sending out the Create with the approval
// URI attached.
- // Put approval in the database and
- // update the status with approvedBy URI.
- approval, err := p.utils.approveReply(ctx, status)
- if err != nil {
- return gtserror.Newf("error pre-approving reply: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: status.InReplyToID,
+ TargetAccountID: status.InReplyToAccountID,
+ TargetAccount: status.InReplyToAccount,
+ InteractingAccountID: status.AccountID,
+ InteractingAccount: status.Account,
+ InteractionURI: status.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Reply: status,
+ URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+ if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
+ return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
+ }
+
+ // Mark the status as now approved.
+ status.PendingApproval = util.Ptr(false)
+ status.PreApproved = false
+ status.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ status,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status: %w", err)
}
- // Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of reply: %w", err)
}
@@ -309,16 +349,16 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
+ if err := p.federate.CreateStatus(ctx, status); err != nil {
+ log.Errorf(ctx, "error federating status: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}
- if err := p.federate.CreateStatus(ctx, status); err != nil {
- log.Errorf(ctx, "error federating status: %v", err)
- }
-
return nil
}
@@ -344,9 +384,6 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
status := vote.Poll.Status
status.Poll = vote.Poll
- // Interaction counts changed on the source status, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
-
if *status.Local {
// These are poll votes in a local status, we only need to
// federate the updated status model with latest vote counts.
@@ -360,6 +397,9 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
}
}
+ // Interaction counts changed on the source status, uncache from timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
+
return nil
}
@@ -429,10 +469,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// If pending approval is true then fave must
// target a status (either one of ours or a
// remote) that requires approval for the fave.
- pendingApproval := util.PtrOrValue(
- fave.PendingApproval,
- false,
- )
+ pendingApproval := util.PtrOrZero(fave.PendingApproval)
switch {
case pendingApproval && !fave.PreApproved:
@@ -442,15 +479,13 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending reply.
- if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
- log.Errorf(ctx, "error notifying pending fave: %v", err)
+ if err := p.utils.requestFave(ctx, fave); err != nil {
+ return gtserror.Newf("error pending fave: %w", err)
}
// Send Like to *remote* account inbox ONLY.
if err := p.federate.Like(ctx, fave); err != nil {
- log.Errorf(ctx, "error federating pending Like: %v", err)
+ return gtserror.Newf("error federating pending Like: %v", err)
}
// Return early.
@@ -466,14 +501,38 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
// sending out the Like with the approval
// URI attached.
- // Put approval in the database and
- // update the fave with approvedBy URI.
- approval, err := p.utils.approveFave(ctx, fave)
- if err != nil {
- return gtserror.Newf("error pre-approving fave: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: fave.StatusID,
+ TargetAccountID: fave.TargetAccountID,
+ TargetAccount: fave.TargetAccount,
+ InteractingAccountID: fave.AccountID,
+ InteractingAccount: fave.Account,
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Like: fave,
+ URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+ if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
+ return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
+ }
+
+ // Mark the fave itself as now approved.
+ fave.PendingApproval = util.Ptr(false)
+ fave.PreApproved = false
+ fave.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatusFave(
+ ctx,
+ fave,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status fave: %w", err)
}
- // Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of fave: %w", err)
}
@@ -485,14 +544,14 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
log.Errorf(ctx, "error notifying fave: %v", err)
}
- // Interaction counts changed on the faved status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
-
if err := p.federate.Like(ctx, fave); err != nil {
log.Errorf(ctx, "error federating like: %v", err)
}
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, fave.StatusID)
+
return nil
}
@@ -505,10 +564,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// If pending approval is true then status must
// boost a status (either one of ours or a
// remote) that requires approval for the boost.
- pendingApproval := util.PtrOrValue(
- boost.PendingApproval,
- false,
- )
+ pendingApproval := util.PtrOrZero(boost.PendingApproval)
switch {
case pendingApproval && !boost.PreApproved:
@@ -518,15 +574,13 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// and/or notify the account that's being
// interacted with (if it's local): they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending announce.
- if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
- log.Errorf(ctx, "error notifying pending boost: %v", err)
+ if err := p.utils.requestAnnounce(ctx, boost); err != nil {
+ return gtserror.Newf("error pending boost: %w", err)
}
// Send Announce to *remote* account inbox ONLY.
if err := p.federate.Announce(ctx, boost); err != nil {
- log.Errorf(ctx, "error federating pending Announce: %v", err)
+ return gtserror.Newf("error federating pending Announce: %v", err)
}
// Return early.
@@ -542,14 +596,38 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
// sending out the Create with the approval
// URI attached.
- // Put approval in the database and
- // update the boost with approvedBy URI.
- approval, err := p.utils.approveAnnounce(ctx, boost)
- if err != nil {
- return gtserror.Newf("error pre-approving boost: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: boost.BoostOfID,
+ TargetAccountID: boost.BoostOfAccountID,
+ TargetAccount: boost.BoostOfAccount,
+ InteractingAccountID: boost.AccountID,
+ InteractingAccount: boost.Account,
+ InteractionURI: boost.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Announce: boost,
+ URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+ if err := p.state.DB.PutInteractionRequest(ctx, approval); err != nil {
+ return gtserror.Newf("db error putting pre-approved interaction request: %w", err)
+ }
+
+ // Mark the boost itself as now approved.
+ boost.PendingApproval = util.Ptr(false)
+ boost.PreApproved = false
+ boost.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ boost,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status: %w", err)
}
- // Send out the approval as Accept.
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
return gtserror.Newf("error federating pre-approval of boost: %w", err)
}
@@ -572,14 +650,14 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
log.Errorf(ctx, "error notifying boost: %v", err)
}
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
-
if err := p.federate.Announce(ctx, boost); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+
return nil
}
@@ -629,9 +707,6 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error federating status update: %v", err)
}
- // Status representation has changed, invalidate from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
-
if status.Poll != nil && status.Poll.Closing {
// If the latest status has a newly closed poll, at least compared
@@ -646,6 +721,9 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error streaming status edit: %v", err)
}
+ // Status representation has changed, invalidate from timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+
return nil
}
@@ -791,14 +869,14 @@ func (p *clientAPI) UndoFave(ctx context.Context, cMsg *messages.FromClientAPI)
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
}
- // Interaction counts changed on the faved status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
-
if err := p.federate.UndoLike(ctx, statusFave); err != nil {
log.Errorf(ctx, "error federating like undo: %v", err)
}
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, statusFave.StatusID)
+
return nil
}
@@ -821,14 +899,14 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error removing timelined status: %v", err)
}
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
-
if err := p.federate.UndoAnnounce(ctx, status); err != nil {
log.Errorf(ctx, "error federating announce undo: %v", err)
}
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.BoostOfID)
+
return nil
}
@@ -874,16 +952,16 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientA
log.Errorf(ctx, "error updating account stats: %v", err)
}
+ if err := p.federate.DeleteStatus(ctx, status); err != nil {
+ log.Errorf(ctx, "error federating status delete: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status;
// uncache the prepared version from all timelines.
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}
- if err := p.federate.DeleteStatus(ctx, status); err != nil {
- log.Errorf(ctx, "error federating status delete: %v", err)
- }
-
return nil
}
@@ -1050,16 +1128,188 @@ func (p *clientAPI) RejectUser(ctx context.Context, cMsg *messages.FromClientAPI
}
func (p *clientAPI) AcceptLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
- // TODO
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ // Notify the fave (distinct from the notif for the pending fave).
+ if err := p.surface.notifyFave(ctx, req.Like); err != nil {
+ log.Errorf(ctx, "error notifying fave: %v", err)
+ }
+
+ // Send out the Accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating approval of like: %v", err)
+ }
+
+ // Interaction counts changed on the faved status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, req.Like.StatusID)
+
return nil
}
func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
- // TODO
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ var (
+ interactingAcct = req.InteractingAccount
+ reply = req.Reply
+ )
+
+ // Update stats for the reply author account.
+ if err := p.utils.incrementStatusesCount(ctx, interactingAcct, reply); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Timeline the reply + notify relevant accounts.
+ if err := p.surface.timelineAndNotifyStatus(ctx, reply); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status reply: %v", err)
+ }
+
+ // Send out the Accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating approval of reply: %v", err)
+ }
+
+ // Interaction counts changed on the replied status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, reply.InReplyToID)
+
return nil
}
func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
- // TODO
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ var (
+ interactingAcct = req.InteractingAccount
+ boost = req.Announce
+ )
+
+ // Update stats for the boost author account.
+ if err := p.utils.incrementStatusesCount(ctx, interactingAcct, boost); err != nil {
+ log.Errorf(ctx, "error updating account stats: %v", err)
+ }
+
+ // Timeline and notify the announce.
+ if err := p.surface.timelineAndNotifyStatus(ctx, boost); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status: %v", err)
+ }
+
+ // Notify the announce (distinct from the notif for the pending announce).
+ if err := p.surface.notifyAnnounce(ctx, boost); err != nil {
+ log.Errorf(ctx, "error notifying announce: %v", err)
+ }
+
+ // Send out the Accept.
+ if err := p.federate.AcceptInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating approval of announce: %v", err)
+ }
+
+ // Interaction counts changed on the original status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+
+ return nil
+}
+
+func (p *clientAPI) RejectLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Send out the Reject.
+ if err := p.federate.RejectInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating rejection of like: %v", err)
+ }
+
+ // Get the rejected fave.
+ fave, err := p.state.DB.GetStatusFaveByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected fave: %w", err)
+ }
+
+ // Delete the status fave.
+ if err := p.state.DB.DeleteStatusFaveByID(ctx, fave.ID); err != nil {
+ return gtserror.Newf("db error deleting status fave: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) RejectReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Send out the Reject.
+ if err := p.federate.RejectInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating rejection of reply: %v", err)
+ }
+
+ // Get the rejected status.
+ status, err := p.state.DB.GetStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected reply: %w", err)
+ }
+
+ // Totally wipe the status.
+ if err := p.utils.wipeStatus(ctx, status, true); err != nil {
+ return gtserror.Newf("error wiping status: %w", err)
+ }
+
+ return nil
+}
+
+func (p *clientAPI) RejectAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
+ req, ok := cMsg.GTSModel.(*gtsmodel.InteractionRequest)
+ if !ok {
+ return gtserror.Newf("%T not parseable as *gtsmodel.InteractionRequest", cMsg.GTSModel)
+ }
+
+ // At this point the InteractionRequest should already
+ // be in the database, we just need to do side effects.
+
+ // Send out the Reject.
+ if err := p.federate.RejectInteraction(ctx, req); err != nil {
+ log.Errorf(ctx, "error federating rejection of announce: %v", err)
+ }
+
+ // Get the rejected boost.
+ boost, err := p.state.DB.GetStatusByURI(
+ gtscontext.SetBarebones(ctx),
+ req.InteractionURI,
+ )
+ if err != nil {
+ return gtserror.Newf("db error getting rejected announce: %w", err)
+ }
+
+ // Totally wipe the status.
+ if err := p.utils.wipeStatus(ctx, boost, true); err != nil {
+ return gtserror.Newf("error wiping status: %w", err)
+ }
+
return nil
}
diff --git a/internal/processing/workers/fromclientapi_test.go b/internal/processing/workers/fromclientapi_test.go
index b4eae0be0..d330e4c2b 100644
--- a/internal/processing/workers/fromclientapi_test.go
+++ b/internal/processing/workers/fromclientapi_test.go
@@ -231,8 +231,8 @@ func (suite *FromClientAPITestSuite) conversationJSON(
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -344,8 +344,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -412,8 +412,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -473,8 +473,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -534,8 +534,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
@@ -610,8 +610,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
@@ -691,8 +691,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
@@ -767,8 +767,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -831,8 +831,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -898,8 +898,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
// A DM to a local user should create a conversation and accompanying notification.
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversation() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -984,8 +984,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversat
// A public message to a local user should not result in a conversation notification.
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreateConversation() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1054,8 +1054,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreate
// A public status with a hashtag followed by a local user who does not otherwise follow the author
// should end up in the tag-following user's home timeline.
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1128,8 +1128,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag(
// should not end up in the tag-following user's home timeline
// if the user has the author blocked.
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagAndBlock() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1209,8 +1209,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagA
// who does not otherwise follow the author or booster
// should end up in the tag-following user's home timeline as the original status.
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1312,8 +1312,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag()
// should not end up in the tag-following user's home timeline
// if the user has the author blocked.
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlock() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1422,8 +1422,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
// should not end up in the tag-following user's home timeline
// if the user has the booster blocked.
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlockedBoost() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1530,8 +1530,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
// Updating a public status with a hashtag followed by a local user who does not otherwise follow the author
// should stream a status update to the tag-following user's home timeline.
func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
@@ -1601,8 +1601,8 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag(
}
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
diff --git a/internal/processing/workers/fromfediapi.go b/internal/processing/workers/fromfediapi.go
index ce7c53388..908369ca6 100644
--- a/internal/processing/workers/fromfediapi.go
+++ b/internal/processing/workers/fromfediapi.go
@@ -20,18 +20,22 @@ package workers
import (
"context"
"errors"
+ "time"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/uris"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -44,6 +48,7 @@ type fediAPI struct {
surface *Surface
federate *federate
account *account.Processor
+ common *common.Processor
utils *utils
}
@@ -231,10 +236,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// If pending approval is true then
// status must reply to a LOCAL status
// that requires approval for the reply.
- pendingApproval := util.PtrOrValue(
- status.PendingApproval,
- false,
- )
+ pendingApproval := util.PtrOrZero(status.PendingApproval)
switch {
case pendingApproval && !status.PreApproved:
@@ -242,10 +244,8 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending reply.
- if err := p.surface.notifyPendingReply(ctx, status); err != nil {
- log.Errorf(ctx, "error notifying pending reply: %v", err)
+ if err := p.utils.requestReply(ctx, status); err != nil {
+ return gtserror.Newf("error pending reply: %w", err)
}
// Return early.
@@ -259,11 +259,33 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Put approval in the database and
- // update the status with approvedBy URI.
- approval, err := p.utils.approveReply(ctx, status)
- if err != nil {
- return gtserror.Newf("error pre-approving reply: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: status.InReplyToID,
+ TargetAccountID: status.InReplyToAccountID,
+ TargetAccount: status.InReplyToAccount,
+ InteractingAccountID: status.AccountID,
+ InteractingAccount: status.Account,
+ InteractionURI: status.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Reply: status,
+ URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+
+ // Mark the status as now approved.
+ status.PendingApproval = util.Ptr(false)
+ status.PreApproved = false
+ status.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ status,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status: %w", err)
}
// Send out the approval as Accept.
@@ -279,6 +301,10 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
log.Errorf(ctx, "error updating account stats: %v", err)
}
+ if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
+ log.Errorf(ctx, "error timelining and notifying status: %v", err)
+ }
+
if status.InReplyToID != "" {
// Interaction counts changed on the replied status; uncache the
// prepared version from all timelines. The status dereferencer
@@ -286,10 +312,6 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
}
- if err := p.surface.timelineAndNotifyStatus(ctx, status); err != nil {
- log.Errorf(ctx, "error timelining and notifying status: %v", err)
- }
-
return nil
}
@@ -320,9 +342,6 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
status := vote.Poll.Status
status.Poll = vote.Poll
- // Interaction counts changed on the source status, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
-
if *status.Local {
// Before federating it, increment the
// poll vote counts on our local copy.
@@ -335,6 +354,9 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
}
}
+ // Interaction counts changed on the source status, uncache from timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
+
return nil
}
@@ -409,10 +431,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// If pending approval is true then
// fave must target a LOCAL status
// that requires approval for the fave.
- pendingApproval := util.PtrOrValue(
- fave.PendingApproval,
- false,
- )
+ pendingApproval := util.PtrOrZero(fave.PendingApproval)
switch {
case pendingApproval && !fave.PreApproved:
@@ -420,10 +439,8 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending fave.
- if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
- log.Errorf(ctx, "error notifying pending fave: %v", err)
+ if err := p.utils.requestFave(ctx, fave); err != nil {
+ return gtserror.Newf("error pending fave: %w", err)
}
// Return early.
@@ -437,11 +454,33 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Put approval in the database and
- // update the fave with approvedBy URI.
- approval, err := p.utils.approveFave(ctx, fave)
- if err != nil {
- return gtserror.Newf("error pre-approving fave: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: fave.StatusID,
+ TargetAccountID: fave.TargetAccountID,
+ TargetAccount: fave.TargetAccount,
+ InteractingAccountID: fave.AccountID,
+ InteractingAccount: fave.Account,
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Like: fave,
+ URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+
+ // Mark the fave itself as now approved.
+ fave.PendingApproval = util.Ptr(false)
+ fave.PreApproved = false
+ fave.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatusFave(
+ ctx,
+ fave,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status fave: %w", err)
}
// Send out the approval as Accept.
@@ -496,10 +535,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// If pending approval is true then
// boost must target a LOCAL status
// that requires approval for the boost.
- pendingApproval := util.PtrOrValue(
- boost.PendingApproval,
- false,
- )
+ pendingApproval := util.PtrOrZero(boost.PendingApproval)
switch {
case pendingApproval && !boost.PreApproved:
@@ -507,10 +543,8 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// preapproved, then just notify the account
// that's being interacted with: they can
// approve or deny the interaction later.
-
- // Notify *local* account of pending announce.
- if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
- log.Errorf(ctx, "error notifying pending boost: %v", err)
+ if err := p.utils.requestAnnounce(ctx, boost); err != nil {
+ return gtserror.Newf("error pending boost: %w", err)
}
// Return early.
@@ -524,11 +558,33 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
// collection. Do the Accept immediately and
// then process everything else as normal.
- // Put approval in the database and
- // update the boost with approvedBy URI.
- approval, err := p.utils.approveAnnounce(ctx, boost)
- if err != nil {
- return gtserror.Newf("error pre-approving boost: %w", err)
+ // Store an already-accepted interaction request.
+ id := id.NewULID()
+ approval := &gtsmodel.InteractionRequest{
+ ID: id,
+ StatusID: boost.BoostOfID,
+ TargetAccountID: boost.BoostOfAccountID,
+ TargetAccount: boost.BoostOfAccount,
+ InteractingAccountID: boost.AccountID,
+ InteractingAccount: boost.Account,
+ InteractionURI: boost.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Announce: boost,
+ URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
+ AcceptedAt: time.Now(),
+ }
+
+ // Mark the boost itself as now approved.
+ boost.PendingApproval = util.Ptr(false)
+ boost.PreApproved = false
+ boost.ApprovedByURI = approval.URI
+ if err := p.state.DB.UpdateStatus(
+ ctx,
+ boost,
+ "pending_approval",
+ "approved_by_uri",
+ ); err != nil {
+ return gtserror.Newf("db error updating status: %w", err)
}
// Send out the approval as Accept.
@@ -729,15 +785,15 @@ func (p *fediAPI) AcceptReply(ctx context.Context, fMsg *messages.FromFediAPI) e
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
- // Interaction counts changed on the replied-to status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
-
// Send out the reply again, fully this time.
if err := p.federate.CreateStatus(ctx, status); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
+ // Interaction counts changed on the replied-to status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
+
return nil
}
@@ -757,15 +813,15 @@ func (p *fediAPI) AcceptAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
log.Errorf(ctx, "error timelining and notifying status: %v", err)
}
- // Interaction counts changed on the boosted status;
- // uncache the prepared version from all timelines.
- p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
-
// Send out the boost again, fully this time.
if err := p.federate.Announce(ctx, boost); err != nil {
log.Errorf(ctx, "error federating announce: %v", err)
}
+ // Interaction counts changed on the boosted status;
+ // uncache the prepared version from all timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, boost.BoostOfID)
+
return nil
}
@@ -792,9 +848,6 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
log.Errorf(ctx, "error refreshing status: %v", err)
}
- // Status representation was refetched, uncache from timelines.
- p.surface.invalidateStatusFromTimelines(ctx, status.ID)
-
if status.Poll != nil && status.Poll.Closing {
// If the latest status has a newly closed poll, at least compared
@@ -809,6 +862,9 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
log.Errorf(ctx, "error streaming status edit: %v", err)
}
+ // Status representation was refetched, uncache from timelines.
+ p.surface.invalidateStatusFromTimelines(ctx, status.ID)
+
return nil
}
diff --git a/internal/processing/workers/fromfediapi_test.go b/internal/processing/workers/fromfediapi_test.go
index f08f059ea..d7d7454e7 100644
--- a/internal/processing/workers/fromfediapi_test.go
+++ b/internal/processing/workers/fromfediapi_test.go
@@ -42,8 +42,8 @@ type FromFediAPITestSuite struct {
// remote_account_1 boosts the first status of local_account_1
func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
boostedStatus := &gtsmodel.Status{}
*boostedStatus = *suite.testStatuses["local_account_1_status_1"]
@@ -106,8 +106,8 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
}
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
repliedAccount := &gtsmodel.Account{}
*repliedAccount = *suite.testAccounts["local_account_1"]
@@ -190,8 +190,8 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
}
func (suite *FromFediAPITestSuite) TestProcessFave() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
favedAccount := suite.testAccounts["local_account_1"]
favedStatus := suite.testStatuses["local_account_1_status_1"]
@@ -262,8 +262,8 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
// This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own
// the fave, but just follow the actor who received the fave.
func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
receivingAccount := suite.testAccounts["local_account_2"]
favedAccount := suite.testAccounts["local_account_1"]
@@ -327,8 +327,8 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
}
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
ctx := context.Background()
@@ -421,8 +421,8 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
}
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
ctx := context.Background()
@@ -478,8 +478,8 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
}
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
ctx := context.Background()
@@ -579,8 +579,8 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
ctx := context.Background()
@@ -604,8 +604,8 @@ func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
}
func (suite *FromFediAPITestSuite) TestMoveAccount() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
// We're gonna migrate foss_satan to our local admin account.
ctx := context.Background()
diff --git a/internal/processing/workers/surfacenotify_test.go b/internal/processing/workers/surfacenotify_test.go
index 876f69933..dc445d0ac 100644
--- a/internal/processing/workers/surfacenotify_test.go
+++ b/internal/processing/workers/surfacenotify_test.go
@@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
+ "github.com/superseriousbusiness/gotosocial/testrig"
)
type SurfaceNotifyTestSuite struct {
@@ -35,8 +36,8 @@ type SurfaceNotifyTestSuite struct {
}
func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
- testStructs := suite.SetupTestStructs()
- defer suite.TearDownTestStructs(testStructs)
+ testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
+ defer testrig.TearDownTestStructs(testStructs)
surface := &workers.Surface{
State: testStructs.State,
diff --git a/internal/processing/workers/util.go b/internal/processing/workers/util.go
index 49c6183a4..bb7faffbf 100644
--- a/internal/processing/workers/util.go
+++ b/internal/processing/workers/util.go
@@ -26,12 +26,11 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"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/processing/account"
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/state"
- "github.com/superseriousbusiness/gotosocial/internal/uris"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@@ -488,128 +487,143 @@ func (u *utils) decrementFollowRequestsCount(
return nil
}
-// approveFave stores + returns an
-// interactionApproval for a fave.
-func (u *utils) approveFave(
+// requestFave stores an interaction request
+// for the given fave, and notifies the interactee.
+func (u *utils) requestFave(
ctx context.Context,
fave *gtsmodel.StatusFave,
-) (*gtsmodel.InteractionApproval, error) {
- id := id.NewULID()
+) error {
+ // Only create interaction request
+ // if fave targets a local status.
+ if fave.Status == nil ||
+ !fave.Status.IsLocal() {
+ return nil
+ }
- approval := &gtsmodel.InteractionApproval{
- ID: id,
- AccountID: fave.TargetAccountID,
- Account: fave.TargetAccount,
- InteractingAccountID: fave.AccountID,
- InteractingAccount: fave.Account,
- InteractionURI: fave.URI,
- InteractionType: gtsmodel.InteractionLike,
- URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
+ // Lock on the interaction URI.
+ unlock := u.state.ProcessingLocks.Lock(fave.URI)
+ defer unlock()
+
+ // Ensure no req with this URI exists already.
+ req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, fave.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error checking for existing interaction request: %w", err)
}
- if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
- err := gtserror.Newf("db error inserting interaction approval: %w", err)
- return nil, err
+ if req != nil {
+ // Interaction req already exists,
+ // no need to do anything else.
+ return nil
}
- // Mark the fave itself as now approved.
- fave.PendingApproval = util.Ptr(false)
- fave.PreApproved = false
- fave.ApprovedByURI = approval.URI
+ // Create + store new interaction request.
+ req, err = typeutils.StatusFaveToInteractionRequest(ctx, fave)
+ if err != nil {
+ return gtserror.Newf("error creating interaction request: %w", err)
+ }
- if err := u.state.DB.UpdateStatusFave(
- ctx,
- fave,
- "pending_approval",
- "approved_by_uri",
- ); err != nil {
- err := gtserror.Newf("db error updating status fave: %w", err)
- return nil, err
+ if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ return gtserror.Newf("db error storing interaction request: %w", err)
}
- return approval, nil
+ // Notify *local* account of pending announce.
+ if err := u.surface.notifyPendingFave(ctx, fave); err != nil {
+ return gtserror.Newf("error notifying pending fave: %w", err)
+ }
+
+ return nil
}
-// approveReply stores + returns an
-// interactionApproval for a reply.
-func (u *utils) approveReply(
+// requestReply stores an interaction request
+// for the given reply, and notifies the interactee.
+func (u *utils) requestReply(
ctx context.Context,
- status *gtsmodel.Status,
-) (*gtsmodel.InteractionApproval, error) {
- id := id.NewULID()
+ reply *gtsmodel.Status,
+) error {
+ // Only create interaction request if
+ // status replies to a local status.
+ if reply.InReplyTo == nil ||
+ !reply.InReplyTo.IsLocal() {
+ return nil
+ }
- approval := &gtsmodel.InteractionApproval{
- ID: id,
- AccountID: status.InReplyToAccountID,
- Account: status.InReplyToAccount,
- InteractingAccountID: status.AccountID,
- InteractingAccount: status.Account,
- InteractionURI: status.URI,
- InteractionType: gtsmodel.InteractionReply,
- URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
+ // Lock on the interaction URI.
+ unlock := u.state.ProcessingLocks.Lock(reply.URI)
+ defer unlock()
+
+ // Ensure no req with this URI exists already.
+ req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, reply.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error checking for existing interaction request: %w", err)
}
- if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
- err := gtserror.Newf("db error inserting interaction approval: %w", err)
- return nil, err
+ if req != nil {
+ // Interaction req already exists,
+ // no need to do anything else.
+ return nil
}
- // Mark the status itself as now approved.
- status.PendingApproval = util.Ptr(false)
- status.PreApproved = false
- status.ApprovedByURI = approval.URI
+ // Create + store interaction request.
+ req, err = typeutils.StatusToInteractionRequest(ctx, reply)
+ if err != nil {
+ return gtserror.Newf("error creating interaction request: %w", err)
+ }
- if err := u.state.DB.UpdateStatus(
- ctx,
- status,
- "pending_approval",
- "approved_by_uri",
- ); err != nil {
- err := gtserror.Newf("db error updating status: %w", err)
- return nil, err
+ if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ return gtserror.Newf("db error storing interaction request: %w", err)
+ }
+
+ // Notify *local* account of pending reply.
+ if err := u.surface.notifyPendingReply(ctx, reply); err != nil {
+ return gtserror.Newf("error notifying pending reply: %w", err)
}
- return approval, nil
+ return nil
}
-// approveAnnounce stores + returns an
-// interactionApproval for an announce.
-func (u *utils) approveAnnounce(
+// requestAnnounce stores an interaction request
+// for the given announce, and notifies the interactee.
+func (u *utils) requestAnnounce(
ctx context.Context,
boost *gtsmodel.Status,
-) (*gtsmodel.InteractionApproval, error) {
- id := id.NewULID()
+) error {
+ // Only create interaction request if
+ // status announces a local status.
+ if boost.BoostOf == nil ||
+ !boost.BoostOf.IsLocal() {
+ return nil
+ }
+
+ // Lock on the interaction URI.
+ unlock := u.state.ProcessingLocks.Lock(boost.URI)
+ defer unlock()
- approval := &gtsmodel.InteractionApproval{
- ID: id,
- AccountID: boost.BoostOfAccountID,
- Account: boost.BoostOfAccount,
- InteractingAccountID: boost.AccountID,
- InteractingAccount: boost.Account,
- InteractionURI: boost.URI,
- InteractionType: gtsmodel.InteractionReply,
- URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
+ // Ensure no req with this URI exists already.
+ req, err := u.state.DB.GetInteractionRequestByInteractionURI(ctx, boost.URI)
+ if err != nil && !errors.Is(err, db.ErrNoEntries) {
+ return gtserror.Newf("db error checking for existing interaction request: %w", err)
}
- if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
- err := gtserror.Newf("db error inserting interaction approval: %w", err)
- return nil, err
+ if req != nil {
+ // Interaction req already exists,
+ // no need to do anything else.
+ return nil
}
- // Mark the status itself as now approved.
- boost.PendingApproval = util.Ptr(false)
- boost.PreApproved = false
- boost.ApprovedByURI = approval.URI
+ // Create + store interaction request.
+ req, err = typeutils.StatusToInteractionRequest(ctx, boost)
+ if err != nil {
+ return gtserror.Newf("error creating interaction request: %w", err)
+ }
- if err := u.state.DB.UpdateStatus(
- ctx,
- boost,
- "pending_approval",
- "approved_by_uri",
- ); err != nil {
- err := gtserror.Newf("db error updating boost wrapper status: %w", err)
- return nil, err
+ if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
+ return gtserror.Newf("db error storing interaction request: %w", err)
+ }
+
+ // Notify *local* account of pending announce.
+ if err := u.surface.notifyPendingAnnounce(ctx, boost); err != nil {
+ return gtserror.Newf("error notifying pending announce: %w", err)
}
- return approval, nil
+ return nil
}
diff --git a/internal/processing/workers/workers.go b/internal/processing/workers/workers.go
index 04010a92e..d4b525783 100644
--- a/internal/processing/workers/workers.go
+++ b/internal/processing/workers/workers.go
@@ -22,6 +22,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
+ "github.com/superseriousbusiness/gotosocial/internal/processing/common"
"github.com/superseriousbusiness/gotosocial/internal/processing/conversations"
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
@@ -38,6 +39,7 @@ type Processor struct {
func New(
state *state.State,
+ common *common.Processor,
federator *federation.Federator,
converter *typeutils.Converter,
visFilter *visibility.Filter,
@@ -82,6 +84,7 @@ func New(
surface: surface,
federate: federate,
account: account,
+ common: common,
utils: utils,
},
fediAPI: fediAPI{
@@ -89,6 +92,7 @@ func New(
surface: surface,
federate: federate,
account: account,
+ common: common,
utils: utils,
},
}
diff --git a/internal/processing/workers/workers_test.go b/internal/processing/workers/workers_test.go
index 65ed3f6b7..ffd40d8fb 100644
--- a/internal/processing/workers/workers_test.go
+++ b/internal/processing/workers/workers_test.go
@@ -21,19 +21,18 @@ import (
"context"
"github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/cleaner"
- "github.com/superseriousbusiness/gotosocial/internal/email"
- "github.com/superseriousbusiness/gotosocial/internal/filter/interaction"
- "github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
- "github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/stream"
- "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
+const (
+ rMediaPath = "../../../testrig/media"
+ rTemplatePath = "../../../web/template"
+)
+
type WorkersTestSuite struct {
// standard suite interfaces
suite.Suite
@@ -56,23 +55,6 @@ type WorkersTestSuite struct {
testListEntries map[string]*gtsmodel.ListEntry
}
-// TestStructs encapsulates structs needed to
-// run one test in this package. Each test should
-// call SetupTestStructs to get a new TestStructs,
-// and defer TearDownTestStructs to close it when
-// the test is complete. The reason for doing things
-// this way here is to prevent the tests in this
-// package from overwriting one another's processors
-// and worker queues, which was causing issues
-// when running all tests at once.
-type TestStructs struct {
- State *state.State
- Processor *processing.Processor
- HTTPClient *testrig.MockHTTPClient
- TypeConverter *typeutils.Converter
- EmailSender email.Sender
-}
-
func (suite *WorkersTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
@@ -132,63 +114,3 @@ func (suite *WorkersTestSuite) openStreams(ctx context.Context, processor *proce
return streams
}
-
-func (suite *WorkersTestSuite) SetupTestStructs() *TestStructs {
- state := state.State{}
-
- state.Caches.Init()
-
- db := testrig.NewTestDB(&state)
- state.DB = db
-
- storage := testrig.NewInMemoryStorage()
- state.Storage = storage
- typeconverter := typeutils.NewConverter(&state)
-
- testrig.StartTimelines(
- &state,
- visibility.NewFilter(&state),
- typeconverter,
- )
-
- httpClient := testrig.NewMockHTTPClient(nil, "../../../testrig/media")
- httpClient.TestRemotePeople = testrig.NewTestFediPeople()
- httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
-
- transportController := testrig.NewTestTransportController(&state, httpClient)
- mediaManager := testrig.NewTestMediaManager(&state)
- federator := testrig.NewTestFederator(&state, transportController, mediaManager)
- oauthServer := testrig.NewTestOauthServer(db)
- emailSender := testrig.NewEmailSender("../../../web/template/", nil)
-
- processor := processing.NewProcessor(
- cleaner.New(&state),
- typeconverter,
- federator,
- oauthServer,
- mediaManager,
- &state,
- emailSender,
- visibility.NewFilter(&state),
- interaction.NewFilter(&state),
- )
-
- testrig.StartWorkers(&state, processor.Workers())
-
- testrig.StandardDBSetup(db, suite.testAccounts)
- testrig.StandardStorageSetup(storage, "../../../testrig/media")
-
- return &TestStructs{
- State: &state,
- Processor: processor,
- HTTPClient: httpClient,
- TypeConverter: typeconverter,
- EmailSender: emailSender,
- }
-}
-
-func (suite *WorkersTestSuite) TearDownTestStructs(testStructs *TestStructs) {
- testrig.StandardDBTeardown(testStructs.State.DB)
- testrig.StandardStorageTeardown(testStructs.State.Storage)
- testrig.StopWorkers(testStructs.State)
-}
diff --git a/internal/timeline/prune_test.go b/internal/timeline/prune_test.go
index 2cc3ef71a..e78db64e8 100644
--- a/internal/timeline/prune_test.go
+++ b/internal/timeline/prune_test.go
@@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
- suite.Equal(19, pruned)
+ suite.Equal(20, pruned)
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
@@ -56,7 +56,7 @@ func (suite *PruneTestSuite) TestPruneTwice() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
- suite.Equal(19, pruned)
+ suite.Equal(20, pruned)
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
// Prune same again, nothing should be pruned this time.
@@ -78,7 +78,7 @@ func (suite *PruneTestSuite) TestPruneTo0() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
- suite.Equal(24, pruned)
+ suite.Equal(25, pruned)
suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
@@ -95,7 +95,7 @@ func (suite *PruneTestSuite) TestPruneToInfinityAndBeyond() {
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
suite.NoError(err)
suite.Equal(0, pruned)
- suite.Equal(24, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
+ suite.Equal(25, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
}
func TestPruneTestSuite(t *testing.T) {
diff --git a/internal/typeutils/internal.go b/internal/typeutils/internal.go
index 75da4b27e..ed4ed4dd9 100644
--- a/internal/typeutils/internal.go
+++ b/internal/typeutils/internal.go
@@ -20,6 +20,7 @@ package typeutils
import (
"context"
+ "github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/uris"
@@ -97,3 +98,80 @@ func (c *Converter) StatusToBoost(
return boost, nil
}
+
+func StatusToInteractionRequest(
+ ctx context.Context,
+ status *gtsmodel.Status,
+) (*gtsmodel.InteractionRequest, error) {
+ reqID, err := id.NewULIDFromTime(status.CreatedAt)
+ if err != nil {
+ return nil, gtserror.Newf("error generating ID: %w", err)
+ }
+
+ var (
+ targetID string
+ target *gtsmodel.Status
+ targetAccountID string
+ targetAccount *gtsmodel.Account
+ interactionType gtsmodel.InteractionType
+ reply *gtsmodel.Status
+ announce *gtsmodel.Status
+ )
+
+ if status.InReplyToID != "" {
+ // It's a reply.
+ targetID = status.InReplyToID
+ target = status.InReplyTo
+ targetAccountID = status.InReplyToAccountID
+ targetAccount = status.InReplyToAccount
+ interactionType = gtsmodel.InteractionReply
+ reply = status
+ } else {
+ // It's a boost.
+ targetID = status.BoostOfID
+ target = status.BoostOf
+ targetAccountID = status.BoostOfAccountID
+ targetAccount = status.BoostOfAccount
+ interactionType = gtsmodel.InteractionAnnounce
+ announce = status
+ }
+
+ return &gtsmodel.InteractionRequest{
+ ID: reqID,
+ CreatedAt: status.CreatedAt,
+ StatusID: targetID,
+ Status: target,
+ TargetAccountID: targetAccountID,
+ TargetAccount: targetAccount,
+ InteractingAccountID: status.AccountID,
+ InteractingAccount: status.Account,
+ InteractionURI: status.URI,
+ InteractionType: interactionType,
+ Reply: reply,
+ Announce: announce,
+ }, nil
+}
+
+func StatusFaveToInteractionRequest(
+ ctx context.Context,
+ fave *gtsmodel.StatusFave,
+) (*gtsmodel.InteractionRequest, error) {
+ reqID, err := id.NewULIDFromTime(fave.CreatedAt)
+ if err != nil {
+ return nil, gtserror.Newf("error generating ID: %w", err)
+ }
+
+ return &gtsmodel.InteractionRequest{
+ ID: reqID,
+ CreatedAt: fave.CreatedAt,
+ StatusID: fave.StatusID,
+ Status: fave.Status,
+ TargetAccountID: fave.TargetAccountID,
+ TargetAccount: fave.TargetAccount,
+ InteractingAccountID: fave.AccountID,
+ InteractingAccount: fave.Account,
+ InteractionURI: fave.URI,
+ InteractionType: gtsmodel.InteractionLike,
+ Like: fave,
+ }, nil
+}
diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go
index 31b256b6c..03710bec8 100644
--- a/internal/typeutils/internaltoas.go
+++ b/internal/typeutils/internaltoas.go
@@ -1960,36 +1960,36 @@ func (c *Converter) InteractionPolicyToASInteractionPolicy(
return policy, nil
}
-// InteractionApprovalToASAccept converts a *gtsmodel.InteractionApproval
+// InteractionReqToASAccept converts a *gtsmodel.InteractionRequest
// to an ActivityStreams Accept, addressed to the interacting account.
-func (c *Converter) InteractionApprovalToASAccept(
+func (c *Converter) InteractionReqToASAccept(
ctx context.Context,
- approval *gtsmodel.InteractionApproval,
+ req *gtsmodel.InteractionRequest,
) (vocab.ActivityStreamsAccept, error) {
accept := streams.NewActivityStreamsAccept()
- acceptID, err := url.Parse(approval.URI)
+ acceptID, err := url.Parse(req.URI)
if err != nil {
return nil, gtserror.Newf("invalid accept uri: %w", err)
}
- actorIRI, err := url.Parse(approval.Account.URI)
+ actorIRI, err := url.Parse(req.TargetAccount.URI)
if err != nil {
return nil, gtserror.Newf("invalid account uri: %w", err)
}
- objectIRI, err := url.Parse(approval.InteractionURI)
+ objectIRI, err := url.Parse(req.InteractionURI)
if err != nil {
return nil, gtserror.Newf("invalid target uri: %w", err)
}
- toIRI, err := url.Parse(approval.InteractingAccount.URI)
+ toIRI, err := url.Parse(req.InteractingAccount.URI)
if err != nil {
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
}
// Set id to the URI of
- // interactionApproval.
+ // interaction request.
ap.SetJSONLDId(accept, acceptID)
// Actor is the account that
diff --git a/internal/typeutils/internaltoas_test.go b/internal/typeutils/internaltoas_test.go
index 905dccfad..50719c0b4 100644
--- a/internal/typeutils/internaltoas_test.go
+++ b/internal/typeutils/internaltoas_test.go
@@ -1057,26 +1057,26 @@ func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
}`, string(bytes))
}
-func (suite *InternalToASTestSuite) TestInteractionApprovalToASAccept() {
+func (suite *InternalToASTestSuite) TestInteractionReqToASAccept() {
acceptingAccount := suite.testAccounts["local_account_1"]
interactingAccount := suite.testAccounts["remote_account_1"]
- interactionApproval := &gtsmodel.InteractionApproval{
+ req := &gtsmodel.InteractionRequest{
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
- UpdatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
- AccountID: acceptingAccount.ID,
- Account: acceptingAccount,
+ TargetAccountID: acceptingAccount.ID,
+ TargetAccount: acceptingAccount,
InteractingAccountID: interactingAccount.ID,
InteractingAccount: interactingAccount,
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
InteractionType: gtsmodel.InteractionAnnounce,
URI: "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
+ AcceptedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
}
- accept, err := suite.typeconverter.InteractionApprovalToASAccept(
+ accept, err := suite.typeconverter.InteractionReqToASAccept(
context.Background(),
- interactionApproval,
+ req,
)
if err != nil {
suite.FailNow(err.Error())
diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go
index 4ed65bb08..07a4c0836 100644
--- a/internal/typeutils/internaltofrontend.go
+++ b/internal/typeutils/internaltofrontend.go
@@ -2592,3 +2592,74 @@ func policyValsToAPIPolicyVals(vals gtsmodel.PolicyValues) []apimodel.PolicyValu
return apiVals
}
+
+// InteractionReqToAPIInteractionReq converts the given *gtsmodel.InteractionRequest
+// to an *apimodel.InteractionRequest, from the perspective of requestingAcct.
+func (c *Converter) InteractionReqToAPIInteractionReq(
+ ctx context.Context,
+ req *gtsmodel.InteractionRequest,
+ requestingAcct *gtsmodel.Account,
+) (*apimodel.InteractionRequest, error) {
+ // Ensure interaction request is populated.
+ if err := c.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
+ err := gtserror.Newf("error populating: %w", err)
+ return nil, err
+ }
+
+ interactingAcct, err := c.AccountToAPIAccountPublic(ctx, req.InteractingAccount)
+ if err != nil {
+ err := gtserror.Newf("error converting interacting acct: %w", err)
+ return nil, err
+ }
+
+ interactedStatus, err := c.StatusToAPIStatus(
+ ctx,
+ req.Status,
+ requestingAcct,
+ statusfilter.FilterContextNone,
+ nil,
+ nil,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting interacted status: %w", err)
+ return nil, err
+ }
+
+ var reply *apimodel.Status
+ if req.InteractionType == gtsmodel.InteractionReply {
+ reply, err = c.StatusToAPIStatus(
+ ctx,
+ req.Reply,
+ requestingAcct,
+ statusfilter.FilterContextNone,
+ nil,
+ nil,
+ )
+ if err != nil {
+ err := gtserror.Newf("error converting reply: %w", err)
+ return nil, err
+ }
+ }
+
+ var acceptedAt string
+ if req.IsAccepted() {
+ acceptedAt = util.FormatISO8601(req.AcceptedAt)
+ }
+
+ var rejectedAt string
+ if req.IsRejected() {
+ rejectedAt = util.FormatISO8601(req.RejectedAt)
+ }
+
+ return &apimodel.InteractionRequest{
+ ID: req.ID,
+ Type: req.InteractionType.String(),
+ CreatedAt: util.FormatISO8601(req.CreatedAt),
+ Account: interactingAcct,
+ Status: interactedStatus,
+ Reply: reply,
+ AcceptedAt: acceptedAt,
+ RejectedAt: rejectedAt,
+ URI: req.URI,
+ }, nil
+}
diff --git a/internal/uris/uri.go b/internal/uris/uri.go
index 159508176..e1783b26c 100644
--- a/internal/uris/uri.go
+++ b/internal/uris/uri.go
@@ -46,7 +46,8 @@ const (
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
TagsPath = "tags" // TagsPath represents the activitypub tags location
- AcceptsPath = "accepts" // AcceptsPath represents the activitypub accepts location
+ AcceptsPath = "accepts" // AcceptsPath represents the activitypub Accept's location
+ RejectsPath = "rejects" // RejectsPath represents the activitypub Reject's location
)
// UserURIs contains a bunch of UserURIs and URLs for a user, host, account, etc.
@@ -137,7 +138,7 @@ func GenerateURIForEmailConfirm(token string) string {
return fmt.Sprintf("%s://%s/%s?token=%s", protocol, host, ConfirmEmailPath, token)
}
-// GenerateURIForAccept returns the AP URI for a new accept activity -- something like:
+// GenerateURIForAccept returns the AP URI for a new Accept activity -- something like:
// https://example.org/users/whatever_user/accepts/01F7XTH1QGBAPMGF49WJZ91XGC
func GenerateURIForAccept(username string, thisAcceptID string) string {
protocol := config.GetProtocol()
@@ -145,6 +146,14 @@ func GenerateURIForAccept(username string, thisAcceptID string) string {
return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, AcceptsPath, thisAcceptID)
}
+// GenerateURIForReject returns the AP URI for a new Reject activity -- something like:
+// https://example.org/users/whatever_user/rejects/01F7XTH1QGBAPMGF49WJZ91XGC
+func GenerateURIForReject(username string, thisRejectID string) string {
+ protocol := config.GetProtocol()
+ host := config.GetHost()
+ return fmt.Sprintf("%s://%s/%s/%s/%s/%s", protocol, host, UsersPath, username, RejectsPath, thisRejectID)
+}
+
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
func GenerateURIsForAccount(username string) *UserURIs {
protocol := config.GetProtocol()