[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
This commit is contained in:
parent
8e5a72ac5c
commit
f23f04e0b1
|
@ -2009,6 +2009,47 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
x-go-name: PolicyValue
|
x-go-name: PolicyValue
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
interactionRequest:
|
||||||
|
properties:
|
||||||
|
accepted_at:
|
||||||
|
description: The timestamp that the interaction request was accepted (ISO 8601 Datetime). Field omitted if request not accepted (yet).
|
||||||
|
type: string
|
||||||
|
x-go-name: AcceptedAt
|
||||||
|
account:
|
||||||
|
$ref: '#/definitions/account'
|
||||||
|
created_at:
|
||||||
|
description: The timestamp of the interaction request (ISO 8601 Datetime)
|
||||||
|
type: string
|
||||||
|
x-go-name: CreatedAt
|
||||||
|
id:
|
||||||
|
description: The id of the interaction request in the database.
|
||||||
|
type: string
|
||||||
|
x-go-name: ID
|
||||||
|
rejected_at:
|
||||||
|
description: The timestamp that the interaction request was rejected (ISO 8601 Datetime). Field omitted if request not rejected (yet).
|
||||||
|
type: string
|
||||||
|
x-go-name: RejectedAt
|
||||||
|
reply:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
status:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
type:
|
||||||
|
description: |-
|
||||||
|
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
|
||||||
|
x-go-name: Type
|
||||||
|
uri:
|
||||||
|
description: URI of the Accept or Reject. Only set if accepted_at or rejected_at is set, else omitted.
|
||||||
|
type: string
|
||||||
|
x-go-name: URI
|
||||||
|
title: InteractionRequest represents a pending, approved, or rejected interaction of type favourite, reply, or reblog.
|
||||||
|
type: object
|
||||||
|
x-go-name: InteractionRequest
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
list:
|
list:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
@ -7521,6 +7562,177 @@ paths:
|
||||||
summary: Update default interaction policies per visibility level for new statuses created by you.
|
summary: Update default interaction policies per visibility level for new statuses created by you.
|
||||||
tags:
|
tags:
|
||||||
- interaction_policies
|
- interaction_policies
|
||||||
|
/api/v1/interaction_requests:
|
||||||
|
get:
|
||||||
|
description: |-
|
||||||
|
```
|
||||||
|
<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"
|
||||||
|
````
|
||||||
|
operationId: getInteractionRequests
|
||||||
|
parameters:
|
||||||
|
- description: If set, then only interactions targeting the given status_id will be included in the results.
|
||||||
|
in: query
|
||||||
|
name: status_id
|
||||||
|
type: string
|
||||||
|
- default: true
|
||||||
|
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
|
||||||
|
name: favourites
|
||||||
|
type: boolean
|
||||||
|
- default: true
|
||||||
|
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
|
||||||
|
name: replies
|
||||||
|
type: boolean
|
||||||
|
- default: true
|
||||||
|
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
|
||||||
|
name: reblogs
|
||||||
|
type: boolean
|
||||||
|
- 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
|
||||||
|
name: max_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
|
||||||
|
name: since_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
|
||||||
|
name: min_id
|
||||||
|
type: string
|
||||||
|
- default: 40
|
||||||
|
description: Number of interaction requests to return.
|
||||||
|
in: query
|
||||||
|
maximum: 80
|
||||||
|
minimum: 1
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: ""
|
||||||
|
headers:
|
||||||
|
Link:
|
||||||
|
description: Links to the next and previous queries.
|
||||||
|
type: string
|
||||||
|
schema:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/interactionRequest'
|
||||||
|
type: array
|
||||||
|
"400":
|
||||||
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- read:notifications
|
||||||
|
summary: Get an array of interactions requested on your statuses by other accounts, and pending your approval.
|
||||||
|
tags:
|
||||||
|
- interaction_requests
|
||||||
|
/api/v1/interaction_requests/{id}:
|
||||||
|
get:
|
||||||
|
operationId: getInteractionRequest
|
||||||
|
parameters:
|
||||||
|
- description: ID of the interaction request targeting you.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
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
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- read:notifications
|
||||||
|
summary: Get interaction request with the given ID.
|
||||||
|
tags:
|
||||||
|
- interaction_requests
|
||||||
|
/api/v1/interaction_requests/{id}/authorize:
|
||||||
|
post:
|
||||||
|
operationId: authorizeInteractionRequest
|
||||||
|
parameters:
|
||||||
|
- description: ID of the interaction request targeting you.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
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
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:statuses
|
||||||
|
summary: Accept/authorize/approve an interaction request with the given ID.
|
||||||
|
tags:
|
||||||
|
- interaction_requests
|
||||||
|
/api/v1/interaction_requests/{id}/reject:
|
||||||
|
post:
|
||||||
|
operationId: rejectInteractionRequest
|
||||||
|
parameters:
|
||||||
|
- description: ID of the interaction request targeting you.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
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
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:statuses
|
||||||
|
summary: Reject an interaction request with the given ID.
|
||||||
|
tags:
|
||||||
|
- interaction_requests
|
||||||
/api/v1/lists:
|
/api/v1/lists:
|
||||||
get:
|
get:
|
||||||
operationId: lists
|
operationId: lists
|
||||||
|
|
|
@ -25,7 +25,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"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) {
|
func (m *Module) AcceptGETHandler(c *gin.Context) {
|
||||||
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
username, errWithCode := apiutil.ParseUsername(c.Param(apiutil.UsernameKey))
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
|
@ -33,7 +33,7 @@ func (m *Module) AcceptGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
reqID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
|
@ -45,7 +45,7 @@ func (m *Module) AcceptGETHandler(c *gin.Context) {
|
||||||
return
|
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 {
|
if errWithCode != nil {
|
||||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
return
|
return
|
||||||
|
|
|
@ -38,6 +38,7 @@ import (
|
||||||
importdata "github.com/superseriousbusiness/gotosocial/internal/api/client/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/instance"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/interactionpolicies"
|
"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/lists"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/markers"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/markers"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/media"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/media"
|
||||||
|
@ -80,6 +81,7 @@ type Client struct {
|
||||||
importData *importdata.Module // api/v1/import
|
importData *importdata.Module // api/v1/import
|
||||||
instance *instance.Module // api/v1/instance
|
instance *instance.Module // api/v1/instance
|
||||||
interactionPolicies *interactionpolicies.Module // api/v1/interaction_policies
|
interactionPolicies *interactionpolicies.Module // api/v1/interaction_policies
|
||||||
|
interactionRequests *interactionrequests.Module // api/v1/interaction_requests
|
||||||
lists *lists.Module // api/v1/lists
|
lists *lists.Module // api/v1/lists
|
||||||
markers *markers.Module // api/v1/markers
|
markers *markers.Module // api/v1/markers
|
||||||
media *media.Module // api/v1/media, api/v2/media
|
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.importData.Route(h)
|
||||||
c.instance.Route(h)
|
c.instance.Route(h)
|
||||||
c.interactionPolicies.Route(h)
|
c.interactionPolicies.Route(h)
|
||||||
|
c.interactionRequests.Route(h)
|
||||||
c.lists.Route(h)
|
c.lists.Route(h)
|
||||||
c.markers.Route(h)
|
c.markers.Route(h)
|
||||||
c.media.Route(h)
|
c.media.Route(h)
|
||||||
|
@ -168,6 +171,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client {
|
||||||
importData: importdata.New(p),
|
importData: importdata.New(p),
|
||||||
instance: instance.New(p),
|
instance: instance.New(p),
|
||||||
interactionPolicies: interactionpolicies.New(p),
|
interactionPolicies: interactionpolicies.New(p),
|
||||||
|
interactionRequests: interactionrequests.New(p),
|
||||||
lists: lists.New(p),
|
lists: lists.New(p),
|
||||||
markers: markers.New(p),
|
markers: markers.New(p),
|
||||||
media: media.New(p),
|
media: media.New(p),
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
|
@ -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"`
|
||||||
|
}
|
|
@ -91,6 +91,13 @@ const (
|
||||||
AdminPermissionsKey = "permissions"
|
AdminPermissionsKey = "permissions"
|
||||||
AdminRoleIDsKey = "role_ids[]"
|
AdminRoleIDsKey = "role_ids[]"
|
||||||
AdminInvitedByKey = "invited_by"
|
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)
|
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.
|
Parse functions for *REQUIRED* parameters.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -81,7 +81,7 @@ func (c *Caches) Init() {
|
||||||
c.initFollowRequestIDs()
|
c.initFollowRequestIDs()
|
||||||
c.initInReplyToIDs()
|
c.initInReplyToIDs()
|
||||||
c.initInstance()
|
c.initInstance()
|
||||||
c.initInteractionApproval()
|
c.initInteractionRequest()
|
||||||
c.initList()
|
c.initList()
|
||||||
c.initListEntry()
|
c.initListEntry()
|
||||||
c.initMarker()
|
c.initMarker()
|
||||||
|
@ -158,7 +158,7 @@ func (c *Caches) Sweep(threshold float64) {
|
||||||
c.DB.FollowRequestIDs.Trim(threshold)
|
c.DB.FollowRequestIDs.Trim(threshold)
|
||||||
c.DB.InReplyToIDs.Trim(threshold)
|
c.DB.InReplyToIDs.Trim(threshold)
|
||||||
c.DB.Instance.Trim(threshold)
|
c.DB.Instance.Trim(threshold)
|
||||||
c.DB.InteractionApproval.Trim(threshold)
|
c.DB.InteractionRequest.Trim(threshold)
|
||||||
c.DB.List.Trim(threshold)
|
c.DB.List.Trim(threshold)
|
||||||
c.DB.ListEntry.Trim(threshold)
|
c.DB.ListEntry.Trim(threshold)
|
||||||
c.DB.Marker.Trim(threshold)
|
c.DB.Marker.Trim(threshold)
|
||||||
|
|
|
@ -106,8 +106,8 @@ type DBCaches struct {
|
||||||
// Instance provides access to the gtsmodel Instance database cache.
|
// Instance provides access to the gtsmodel Instance database cache.
|
||||||
Instance StructCache[*gtsmodel.Instance]
|
Instance StructCache[*gtsmodel.Instance]
|
||||||
|
|
||||||
// InteractionApproval provides access to the gtsmodel InteractionApproval database cache.
|
// InteractionRequest provides access to the gtsmodel InteractionRequest database cache.
|
||||||
InteractionApproval StructCache[*gtsmodel.InteractionApproval]
|
InteractionRequest StructCache[*gtsmodel.InteractionRequest]
|
||||||
|
|
||||||
// InReplyToIDs provides access to the status in reply to IDs list database cache.
|
// InReplyToIDs provides access to the status in reply to IDs list database cache.
|
||||||
InReplyToIDs SliceCache[string]
|
InReplyToIDs SliceCache[string]
|
||||||
|
@ -802,31 +802,36 @@ func (c *Caches) initInstance() {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Caches) initInteractionApproval() {
|
func (c *Caches) initInteractionRequest() {
|
||||||
// Calculate maximum cache size.
|
// Calculate maximum cache size.
|
||||||
cap := calculateResultCacheMax(
|
cap := calculateResultCacheMax(
|
||||||
sizeofInteractionApproval(),
|
sizeofInteractionRequest(),
|
||||||
config.GetCacheInteractionApprovalMemRatio(),
|
config.GetCacheInteractionRequestMemRatio(),
|
||||||
)
|
)
|
||||||
|
|
||||||
log.Infof(nil, "cache size = %d", cap)
|
log.Infof(nil, "cache size = %d", cap)
|
||||||
|
|
||||||
copyF := func(i1 *gtsmodel.InteractionApproval) *gtsmodel.InteractionApproval {
|
copyF := func(i1 *gtsmodel.InteractionRequest) *gtsmodel.InteractionRequest {
|
||||||
i2 := new(gtsmodel.InteractionApproval)
|
i2 := new(gtsmodel.InteractionRequest)
|
||||||
*i2 = *i1
|
*i2 = *i1
|
||||||
|
|
||||||
// Don't include ptr fields that
|
// Don't include ptr fields that
|
||||||
// will be populated separately.
|
// will be populated separately.
|
||||||
// See internal/db/bundb/interaction.go.
|
// See internal/db/bundb/interaction.go.
|
||||||
i2.Account = nil
|
i2.Status = nil
|
||||||
|
i2.TargetAccount = nil
|
||||||
i2.InteractingAccount = nil
|
i2.InteractingAccount = nil
|
||||||
|
i2.Like = nil
|
||||||
|
i2.Reply = nil
|
||||||
|
i2.Announce = nil
|
||||||
|
|
||||||
return i2
|
return i2
|
||||||
}
|
}
|
||||||
|
|
||||||
c.DB.InteractionApproval.Init(structr.CacheConfig[*gtsmodel.InteractionApproval]{
|
c.DB.InteractionRequest.Init(structr.CacheConfig[*gtsmodel.InteractionRequest]{
|
||||||
Indices: []structr.IndexConfig{
|
Indices: []structr.IndexConfig{
|
||||||
{Fields: "ID"},
|
{Fields: "ID"},
|
||||||
|
{Fields: "InteractionURI"},
|
||||||
{Fields: "URI"},
|
{Fields: "URI"},
|
||||||
},
|
},
|
||||||
MaxSize: cap,
|
MaxSize: cap,
|
||||||
|
|
|
@ -190,7 +190,7 @@ func totalOfRatios() float64 {
|
||||||
config.GetCacheFollowRequestMemRatio() +
|
config.GetCacheFollowRequestMemRatio() +
|
||||||
config.GetCacheFollowRequestIDsMemRatio() +
|
config.GetCacheFollowRequestIDsMemRatio() +
|
||||||
config.GetCacheInstanceMemRatio() +
|
config.GetCacheInstanceMemRatio() +
|
||||||
config.GetCacheInteractionApprovalMemRatio() +
|
config.GetCacheInteractionRequestMemRatio() +
|
||||||
config.GetCacheInReplyToIDsMemRatio() +
|
config.GetCacheInReplyToIDsMemRatio() +
|
||||||
config.GetCacheListMemRatio() +
|
config.GetCacheListMemRatio() +
|
||||||
config.GetCacheListEntryMemRatio() +
|
config.GetCacheListEntryMemRatio() +
|
||||||
|
@ -441,16 +441,17 @@ func sizeofInstance() uintptr {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
func sizeofInteractionApproval() uintptr {
|
func sizeofInteractionRequest() uintptr {
|
||||||
return uintptr(size.Of(>smodel.InteractionApproval{
|
return uintptr(size.Of(>smodel.InteractionRequest{
|
||||||
ID: exampleID,
|
ID: exampleID,
|
||||||
CreatedAt: exampleTime,
|
CreatedAt: exampleTime,
|
||||||
UpdatedAt: exampleTime,
|
StatusID: exampleID,
|
||||||
AccountID: exampleID,
|
TargetAccountID: exampleID,
|
||||||
InteractingAccountID: exampleID,
|
InteractingAccountID: exampleID,
|
||||||
InteractionURI: exampleURI,
|
InteractionURI: exampleURI,
|
||||||
InteractionType: gtsmodel.InteractionAnnounce,
|
InteractionType: gtsmodel.InteractionAnnounce,
|
||||||
URI: exampleURI,
|
URI: exampleURI,
|
||||||
|
AcceptedAt: exampleTime,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -218,7 +218,7 @@ type CacheConfiguration struct {
|
||||||
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
|
FollowRequestIDsMemRatio float64 `name:"follow-request-ids-mem-ratio"`
|
||||||
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
|
InReplyToIDsMemRatio float64 `name:"in-reply-to-ids-mem-ratio"`
|
||||||
InstanceMemRatio float64 `name:"instance-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"`
|
ListMemRatio float64 `name:"list-mem-ratio"`
|
||||||
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
|
ListEntryMemRatio float64 `name:"list-entry-mem-ratio"`
|
||||||
MarkerMemRatio float64 `name:"marker-mem-ratio"`
|
MarkerMemRatio float64 `name:"marker-mem-ratio"`
|
||||||
|
|
|
@ -181,7 +181,7 @@ var Defaults = Configuration{
|
||||||
FollowRequestIDsMemRatio: 2,
|
FollowRequestIDsMemRatio: 2,
|
||||||
InReplyToIDsMemRatio: 3,
|
InReplyToIDsMemRatio: 3,
|
||||||
InstanceMemRatio: 1,
|
InstanceMemRatio: 1,
|
||||||
InteractionApprovalMemRatio: 1,
|
InteractionRequestMemRatio: 1,
|
||||||
ListMemRatio: 1,
|
ListMemRatio: 1,
|
||||||
ListEntryMemRatio: 2,
|
ListEntryMemRatio: 2,
|
||||||
MarkerMemRatio: 0.5,
|
MarkerMemRatio: 0.5,
|
||||||
|
|
|
@ -3412,32 +3412,30 @@ func GetCacheInstanceMemRatio() float64 { return global.GetCacheInstanceMemRatio
|
||||||
// SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field
|
// SetCacheInstanceMemRatio safely sets the value for global configuration 'Cache.InstanceMemRatio' field
|
||||||
func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) }
|
func SetCacheInstanceMemRatio(v float64) { global.SetCacheInstanceMemRatio(v) }
|
||||||
|
|
||||||
// GetCacheInteractionApprovalMemRatio safely fetches the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
|
// GetCacheInteractionRequestMemRatio safely fetches the Configuration value for state's 'Cache.InteractionRequestMemRatio' field
|
||||||
func (st *ConfigState) GetCacheInteractionApprovalMemRatio() (v float64) {
|
func (st *ConfigState) GetCacheInteractionRequestMemRatio() (v float64) {
|
||||||
st.mutex.RLock()
|
st.mutex.RLock()
|
||||||
v = st.config.Cache.InteractionApprovalMemRatio
|
v = st.config.Cache.InteractionRequestMemRatio
|
||||||
st.mutex.RUnlock()
|
st.mutex.RUnlock()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCacheInteractionApprovalMemRatio safely sets the Configuration value for state's 'Cache.InteractionApprovalMemRatio' field
|
// SetCacheInteractionRequestMemRatio safely sets the Configuration value for state's 'Cache.InteractionRequestMemRatio' field
|
||||||
func (st *ConfigState) SetCacheInteractionApprovalMemRatio(v float64) {
|
func (st *ConfigState) SetCacheInteractionRequestMemRatio(v float64) {
|
||||||
st.mutex.Lock()
|
st.mutex.Lock()
|
||||||
defer st.mutex.Unlock()
|
defer st.mutex.Unlock()
|
||||||
st.config.Cache.InteractionApprovalMemRatio = v
|
st.config.Cache.InteractionRequestMemRatio = v
|
||||||
st.reloadToViper()
|
st.reloadToViper()
|
||||||
}
|
}
|
||||||
|
|
||||||
// CacheInteractionApprovalMemRatioFlag returns the flag name for the 'Cache.InteractionApprovalMemRatio' field
|
// CacheInteractionRequestMemRatioFlag returns the flag name for the 'Cache.InteractionRequestMemRatio' field
|
||||||
func CacheInteractionApprovalMemRatioFlag() string { return "cache-interaction-approval-mem-ratio" }
|
func CacheInteractionRequestMemRatioFlag() string { return "cache-interaction-request-mem-ratio" }
|
||||||
|
|
||||||
// GetCacheInteractionApprovalMemRatio safely fetches the value for global configuration 'Cache.InteractionApprovalMemRatio' field
|
// GetCacheInteractionRequestMemRatio safely fetches the value for global configuration 'Cache.InteractionRequestMemRatio' field
|
||||||
func GetCacheInteractionApprovalMemRatio() float64 {
|
func GetCacheInteractionRequestMemRatio() float64 { return global.GetCacheInteractionRequestMemRatio() }
|
||||||
return global.GetCacheInteractionApprovalMemRatio()
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetCacheInteractionApprovalMemRatio safely sets the value for global configuration 'Cache.InteractionApprovalMemRatio' field
|
// SetCacheInteractionRequestMemRatio safely sets the value for global configuration 'Cache.InteractionRequestMemRatio' field
|
||||||
func SetCacheInteractionApprovalMemRatio(v float64) { global.SetCacheInteractionApprovalMemRatio(v) }
|
func SetCacheInteractionRequestMemRatio(v float64) { global.SetCacheInteractionRequestMemRatio(v) }
|
||||||
|
|
||||||
// GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field
|
// GetCacheListMemRatio safely fetches the Configuration value for state's 'Cache.ListMemRatio' field
|
||||||
func (st *ConfigState) GetCacheListMemRatio() (v float64) {
|
func (st *ConfigState) GetCacheListMemRatio() (v float64) {
|
||||||
|
|
|
@ -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 {
|
if err := a.db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
var err 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().
|
statusesCount, err := tx.NewSelect().
|
||||||
Table("statuses").
|
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||||
Where("? = ?", bun.Ident("account_id"), account.ID).
|
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||||
|
Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
|
||||||
Count(ctx)
|
Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats.StatusesCount = &statusesCount
|
stats.StatusesCount = &statusesCount
|
||||||
|
|
||||||
// Scan database for pinned statuses.
|
// Scan database for pinned statuses, ignoring
|
||||||
|
// statuses that are currently pending approval.
|
||||||
statusesPinnedCount, err := tx.NewSelect().
|
statusesPinnedCount, err := tx.NewSelect().
|
||||||
Table("statuses").
|
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||||
Where("? = ?", bun.Ident("account_id"), account.ID).
|
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||||
Where("? IS NOT NULL", bun.Ident("pinned_at")).
|
Where("? IS NOT NULL", bun.Ident("status.pinned_at")).
|
||||||
|
Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
|
||||||
Count(ctx)
|
Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
stats.StatusesPinnedCount = &statusesPinnedCount
|
stats.StatusesPinnedCount = &statusesPinnedCount
|
||||||
|
|
||||||
// Scan database for last status.
|
// Scan database for last status, ignoring
|
||||||
|
// statuses that are currently pending approval.
|
||||||
lastStatusAt := time.Time{}
|
lastStatusAt := time.Time{}
|
||||||
err = tx.
|
err = tx.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
TableExpr("? AS ?", bun.Ident("statuses"), bun.Ident("status")).
|
||||||
Column("status.created_at").
|
Column("status.created_at").
|
||||||
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
Where("? = ?", bun.Ident("status.account_id"), account.ID).
|
||||||
|
Where("NOT ? = ?", bun.Ident("status.pending_approval"), true).
|
||||||
Order("status.id DESC").
|
Order("status.id DESC").
|
||||||
Limit(1).
|
Limit(1).
|
||||||
Scan(ctx, &lastStatusAt)
|
Scan(ctx, &lastStatusAt)
|
||||||
|
|
|
@ -114,7 +114,7 @@ func (suite *BasicTestSuite) TestGetAllStatuses() {
|
||||||
s := []*gtsmodel.Status{}
|
s := []*gtsmodel.Status{}
|
||||||
err := suite.db.GetAll(context.Background(), &s)
|
err := suite.db.GetAll(context.Background(), &s)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Len(s, 24)
|
suite.Len(s, 25)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BasicTestSuite) TestGetAllNotNull() {
|
func (suite *BasicTestSuite) TestGetAllNotNull() {
|
||||||
|
|
|
@ -56,6 +56,7 @@ type BunDBStandardTestSuite struct {
|
||||||
testThreads map[string]*gtsmodel.Thread
|
testThreads map[string]*gtsmodel.Thread
|
||||||
testPolls map[string]*gtsmodel.Poll
|
testPolls map[string]*gtsmodel.Poll
|
||||||
testPollVotes map[string]*gtsmodel.PollVote
|
testPollVotes map[string]*gtsmodel.PollVote
|
||||||
|
testInteractionRequests map[string]*gtsmodel.InteractionRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
|
@ -81,6 +82,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
suite.testThreads = testrig.NewTestThreads()
|
suite.testThreads = testrig.NewTestThreads()
|
||||||
suite.testPolls = testrig.NewTestPolls()
|
suite.testPolls = testrig.NewTestPolls()
|
||||||
suite.testPollVotes = testrig.NewTestPollVotes()
|
suite.testPollVotes = testrig.NewTestPollVotes()
|
||||||
|
suite.testInteractionRequests = testrig.NewTestInteractionRequests()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -76,6 +76,9 @@ func (i *instanceDB) CountInstanceStatuses(ctx context.Context, domain string) (
|
||||||
Where("? = ?", bun.Ident("account.domain"), domain)
|
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)
|
count, err := q.Count(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
|
|
|
@ -19,10 +19,14 @@ package bundb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
@ -32,56 +36,70 @@ type interactionDB struct {
|
||||||
state *state.State
|
state *state.State
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) newInteractionApprovalQ(approval interface{}) *bun.SelectQuery {
|
func (i *interactionDB) newInteractionRequestQ(request interface{}) *bun.SelectQuery {
|
||||||
return r.db.
|
return i.db.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
Model(approval)
|
Model(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error) {
|
func (i *interactionDB) GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error) {
|
||||||
return r.getInteractionApproval(
|
return i.getInteractionRequest(
|
||||||
ctx,
|
ctx,
|
||||||
"ID",
|
"ID",
|
||||||
func(approval *gtsmodel.InteractionApproval) error {
|
func(request *gtsmodel.InteractionRequest) error {
|
||||||
return r.
|
return i.
|
||||||
newInteractionApprovalQ(approval).
|
newInteractionRequestQ(request).
|
||||||
Where("? = ?", bun.Ident("interaction_approval.id"), id).
|
Where("? = ?", bun.Ident("interaction_request.id"), id).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
},
|
},
|
||||||
id,
|
id,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) GetInteractionApprovalByURI(ctx context.Context, uri string) (*gtsmodel.InteractionApproval, error) {
|
func (i *interactionDB) GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
|
||||||
return r.getInteractionApproval(
|
return i.getInteractionRequest(
|
||||||
ctx,
|
ctx,
|
||||||
"URI",
|
"InteractionURI",
|
||||||
func(approval *gtsmodel.InteractionApproval) error {
|
func(request *gtsmodel.InteractionRequest) error {
|
||||||
return r.
|
return i.
|
||||||
newInteractionApprovalQ(approval).
|
newInteractionRequestQ(request).
|
||||||
Where("? = ?", bun.Ident("interaction_approval.uri"), uri).
|
Where("? = ?", bun.Ident("interaction_request.interaction_uri"), uri).
|
||||||
Scan(ctx)
|
Scan(ctx)
|
||||||
},
|
},
|
||||||
uri,
|
uri,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) getInteractionApproval(
|
func (i *interactionDB) GetInteractionRequestByURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error) {
|
||||||
|
return i.getInteractionRequest(
|
||||||
|
ctx,
|
||||||
|
"URI",
|
||||||
|
func(request *gtsmodel.InteractionRequest) error {
|
||||||
|
return i.
|
||||||
|
newInteractionRequestQ(request).
|
||||||
|
Where("? = ?", bun.Ident("interaction_request.uri"), uri).
|
||||||
|
Scan(ctx)
|
||||||
|
},
|
||||||
|
uri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *interactionDB) getInteractionRequest(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
lookup string,
|
lookup string,
|
||||||
dbQuery func(*gtsmodel.InteractionApproval) error,
|
dbQuery func(*gtsmodel.InteractionRequest) error,
|
||||||
keyParts ...any,
|
keyParts ...any,
|
||||||
) (*gtsmodel.InteractionApproval, error) {
|
) (*gtsmodel.InteractionRequest, error) {
|
||||||
// Fetch approval from database cache with loader callback
|
// Fetch request from database cache with loader callback
|
||||||
approval, err := r.state.Caches.DB.InteractionApproval.LoadOne(lookup, func() (*gtsmodel.InteractionApproval, error) {
|
request, err := i.state.Caches.DB.InteractionRequest.LoadOne(lookup, func() (*gtsmodel.InteractionRequest, error) {
|
||||||
var approval gtsmodel.InteractionApproval
|
var request gtsmodel.InteractionRequest
|
||||||
|
|
||||||
// Not cached! Perform database query
|
// Not cached! Perform database query
|
||||||
if err := dbQuery(&approval); err != nil {
|
if err := dbQuery(&request); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &approval, nil
|
return &request, nil
|
||||||
}, keyParts...)
|
}, keyParts...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Error already processed.
|
// Error already processed.
|
||||||
|
@ -90,60 +108,241 @@ func (r *interactionDB) getInteractionApproval(
|
||||||
|
|
||||||
if gtscontext.Barebones(ctx) {
|
if gtscontext.Barebones(ctx) {
|
||||||
// Only a barebones model was requested.
|
// 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 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 (
|
var (
|
||||||
err error
|
err error
|
||||||
errs = gtserror.NewMultiError(2)
|
errs = gtserror.NewMultiError(4)
|
||||||
)
|
)
|
||||||
|
|
||||||
if approval.Account == nil {
|
if req.Status == nil {
|
||||||
// Account is not set, fetch from the database.
|
// Target status is not set, fetch from the database.
|
||||||
approval.Account, err = r.state.DB.GetAccountByID(
|
req.Status, err = i.state.DB.GetStatusByID(
|
||||||
gtscontext.SetBarebones(ctx),
|
gtscontext.SetBarebones(ctx),
|
||||||
approval.AccountID,
|
req.StatusID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Appendf("error populating interactionApproval account: %w", err)
|
errs.Appendf("error populating interactionRequest target: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if approval.InteractingAccount == nil {
|
if req.TargetAccount == nil {
|
||||||
|
// Target account is not set, fetch from the database.
|
||||||
|
req.TargetAccount, err = i.state.DB.GetAccountByID(
|
||||||
|
gtscontext.SetBarebones(ctx),
|
||||||
|
req.TargetAccountID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
errs.Appendf("error populating interactionRequest target account: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.InteractingAccount == nil {
|
||||||
// InteractingAccount is not set, fetch from the database.
|
// 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),
|
gtscontext.SetBarebones(ctx),
|
||||||
approval.InteractingAccountID,
|
req.InteractingAccountID,
|
||||||
)
|
)
|
||||||
if err != nil {
|
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()
|
return errs.Combine()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error {
|
func (i *interactionDB) PutInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error {
|
||||||
return r.state.Caches.DB.InteractionApproval.Store(approval, func() error {
|
return i.state.Caches.DB.InteractionRequest.Store(request, func() error {
|
||||||
_, err := r.db.NewInsert().Model(approval).Exec(ctx)
|
_, err := i.db.NewInsert().Model(request).Exec(ctx)
|
||||||
return err
|
return err
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *interactionDB) DeleteInteractionApprovalByID(ctx context.Context, id string) error {
|
func (i *interactionDB) UpdateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest, columns ...string) error {
|
||||||
defer r.state.Caches.DB.InteractionApproval.Invalidate("ID", id)
|
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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
_, err := r.db.NewDelete().
|
func (i *interactionDB) DeleteInteractionRequestByID(ctx context.Context, id string) error {
|
||||||
TableExpr("? AS ?", bun.Ident("interaction_approvals"), bun.Ident("interaction_approval")).
|
defer i.state.Caches.DB.InteractionRequest.Invalidate("ID", id)
|
||||||
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)
|
Exec(ctx)
|
||||||
return err
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
|
@ -20,41 +20,12 @@ package migrations
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
up := func(ctx context.Context, db *bun.DB) error {
|
up := func(ctx context.Context, db *bun.DB) error {
|
||||||
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
if _, err := tx.
|
|
||||||
NewCreateTable().
|
|
||||||
Model(>smodel.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
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(>smodel.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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -89,19 +89,6 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
|
||||||
q = q.Where("? = ?", bun.Ident("status.local"), local)
|
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
|
// As this is the home timeline, it should be
|
||||||
// populated by statuses from accounts followed
|
// populated by statuses from accounts followed
|
||||||
// by accountID, and posts from accountID itself.
|
// by accountID, and posts from accountID itself.
|
||||||
|
@ -137,6 +124,22 @@ func (t *timelineDB) GetHomeTimeline(ctx context.Context, accountID string, maxI
|
||||||
bun.In(targetAccountIDs),
|
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 {
|
if err := q.Scan(ctx, &statusIDs); err != nil {
|
||||||
return nil, err
|
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)
|
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 {
|
if limit > 0 {
|
||||||
// limit amount of statuses returned
|
// limit amount of statuses returned
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
|
@ -395,6 +401,9 @@ func (t *timelineDB) GetListTimeline(
|
||||||
frontToBack = false
|
frontToBack = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only include statuses that aren't pending approval.
|
||||||
|
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
// limit amount of statuses returned
|
// limit amount of statuses returned
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
|
@ -491,6 +500,9 @@ func (t *timelineDB) GetTagTimeline(
|
||||||
frontToBack = false
|
frontToBack = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only include statuses that aren't pending approval.
|
||||||
|
q = q.Where("NOT ? = ?", bun.Ident("status.pending_approval"), true)
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
// limit amount of statuses returned
|
// limit amount of statuses returned
|
||||||
q = q.Limit(limit)
|
q = q.Limit(limit)
|
||||||
|
|
|
@ -21,21 +21,47 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/paging"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Interaction interface {
|
type Interaction interface {
|
||||||
// GetInteractionApprovalByID gets one approval with the given id.
|
// GetInteractionRequestByID gets one request with the given id.
|
||||||
GetInteractionApprovalByID(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
|
GetInteractionRequestByID(ctx context.Context, id string) (*gtsmodel.InteractionRequest, error)
|
||||||
|
|
||||||
// GetInteractionApprovalByID gets one approval with the given uri.
|
// GetInteractionRequestByID gets one request with the given interaction uri.
|
||||||
GetInteractionApprovalByURI(ctx context.Context, id string) (*gtsmodel.InteractionApproval, error)
|
GetInteractionRequestByInteractionURI(ctx context.Context, uri string) (*gtsmodel.InteractionRequest, error)
|
||||||
|
|
||||||
// PopulateInteractionApproval ensures that the approval's struct fields are populated.
|
// GetInteractionRequestByURI returns one accepted or rejected
|
||||||
PopulateInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
|
// 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.
|
// PopulateInteractionRequest ensures that the request's struct fields are populated.
|
||||||
PutInteractionApproval(ctx context.Context, approval *gtsmodel.InteractionApproval) error
|
PopulateInteractionRequest(ctx context.Context, request *gtsmodel.InteractionRequest) error
|
||||||
|
|
||||||
// DeleteInteractionApprovalByID deletes one approval with the given ID.
|
// PutInteractionRequest puts a new request in the database.
|
||||||
DeleteInteractionApprovalByID(ctx context.Context, id string) error
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -38,11 +38,11 @@ func (f *federatingDB) GetAccept(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
acceptIRI *url.URL,
|
acceptIRI *url.URL,
|
||||||
) (vocab.ActivityStreamsAccept, error) {
|
) (vocab.ActivityStreamsAccept, error) {
|
||||||
approval, err := f.state.DB.GetInteractionApprovalByURI(ctx, acceptIRI.String())
|
approval, err := f.state.DB.GetInteractionRequestByURI(ctx, acceptIRI.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
|
||||||
|
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
|
||||||
)
|
|
|
@ -27,14 +27,14 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// AcceptGet handles the getting of a fedi/activitypub
|
// 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
|
// It performs appropriate authentication before
|
||||||
// returning a JSON serializable interface.
|
// returning a JSON serializable interface.
|
||||||
func (p *Processor) AcceptGet(
|
func (p *Processor) AcceptGet(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
requestedUser string,
|
requestedUser string,
|
||||||
approvalID string,
|
reqID string,
|
||||||
) (interface{}, gtserror.WithCode) {
|
) (interface{}, gtserror.WithCode) {
|
||||||
// Authenticate incoming request, getting related accounts.
|
// Authenticate incoming request, getting related accounts.
|
||||||
auth, errWithCode := p.authenticate(ctx, requestedUser)
|
auth, errWithCode := p.authenticate(ctx, requestedUser)
|
||||||
|
@ -52,25 +52,26 @@ func (p *Processor) AcceptGet(
|
||||||
|
|
||||||
receivingAcct := auth.receivingAcct
|
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) {
|
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)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if approval.AccountID != receivingAcct.ID {
|
if req == nil || !req.IsAccepted() {
|
||||||
const text = "approval does not belong to receiving account"
|
// Request doesn't exist or hasn't been accepted.
|
||||||
return nil, gtserror.NewErrorNotFound(errors.New(text))
|
err := gtserror.Newf("interaction request %s not found", reqID)
|
||||||
}
|
|
||||||
|
|
||||||
if approval == nil {
|
|
||||||
err := gtserror.Newf("approval %s not found", approvalID)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
accept, err := p.converter.InteractionApprovalToASAccept(ctx, approval)
|
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.InteractionReqToASAccept(ctx, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err := gtserror.Newf("error converting approval: %w", err)
|
err := gtserror.Newf("error converting accept: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -34,6 +34,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
|
||||||
filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
|
filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
|
||||||
filtersv2 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v2"
|
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/list"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/markers"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/markers"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||||
|
@ -81,6 +82,7 @@ type Processor struct {
|
||||||
fedi fedi.Processor
|
fedi fedi.Processor
|
||||||
filtersv1 filtersv1.Processor
|
filtersv1 filtersv1.Processor
|
||||||
filtersv2 filtersv2.Processor
|
filtersv2 filtersv2.Processor
|
||||||
|
interactionRequests interactionrequests.Processor
|
||||||
list list.Processor
|
list list.Processor
|
||||||
markers markers.Processor
|
markers markers.Processor
|
||||||
media media.Processor
|
media media.Processor
|
||||||
|
@ -123,6 +125,10 @@ func (p *Processor) FiltersV2() *filtersv2.Processor {
|
||||||
return &p.filtersv2
|
return &p.filtersv2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) InteractionRequests() *interactionrequests.Processor {
|
||||||
|
return &p.interactionRequests
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Processor) List() *list.Processor {
|
func (p *Processor) List() *list.Processor {
|
||||||
return &p.list
|
return &p.list
|
||||||
}
|
}
|
||||||
|
@ -209,6 +215,7 @@ func NewProcessor(
|
||||||
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
|
processor.fedi = fedi.New(state, &common, converter, federator, visFilter)
|
||||||
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
processor.filtersv1 = filtersv1.New(state, converter, &processor.stream)
|
||||||
processor.filtersv2 = filtersv2.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.list = list.New(state, converter)
|
||||||
processor.markers = markers.New(state, converter)
|
processor.markers = markers.New(state, converter)
|
||||||
processor.polls = polls.New(&common, state, converter)
|
processor.polls = polls.New(&common, state, converter)
|
||||||
|
@ -227,6 +234,7 @@ func NewProcessor(
|
||||||
// and pass subset of sub processors it needs.
|
// and pass subset of sub processors it needs.
|
||||||
processor.workers = workers.New(
|
processor.workers = workers.New(
|
||||||
state,
|
state,
|
||||||
|
&common,
|
||||||
federator,
|
federator,
|
||||||
converter,
|
converter,
|
||||||
visFilter,
|
visFilter,
|
||||||
|
|
|
@ -1127,17 +1127,17 @@ func (f *federate) MoveAccount(ctx context.Context, account *gtsmodel.Account) e
|
||||||
|
|
||||||
func (f *federate) AcceptInteraction(
|
func (f *federate) AcceptInteraction(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
approval *gtsmodel.InteractionApproval,
|
req *gtsmodel.InteractionRequest,
|
||||||
) error {
|
) error {
|
||||||
// Populate model.
|
// Populate model.
|
||||||
if err := f.state.DB.PopulateInteractionApproval(ctx, approval); err != nil {
|
if err := f.state.DB.PopulateInteractionRequest(ctx, req); err != nil {
|
||||||
return gtserror.Newf("error populating approval: %w", err)
|
return gtserror.Newf("error populating request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bail if interacting account is ours:
|
// Bail if interacting account is ours:
|
||||||
// we've already accepted internally and
|
// we've already accepted internally and
|
||||||
// shouldn't send an Accept to ourselves.
|
// shouldn't send an Accept to ourselves.
|
||||||
if approval.InteractingAccount.IsLocal() {
|
if req.InteractingAccount.IsLocal() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1145,27 +1145,27 @@ func (f *federate) AcceptInteraction(
|
||||||
// we can't Accept on another
|
// we can't Accept on another
|
||||||
// instance's behalf. (This
|
// instance's behalf. (This
|
||||||
// should never happen but...)
|
// should never happen but...)
|
||||||
if approval.Account.IsRemote() {
|
if req.TargetAccount.IsRemote() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse relevant URI(s).
|
// Parse relevant URI(s).
|
||||||
outboxIRI, err := parseURI(approval.Account.OutboxURI)
|
outboxIRI, err := parseURI(req.TargetAccount.OutboxURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
acceptingAcctIRI, err := parseURI(approval.Account.URI)
|
acceptingAcctIRI, err := parseURI(req.TargetAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
interactingAcctURI, err := parseURI(approval.InteractingAccount.URI)
|
interactingAcctURI, err := parseURI(req.InteractingAccount.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
interactionURI, err := parseURI(approval.InteractionURI)
|
interactionURI, err := parseURI(req.InteractionURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1190,7 +1190,79 @@ func (f *federate) AcceptInteraction(
|
||||||
); err != nil {
|
); err != nil {
|
||||||
return gtserror.Newf(
|
return gtserror.Newf(
|
||||||
"error sending activity %T for %v via outbox %s: %w",
|
"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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,18 +20,23 @@ package workers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"codeberg.org/gruf/go-logger/v2/level"
|
"codeberg.org/gruf/go-logger/v2/level"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"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/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -44,6 +49,7 @@ type clientAPI struct {
|
||||||
surface *Surface
|
surface *Surface
|
||||||
federate *federate
|
federate *federate
|
||||||
account *account.Processor
|
account *account.Processor
|
||||||
|
common *common.Processor
|
||||||
utils *utils
|
utils *utils
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +166,18 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.Fro
|
||||||
// REJECT USER (ie., new user+account sign-up)
|
// REJECT USER (ie., new user+account sign-up)
|
||||||
case ap.ObjectProfile:
|
case ap.ObjectProfile:
|
||||||
return p.clientAPI.RejectUser(ctx, cMsg)
|
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
|
// UNDO SOMETHING
|
||||||
|
@ -261,15 +279,13 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
|
||||||
// and/or notify the account that's being
|
// and/or notify the account that's being
|
||||||
// interacted with (if it's local): they can
|
// interacted with (if it's local): they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestReply(ctx, status); err != nil {
|
||||||
// Notify *local* account of pending reply.
|
return gtserror.Newf("error pending reply: %w", err)
|
||||||
if err := p.surface.notifyPendingReply(ctx, status); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending reply: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Create to *remote* account inbox ONLY.
|
// Send Create to *remote* account inbox ONLY.
|
||||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
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.
|
// Return early.
|
||||||
|
@ -285,14 +301,38 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientA
|
||||||
// sending out the Create with the approval
|
// sending out the Create with the approval
|
||||||
// URI attached.
|
// URI attached.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the status with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveReply(ctx, status)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving reply: %w", err)
|
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 {
|
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||||
return gtserror.Newf("error federating pre-approval of reply: %w", err)
|
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)
|
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 != "" {
|
if status.InReplyToID != "" {
|
||||||
// Interaction counts changed on the replied status;
|
// Interaction counts changed on the replied status;
|
||||||
// uncache the prepared version from all timelines.
|
// uncache the prepared version from all timelines.
|
||||||
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -344,9 +384,6 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClien
|
||||||
status := vote.Poll.Status
|
status := vote.Poll.Status
|
||||||
status.Poll = vote.Poll
|
status.Poll = vote.Poll
|
||||||
|
|
||||||
// Interaction counts changed on the source status, uncache from timelines.
|
|
||||||
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
|
|
||||||
|
|
||||||
if *status.Local {
|
if *status.Local {
|
||||||
// These are poll votes in a local status, we only need to
|
// These are poll votes in a local status, we only need to
|
||||||
// federate the updated status model with latest vote counts.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -429,10 +469,7 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
||||||
// If pending approval is true then fave must
|
// If pending approval is true then fave must
|
||||||
// target a status (either one of ours or a
|
// target a status (either one of ours or a
|
||||||
// remote) that requires approval for the fave.
|
// remote) that requires approval for the fave.
|
||||||
pendingApproval := util.PtrOrValue(
|
pendingApproval := util.PtrOrZero(fave.PendingApproval)
|
||||||
fave.PendingApproval,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case pendingApproval && !fave.PreApproved:
|
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
|
// and/or notify the account that's being
|
||||||
// interacted with (if it's local): they can
|
// interacted with (if it's local): they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestFave(ctx, fave); err != nil {
|
||||||
// Notify *local* account of pending reply.
|
return gtserror.Newf("error pending fave: %w", err)
|
||||||
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending fave: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Like to *remote* account inbox ONLY.
|
// Send Like to *remote* account inbox ONLY.
|
||||||
if err := p.federate.Like(ctx, fave); err != nil {
|
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.
|
// Return early.
|
||||||
|
@ -466,14 +501,38 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI
|
||||||
// sending out the Like with the approval
|
// sending out the Like with the approval
|
||||||
// URI attached.
|
// URI attached.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the fave with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveFave(ctx, fave)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving fave: %w", err)
|
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 {
|
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||||
return gtserror.Newf("error federating pre-approval of fave: %w", err)
|
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)
|
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 {
|
if err := p.federate.Like(ctx, fave); err != nil {
|
||||||
log.Errorf(ctx, "error federating like: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -505,10 +564,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
|
||||||
// If pending approval is true then status must
|
// If pending approval is true then status must
|
||||||
// boost a status (either one of ours or a
|
// boost a status (either one of ours or a
|
||||||
// remote) that requires approval for the boost.
|
// remote) that requires approval for the boost.
|
||||||
pendingApproval := util.PtrOrValue(
|
pendingApproval := util.PtrOrZero(boost.PendingApproval)
|
||||||
boost.PendingApproval,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case pendingApproval && !boost.PreApproved:
|
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
|
// and/or notify the account that's being
|
||||||
// interacted with (if it's local): they can
|
// interacted with (if it's local): they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestAnnounce(ctx, boost); err != nil {
|
||||||
// Notify *local* account of pending announce.
|
return gtserror.Newf("error pending boost: %w", err)
|
||||||
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending boost: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Announce to *remote* account inbox ONLY.
|
// Send Announce to *remote* account inbox ONLY.
|
||||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
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.
|
// Return early.
|
||||||
|
@ -542,14 +596,38 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClien
|
||||||
// sending out the Create with the approval
|
// sending out the Create with the approval
|
||||||
// URI attached.
|
// URI attached.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the boost with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveAnnounce(ctx, boost)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving boost: %w", err)
|
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 {
|
if err := p.federate.AcceptInteraction(ctx, approval); err != nil {
|
||||||
return gtserror.Newf("error federating pre-approval of boost: %w", err)
|
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)
|
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 {
|
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||||
log.Errorf(ctx, "error federating announce: %v", err)
|
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
|
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)
|
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 status.Poll != nil && status.Poll.Closing {
|
||||||
|
|
||||||
// If the latest status has a newly closed poll, at least compared
|
// 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)
|
log.Errorf(ctx, "error streaming status edit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status representation has changed, invalidate from timelines.
|
||||||
|
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
|
||||||
|
|
||||||
return nil
|
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)
|
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 {
|
if err := p.federate.UndoLike(ctx, statusFave); err != nil {
|
||||||
log.Errorf(ctx, "error federating like undo: %v", err)
|
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
|
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)
|
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 {
|
if err := p.federate.UndoAnnounce(ctx, status); err != nil {
|
||||||
log.Errorf(ctx, "error federating announce undo: %v", err)
|
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
|
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)
|
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 != "" {
|
if status.InReplyToID != "" {
|
||||||
// Interaction counts changed on the replied status;
|
// Interaction counts changed on the replied status;
|
||||||
// uncache the prepared version from all timelines.
|
// uncache the prepared version from all timelines.
|
||||||
p.surface.invalidateStatusFromTimelines(ctx, status.InReplyToID)
|
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
|
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 {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *clientAPI) AcceptReply(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *clientAPI) AcceptAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -231,8 +231,8 @@ func (suite *FromClientAPITestSuite) conversationJSON(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -344,8 +344,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -412,8 +412,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -473,8 +473,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -534,8 +534,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
// We're modifying the test list so take a copy.
|
// We're modifying the test list so take a copy.
|
||||||
testList := new(gtsmodel.List)
|
testList := new(gtsmodel.List)
|
||||||
|
@ -610,8 +610,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
// We're modifying the test list so take a copy.
|
// We're modifying the test list so take a copy.
|
||||||
testList := new(gtsmodel.List)
|
testList := new(gtsmodel.List)
|
||||||
|
@ -691,8 +691,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
// We're modifying the test list so take a copy.
|
// We're modifying the test list so take a copy.
|
||||||
testList := new(gtsmodel.List)
|
testList := new(gtsmodel.List)
|
||||||
|
@ -767,8 +767,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -831,8 +831,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -898,8 +898,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
|
||||||
|
|
||||||
// A DM to a local user should create a conversation and accompanying notification.
|
// A DM to a local user should create a conversation and accompanying notification.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversation() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichBeginsConversation() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
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.
|
// A public message to a local user should not result in a conversation notification.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreateConversation() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWhichShouldNotCreateConversation() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
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
|
// 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.
|
// should end up in the tag-following user's home timeline.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -1128,8 +1128,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtag(
|
||||||
// should not end up in the tag-following user's home timeline
|
// should not end up in the tag-following user's home timeline
|
||||||
// if the user has the author blocked.
|
// if the user has the author blocked.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagAndBlock() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagAndBlock() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -1209,8 +1209,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithFollowedHashtagA
|
||||||
// who does not otherwise follow the author or booster
|
// who does not otherwise follow the author or booster
|
||||||
// should end up in the tag-following user's home timeline as the original status.
|
// should end up in the tag-following user's home timeline as the original status.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -1312,8 +1312,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtag()
|
||||||
// should not end up in the tag-following user's home timeline
|
// should not end up in the tag-following user's home timeline
|
||||||
// if the user has the author blocked.
|
// if the user has the author blocked.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlock() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlock() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -1422,8 +1422,8 @@ func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAn
|
||||||
// should not end up in the tag-following user's home timeline
|
// should not end up in the tag-following user's home timeline
|
||||||
// if the user has the booster blocked.
|
// if the user has the booster blocked.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlockedBoost() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateBoostWithFollowedHashtagAndBlockedBoost() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
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
|
// 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.
|
// should stream a status update to the tag-following user's home timeline.
|
||||||
func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag() {
|
func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
@ -1601,8 +1601,8 @@ func (suite *FromClientAPITestSuite) TestProcessUpdateStatusWithFollowedHashtag(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
|
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
|
|
|
@ -20,18 +20,22 @@ package workers
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"time"
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"codeberg.org/gruf/go-logger/v2/level"
|
"codeberg.org/gruf/go-logger/v2/level"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"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/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"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/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -44,6 +48,7 @@ type fediAPI struct {
|
||||||
surface *Surface
|
surface *Surface
|
||||||
federate *federate
|
federate *federate
|
||||||
account *account.Processor
|
account *account.Processor
|
||||||
|
common *common.Processor
|
||||||
utils *utils
|
utils *utils
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -231,10 +236,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||||
// If pending approval is true then
|
// If pending approval is true then
|
||||||
// status must reply to a LOCAL status
|
// status must reply to a LOCAL status
|
||||||
// that requires approval for the reply.
|
// that requires approval for the reply.
|
||||||
pendingApproval := util.PtrOrValue(
|
pendingApproval := util.PtrOrZero(status.PendingApproval)
|
||||||
status.PendingApproval,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case pendingApproval && !status.PreApproved:
|
case pendingApproval && !status.PreApproved:
|
||||||
|
@ -242,10 +244,8 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||||
// preapproved, then just notify the account
|
// preapproved, then just notify the account
|
||||||
// that's being interacted with: they can
|
// that's being interacted with: they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestReply(ctx, status); err != nil {
|
||||||
// Notify *local* account of pending reply.
|
return gtserror.Newf("error pending reply: %w", err)
|
||||||
if err := p.surface.notifyPendingReply(ctx, status); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending reply: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return early.
|
// Return early.
|
||||||
|
@ -259,11 +259,33 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||||
// collection. Do the Accept immediately and
|
// collection. Do the Accept immediately and
|
||||||
// then process everything else as normal.
|
// then process everything else as normal.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the status with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveReply(ctx, status)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving reply: %w", err)
|
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.
|
// 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)
|
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 != "" {
|
if status.InReplyToID != "" {
|
||||||
// Interaction counts changed on the replied status; uncache the
|
// Interaction counts changed on the replied status; uncache the
|
||||||
// prepared version from all timelines. The status dereferencer
|
// 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)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -320,9 +342,6 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
status := vote.Poll.Status
|
status := vote.Poll.Status
|
||||||
status.Poll = vote.Poll
|
status.Poll = vote.Poll
|
||||||
|
|
||||||
// Interaction counts changed on the source status, uncache from timelines.
|
|
||||||
p.surface.invalidateStatusFromTimelines(ctx, vote.Poll.StatusID)
|
|
||||||
|
|
||||||
if *status.Local {
|
if *status.Local {
|
||||||
// Before federating it, increment the
|
// Before federating it, increment the
|
||||||
// poll vote counts on our local copy.
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -409,10 +431,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
||||||
// If pending approval is true then
|
// If pending approval is true then
|
||||||
// fave must target a LOCAL status
|
// fave must target a LOCAL status
|
||||||
// that requires approval for the fave.
|
// that requires approval for the fave.
|
||||||
pendingApproval := util.PtrOrValue(
|
pendingApproval := util.PtrOrZero(fave.PendingApproval)
|
||||||
fave.PendingApproval,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case pendingApproval && !fave.PreApproved:
|
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
|
// preapproved, then just notify the account
|
||||||
// that's being interacted with: they can
|
// that's being interacted with: they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestFave(ctx, fave); err != nil {
|
||||||
// Notify *local* account of pending fave.
|
return gtserror.Newf("error pending fave: %w", err)
|
||||||
if err := p.surface.notifyPendingFave(ctx, fave); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending fave: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return early.
|
// Return early.
|
||||||
|
@ -437,11 +454,33 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) er
|
||||||
// collection. Do the Accept immediately and
|
// collection. Do the Accept immediately and
|
||||||
// then process everything else as normal.
|
// then process everything else as normal.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the fave with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveFave(ctx, fave)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving fave: %w", err)
|
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.
|
// 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
|
// If pending approval is true then
|
||||||
// boost must target a LOCAL status
|
// boost must target a LOCAL status
|
||||||
// that requires approval for the boost.
|
// that requires approval for the boost.
|
||||||
pendingApproval := util.PtrOrValue(
|
pendingApproval := util.PtrOrZero(boost.PendingApproval)
|
||||||
boost.PendingApproval,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case pendingApproval && !boost.PreApproved:
|
case pendingApproval && !boost.PreApproved:
|
||||||
|
@ -507,10 +543,8 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
// preapproved, then just notify the account
|
// preapproved, then just notify the account
|
||||||
// that's being interacted with: they can
|
// that's being interacted with: they can
|
||||||
// approve or deny the interaction later.
|
// approve or deny the interaction later.
|
||||||
|
if err := p.utils.requestAnnounce(ctx, boost); err != nil {
|
||||||
// Notify *local* account of pending announce.
|
return gtserror.Newf("error pending boost: %w", err)
|
||||||
if err := p.surface.notifyPendingAnnounce(ctx, boost); err != nil {
|
|
||||||
log.Errorf(ctx, "error notifying pending boost: %v", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return early.
|
// Return early.
|
||||||
|
@ -524,11 +558,33 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI
|
||||||
// collection. Do the Accept immediately and
|
// collection. Do the Accept immediately and
|
||||||
// then process everything else as normal.
|
// then process everything else as normal.
|
||||||
|
|
||||||
// Put approval in the database and
|
// Store an already-accepted interaction request.
|
||||||
// update the boost with approvedBy URI.
|
id := id.NewULID()
|
||||||
approval, err := p.utils.approveAnnounce(ctx, boost)
|
approval := >smodel.InteractionRequest{
|
||||||
if err != nil {
|
ID: id,
|
||||||
return gtserror.Newf("error pre-approving boost: %w", err)
|
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.
|
// 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)
|
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.
|
// Send out the reply again, fully this time.
|
||||||
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
if err := p.federate.CreateStatus(ctx, status); err != nil {
|
||||||
log.Errorf(ctx, "error federating announce: %v", err)
|
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
|
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)
|
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.
|
// Send out the boost again, fully this time.
|
||||||
if err := p.federate.Announce(ctx, boost); err != nil {
|
if err := p.federate.Announce(ctx, boost); err != nil {
|
||||||
log.Errorf(ctx, "error federating announce: %v", err)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -792,9 +848,6 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI)
|
||||||
log.Errorf(ctx, "error refreshing status: %v", err)
|
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 status.Poll != nil && status.Poll.Closing {
|
||||||
|
|
||||||
// If the latest status has a newly closed poll, at least compared
|
// 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)
|
log.Errorf(ctx, "error streaming status edit: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Status representation was refetched, uncache from timelines.
|
||||||
|
p.surface.invalidateStatusFromTimelines(ctx, status.ID)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -42,8 +42,8 @@ type FromFediAPITestSuite struct {
|
||||||
|
|
||||||
// remote_account_1 boosts the first status of local_account_1
|
// remote_account_1 boosts the first status of local_account_1
|
||||||
func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
boostedStatus := >smodel.Status{}
|
boostedStatus := >smodel.Status{}
|
||||||
*boostedStatus = *suite.testStatuses["local_account_1_status_1"]
|
*boostedStatus = *suite.testStatuses["local_account_1_status_1"]
|
||||||
|
@ -106,8 +106,8 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
repliedAccount := >smodel.Account{}
|
repliedAccount := >smodel.Account{}
|
||||||
*repliedAccount = *suite.testAccounts["local_account_1"]
|
*repliedAccount = *suite.testAccounts["local_account_1"]
|
||||||
|
@ -190,8 +190,8 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
func (suite *FromFediAPITestSuite) TestProcessFave() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
favedAccount := suite.testAccounts["local_account_1"]
|
favedAccount := suite.testAccounts["local_account_1"]
|
||||||
favedStatus := suite.testStatuses["local_account_1_status_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
|
// 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.
|
// the fave, but just follow the actor who received the fave.
|
||||||
func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
|
func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
receivingAccount := suite.testAccounts["local_account_2"]
|
receivingAccount := suite.testAccounts["local_account_2"]
|
||||||
favedAccount := suite.testAccounts["local_account_1"]
|
favedAccount := suite.testAccounts["local_account_1"]
|
||||||
|
@ -327,8 +327,8 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -421,8 +421,8 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
|
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -478,8 +478,8 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
|
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -579,8 +579,8 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
|
||||||
|
|
||||||
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
|
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
|
||||||
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
|
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
@ -604,8 +604,8 @@ func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FromFediAPITestSuite) TestMoveAccount() {
|
func (suite *FromFediAPITestSuite) TestMoveAccount() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
// We're gonna migrate foss_satan to our local admin account.
|
// We're gonna migrate foss_satan to our local admin account.
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SurfaceNotifyTestSuite struct {
|
type SurfaceNotifyTestSuite struct {
|
||||||
|
@ -35,8 +36,8 @@ type SurfaceNotifyTestSuite struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
|
func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
|
||||||
testStructs := suite.SetupTestStructs()
|
testStructs := testrig.SetupTestStructs(rMediaPath, rTemplatePath)
|
||||||
defer suite.TearDownTestStructs(testStructs)
|
defer testrig.TearDownTestStructs(testStructs)
|
||||||
|
|
||||||
surface := &workers.Surface{
|
surface := &workers.Surface{
|
||||||
State: testStructs.State,
|
State: testStructs.State,
|
||||||
|
|
|
@ -26,12 +26,11 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -488,128 +487,143 @@ func (u *utils) decrementFollowRequestsCount(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// approveFave stores + returns an
|
// requestFave stores an interaction request
|
||||||
// interactionApproval for a fave.
|
// for the given fave, and notifies the interactee.
|
||||||
func (u *utils) approveFave(
|
func (u *utils) requestFave(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
fave *gtsmodel.StatusFave,
|
fave *gtsmodel.StatusFave,
|
||||||
) (*gtsmodel.InteractionApproval, error) {
|
) error {
|
||||||
id := id.NewULID()
|
// Only create interaction request
|
||||||
|
// if fave targets a local status.
|
||||||
approval := >smodel.InteractionApproval{
|
if fave.Status == nil ||
|
||||||
ID: id,
|
!fave.Status.IsLocal() {
|
||||||
AccountID: fave.TargetAccountID,
|
return nil
|
||||||
Account: fave.TargetAccount,
|
|
||||||
InteractingAccountID: fave.AccountID,
|
|
||||||
InteractingAccount: fave.Account,
|
|
||||||
InteractionURI: fave.URI,
|
|
||||||
InteractionType: gtsmodel.InteractionLike,
|
|
||||||
URI: uris.GenerateURIForAccept(fave.TargetAccount.Username, id),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
// Lock on the interaction URI.
|
||||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
unlock := u.state.ProcessingLocks.Lock(fave.URI)
|
||||||
return nil, err
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the fave itself as now approved.
|
if req != nil {
|
||||||
fave.PendingApproval = util.Ptr(false)
|
// Interaction req already exists,
|
||||||
fave.PreApproved = false
|
// no need to do anything else.
|
||||||
fave.ApprovedByURI = approval.URI
|
return nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return approval, nil
|
// Create + store new interaction request.
|
||||||
|
req, err = typeutils.StatusFaveToInteractionRequest(ctx, fave)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.Newf("error creating interaction request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// approveReply stores + returns an
|
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||||
// interactionApproval for a reply.
|
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||||
func (u *utils) approveReply(
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestReply stores an interaction request
|
||||||
|
// for the given reply, and notifies the interactee.
|
||||||
|
func (u *utils) requestReply(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
status *gtsmodel.Status,
|
reply *gtsmodel.Status,
|
||||||
) (*gtsmodel.InteractionApproval, error) {
|
) error {
|
||||||
id := id.NewULID()
|
// Only create interaction request if
|
||||||
|
// status replies to a local status.
|
||||||
approval := >smodel.InteractionApproval{
|
if reply.InReplyTo == nil ||
|
||||||
ID: id,
|
!reply.InReplyTo.IsLocal() {
|
||||||
AccountID: status.InReplyToAccountID,
|
return nil
|
||||||
Account: status.InReplyToAccount,
|
|
||||||
InteractingAccountID: status.AccountID,
|
|
||||||
InteractingAccount: status.Account,
|
|
||||||
InteractionURI: status.URI,
|
|
||||||
InteractionType: gtsmodel.InteractionReply,
|
|
||||||
URI: uris.GenerateURIForAccept(status.InReplyToAccount.Username, id),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
// Lock on the interaction URI.
|
||||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
unlock := u.state.ProcessingLocks.Lock(reply.URI)
|
||||||
return nil, err
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the status itself as now approved.
|
if req != nil {
|
||||||
status.PendingApproval = util.Ptr(false)
|
// Interaction req already exists,
|
||||||
status.PreApproved = false
|
// no need to do anything else.
|
||||||
status.ApprovedByURI = approval.URI
|
return nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return approval, nil
|
// Create + store interaction request.
|
||||||
|
req, err = typeutils.StatusToInteractionRequest(ctx, reply)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.Newf("error creating interaction request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// approveAnnounce stores + returns an
|
if err := u.state.DB.PutInteractionRequest(ctx, req); err != nil {
|
||||||
// interactionApproval for an announce.
|
return gtserror.Newf("db error storing interaction request: %w", err)
|
||||||
func (u *utils) approveAnnounce(
|
}
|
||||||
|
|
||||||
|
// 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 nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// requestAnnounce stores an interaction request
|
||||||
|
// for the given announce, and notifies the interactee.
|
||||||
|
func (u *utils) requestAnnounce(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
boost *gtsmodel.Status,
|
boost *gtsmodel.Status,
|
||||||
) (*gtsmodel.InteractionApproval, error) {
|
) error {
|
||||||
id := id.NewULID()
|
// Only create interaction request if
|
||||||
|
// status announces a local status.
|
||||||
approval := >smodel.InteractionApproval{
|
if boost.BoostOf == nil ||
|
||||||
ID: id,
|
!boost.BoostOf.IsLocal() {
|
||||||
AccountID: boost.BoostOfAccountID,
|
return nil
|
||||||
Account: boost.BoostOfAccount,
|
|
||||||
InteractingAccountID: boost.AccountID,
|
|
||||||
InteractingAccount: boost.Account,
|
|
||||||
InteractionURI: boost.URI,
|
|
||||||
InteractionType: gtsmodel.InteractionReply,
|
|
||||||
URI: uris.GenerateURIForAccept(boost.BoostOfAccount.Username, id),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.state.DB.PutInteractionApproval(ctx, approval); err != nil {
|
// Lock on the interaction URI.
|
||||||
err := gtserror.Newf("db error inserting interaction approval: %w", err)
|
unlock := u.state.ProcessingLocks.Lock(boost.URI)
|
||||||
return nil, err
|
defer unlock()
|
||||||
|
|
||||||
|
// 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark the status itself as now approved.
|
if req != nil {
|
||||||
boost.PendingApproval = util.Ptr(false)
|
// Interaction req already exists,
|
||||||
boost.PreApproved = false
|
// no need to do anything else.
|
||||||
boost.ApprovedByURI = approval.URI
|
return nil
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return approval, nil
|
// 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.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 nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/account"
|
"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/conversations"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/stream"
|
||||||
|
@ -38,6 +39,7 @@ type Processor struct {
|
||||||
|
|
||||||
func New(
|
func New(
|
||||||
state *state.State,
|
state *state.State,
|
||||||
|
common *common.Processor,
|
||||||
federator *federation.Federator,
|
federator *federation.Federator,
|
||||||
converter *typeutils.Converter,
|
converter *typeutils.Converter,
|
||||||
visFilter *visibility.Filter,
|
visFilter *visibility.Filter,
|
||||||
|
@ -82,6 +84,7 @@ func New(
|
||||||
surface: surface,
|
surface: surface,
|
||||||
federate: federate,
|
federate: federate,
|
||||||
account: account,
|
account: account,
|
||||||
|
common: common,
|
||||||
utils: utils,
|
utils: utils,
|
||||||
},
|
},
|
||||||
fediAPI: fediAPI{
|
fediAPI: fediAPI{
|
||||||
|
@ -89,6 +92,7 @@ func New(
|
||||||
surface: surface,
|
surface: surface,
|
||||||
federate: federate,
|
federate: federate,
|
||||||
account: account,
|
account: account,
|
||||||
|
common: common,
|
||||||
utils: utils,
|
utils: utils,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,19 +21,18 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"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/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
rMediaPath = "../../../testrig/media"
|
||||||
|
rTemplatePath = "../../../web/template"
|
||||||
|
)
|
||||||
|
|
||||||
type WorkersTestSuite struct {
|
type WorkersTestSuite struct {
|
||||||
// standard suite interfaces
|
// standard suite interfaces
|
||||||
suite.Suite
|
suite.Suite
|
||||||
|
@ -56,23 +55,6 @@ type WorkersTestSuite struct {
|
||||||
testListEntries map[string]*gtsmodel.ListEntry
|
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() {
|
func (suite *WorkersTestSuite) SetupSuite() {
|
||||||
suite.testTokens = testrig.NewTestTokens()
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
suite.testClients = testrig.NewTestClients()
|
suite.testClients = testrig.NewTestClients()
|
||||||
|
@ -132,63 +114,3 @@ func (suite *WorkersTestSuite) openStreams(ctx context.Context, processor *proce
|
||||||
|
|
||||||
return streams
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ func (suite *PruneTestSuite) TestPrune() {
|
||||||
|
|
||||||
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(19, pruned)
|
suite.Equal(20, pruned)
|
||||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
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)
|
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(19, pruned)
|
suite.Equal(20, pruned)
|
||||||
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
suite.Equal(5, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
||||||
|
|
||||||
// Prune same again, nothing should be pruned this time.
|
// 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)
|
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(24, pruned)
|
suite.Equal(25, pruned)
|
||||||
suite.Equal(0, suite.state.Timelines.Home.GetIndexedLength(ctx, testAccountID))
|
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)
|
pruned, err := suite.state.Timelines.Home.Prune(ctx, testAccountID, desiredPreparedItemsLength, desiredIndexedItemsLength)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(0, pruned)
|
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) {
|
func TestPruneTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -20,6 +20,7 @@ package typeutils
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
|
@ -97,3 +98,80 @@ func (c *Converter) StatusToBoost(
|
||||||
|
|
||||||
return boost, nil
|
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 >smodel.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 >smodel.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
|
||||||
|
}
|
||||||
|
|
|
@ -1960,36 +1960,36 @@ func (c *Converter) InteractionPolicyToASInteractionPolicy(
|
||||||
return policy, nil
|
return policy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InteractionApprovalToASAccept converts a *gtsmodel.InteractionApproval
|
// InteractionReqToASAccept converts a *gtsmodel.InteractionRequest
|
||||||
// to an ActivityStreams Accept, addressed to the interacting account.
|
// to an ActivityStreams Accept, addressed to the interacting account.
|
||||||
func (c *Converter) InteractionApprovalToASAccept(
|
func (c *Converter) InteractionReqToASAccept(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
approval *gtsmodel.InteractionApproval,
|
req *gtsmodel.InteractionRequest,
|
||||||
) (vocab.ActivityStreamsAccept, error) {
|
) (vocab.ActivityStreamsAccept, error) {
|
||||||
accept := streams.NewActivityStreamsAccept()
|
accept := streams.NewActivityStreamsAccept()
|
||||||
|
|
||||||
acceptID, err := url.Parse(approval.URI)
|
acceptID, err := url.Parse(req.URI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("invalid accept uri: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("invalid account uri: %w", err)
|
return nil, gtserror.Newf("invalid account uri: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
objectIRI, err := url.Parse(approval.InteractionURI)
|
objectIRI, err := url.Parse(req.InteractionURI)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("invalid target uri: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
return nil, gtserror.Newf("invalid interacting account uri: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set id to the URI of
|
// Set id to the URI of
|
||||||
// interactionApproval.
|
// interaction request.
|
||||||
ap.SetJSONLDId(accept, acceptID)
|
ap.SetJSONLDId(accept, acceptID)
|
||||||
|
|
||||||
// Actor is the account that
|
// Actor is the account that
|
||||||
|
|
|
@ -1057,26 +1057,26 @@ func (suite *InternalToASTestSuite) TestPollVoteToASCreate() {
|
||||||
}`, string(bytes))
|
}`, string(bytes))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InternalToASTestSuite) TestInteractionApprovalToASAccept() {
|
func (suite *InternalToASTestSuite) TestInteractionReqToASAccept() {
|
||||||
acceptingAccount := suite.testAccounts["local_account_1"]
|
acceptingAccount := suite.testAccounts["local_account_1"]
|
||||||
interactingAccount := suite.testAccounts["remote_account_1"]
|
interactingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
|
||||||
interactionApproval := >smodel.InteractionApproval{
|
req := >smodel.InteractionRequest{
|
||||||
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
ID: "01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
||||||
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
|
CreatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
|
||||||
UpdatedAt: testrig.TimeMustParse("2022-06-09T13:12:00Z"),
|
TargetAccountID: acceptingAccount.ID,
|
||||||
AccountID: acceptingAccount.ID,
|
TargetAccount: acceptingAccount,
|
||||||
Account: acceptingAccount,
|
|
||||||
InteractingAccountID: interactingAccount.ID,
|
InteractingAccountID: interactingAccount.ID,
|
||||||
InteractingAccount: interactingAccount,
|
InteractingAccount: interactingAccount,
|
||||||
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
InteractionURI: "https://fossbros-anonymous.io/users/foss_satan/statuses/01J1AKRRHQ6MDDQHV0TP716T2K",
|
||||||
InteractionType: gtsmodel.InteractionAnnounce,
|
InteractionType: gtsmodel.InteractionAnnounce,
|
||||||
URI: "http://localhost:8080/users/the_mighty_zork/accepts/01J1AKMZ8JE5NW0ZSFTRC1JJNE",
|
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(),
|
context.Background(),
|
||||||
interactionApproval,
|
req,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
|
|
|
@ -2592,3 +2592,74 @@ func policyValsToAPIPolicyVals(vals gtsmodel.PolicyValues) []apimodel.PolicyValu
|
||||||
|
|
||||||
return apiVals
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -46,7 +46,8 @@ const (
|
||||||
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
FileserverPath = "fileserver" // FileserverPath is a path component for serving attachments + media
|
||||||
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
EmojiPath = "emoji" // EmojiPath represents the activitypub emoji location
|
||||||
TagsPath = "tags" // TagsPath represents the activitypub tags 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.
|
// 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)
|
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
|
// https://example.org/users/whatever_user/accepts/01F7XTH1QGBAPMGF49WJZ91XGC
|
||||||
func GenerateURIForAccept(username string, thisAcceptID string) string {
|
func GenerateURIForAccept(username string, thisAcceptID string) string {
|
||||||
protocol := config.GetProtocol()
|
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)
|
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.
|
// GenerateURIsForAccount throws together a bunch of URIs for the given username, with the given protocol and host.
|
||||||
func GenerateURIsForAccount(username string) *UserURIs {
|
func GenerateURIsForAccount(username string) *UserURIs {
|
||||||
protocol := config.GetProtocol()
|
protocol := config.GetProtocol()
|
||||||
|
|
|
@ -46,7 +46,7 @@ EXPECT=$(cat << "EOF"
|
||||||
"follow-request-mem-ratio": 2,
|
"follow-request-mem-ratio": 2,
|
||||||
"in-reply-to-ids-mem-ratio": 3,
|
"in-reply-to-ids-mem-ratio": 3,
|
||||||
"instance-mem-ratio": 1,
|
"instance-mem-ratio": 1,
|
||||||
"interaction-approval-mem-ratio": 1,
|
"interaction-request-mem-ratio": 1,
|
||||||
"list-entry-mem-ratio": 2,
|
"list-entry-mem-ratio": 2,
|
||||||
"list-mem-ratio": 1,
|
"list-mem-ratio": 1,
|
||||||
"marker-mem-ratio": 0.5,
|
"marker-mem-ratio": 0.5,
|
||||||
|
|
|
@ -41,6 +41,7 @@ var testModels = []interface{}{
|
||||||
>smodel.FilterStatus{},
|
>smodel.FilterStatus{},
|
||||||
>smodel.Follow{},
|
>smodel.Follow{},
|
||||||
>smodel.FollowRequest{},
|
>smodel.FollowRequest{},
|
||||||
|
>smodel.InteractionRequest{},
|
||||||
>smodel.List{},
|
>smodel.List{},
|
||||||
>smodel.ListEntry{},
|
>smodel.ListEntry{},
|
||||||
>smodel.Marker{},
|
>smodel.Marker{},
|
||||||
|
@ -346,6 +347,12 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range NewTestInteractionRequests() {
|
||||||
|
if err := db.Put(ctx, v); err != nil {
|
||||||
|
log.Panic(nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := db.CreateInstanceAccount(ctx); err != nil {
|
if err := db.CreateInstanceAccount(ctx); err != nil {
|
||||||
log.Panic(nil, err)
|
log.Panic(nil, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1509,6 +1509,31 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
ActivityStreamsType: ap.ObjectNote,
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
PendingApproval: util.Ptr(false),
|
PendingApproval: util.Ptr(false),
|
||||||
},
|
},
|
||||||
|
"admin_account_status_5": {
|
||||||
|
ID: "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
URI: "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
URL: "http://localhost:8080/@admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
Content: `<p>Hi <span class="h-card"><a href="http://localhost:8080/@1happyturtle" class="u-url mention" rel="nofollow noreferrer noopener" target="_blank">@<span>1happyturtle</span></a></span>, can I reply?</p>`,
|
||||||
|
Text: "Hi @1happyturtle, can I reply?",
|
||||||
|
CreatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||||
|
UpdatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||||
|
Local: util.Ptr(true),
|
||||||
|
AccountURI: "http://localhost:8080/users/admin",
|
||||||
|
MentionIDs: []string{"01J5QVP69ANF1K4WHES6GA4WXP"},
|
||||||
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
InReplyToID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
InReplyToAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
InReplyToURI: "http://localhost:8080/users/1happyturtle/statuses/01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
BoostOfID: "",
|
||||||
|
BoostOfAccountID: "",
|
||||||
|
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||||
|
Visibility: gtsmodel.VisibilityUnlocked,
|
||||||
|
Sensitive: util.Ptr(false),
|
||||||
|
CreatedWithApplicationID: "01F8MGXQRHYF5QPMTMXP78QC2F",
|
||||||
|
Federated: util.Ptr(true),
|
||||||
|
ActivityStreamsType: ap.ObjectNote,
|
||||||
|
PendingApproval: util.Ptr(true),
|
||||||
|
},
|
||||||
"local_account_1_status_1": {
|
"local_account_1_status_1": {
|
||||||
ID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
ID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
URI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
@ -2298,6 +2323,10 @@ func NewTestThreadToStatus() []*gtsmodel.ThreadToStatus {
|
||||||
ThreadID: "01HCWE7ZNC2SS4P05WA5QYED23",
|
ThreadID: "01HCWE7ZNC2SS4P05WA5QYED23",
|
||||||
StatusID: "01G20ZM733MGN8J344T4ZDDFY1",
|
StatusID: "01G20ZM733MGN8J344T4ZDDFY1",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||||
|
StatusID: "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2352,6 +2381,18 @@ func NewTestMentions() map[string]*gtsmodel.Mention {
|
||||||
TargetAccountURI: "http://localhost:8080/users/the_mighty_zork",
|
TargetAccountURI: "http://localhost:8080/users/the_mighty_zork",
|
||||||
TargetAccountURL: "http://localhost:8080/@the_mighty_zork",
|
TargetAccountURL: "http://localhost:8080/@the_mighty_zork",
|
||||||
},
|
},
|
||||||
|
"admin_account_mention_turtle": {
|
||||||
|
ID: "01J5QVP69ANF1K4WHES6GA4WXP",
|
||||||
|
StatusID: "01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
CreatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||||
|
UpdatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||||
|
OriginAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
OriginAccountURI: "http://localhost:8080/users/admin",
|
||||||
|
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
NameString: "@1happyturtle",
|
||||||
|
TargetAccountURI: "http://localhost:8080/users/1happyturtle",
|
||||||
|
TargetAccountURL: "http://localhost:8080/@1happyturtle",
|
||||||
|
},
|
||||||
"remote_account_2_mention_admin": {
|
"remote_account_2_mention_admin": {
|
||||||
ID: "01HE7XQNMKTVC8MNPCE1JGK4J3",
|
ID: "01HE7XQNMKTVC8MNPCE1JGK4J3",
|
||||||
StatusID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
StatusID: "01HE7XJ1CG84TBKH5V9XKBVGF5",
|
||||||
|
@ -3430,6 +3471,20 @@ func NewTestUserMutes() map[string]*gtsmodel.UserMute {
|
||||||
return map[string]*gtsmodel.UserMute{}
|
return map[string]*gtsmodel.UserMute{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewTestInteractionRequests() map[string]*gtsmodel.InteractionRequest {
|
||||||
|
return map[string]*gtsmodel.InteractionRequest{
|
||||||
|
"admin_account_reply_turtle": {
|
||||||
|
ID: "01J5QVXCCEATJYSXM9H6MZT4JR",
|
||||||
|
CreatedAt: TimeMustParse("2024-02-20T12:41:37+02:00"),
|
||||||
|
StatusID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
TargetAccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
|
InteractingAccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
InteractionURI: "http://localhost:8080/users/admin/statuses/01J5QVB9VC76NPPRQ207GG4DRZ",
|
||||||
|
InteractionType: gtsmodel.InteractionReply,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values.
|
// GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values.
|
||||||
func GetSignatureForActivity(activity pub.Activity, pubKeyID string, privkey *rsa.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
|
func GetSignatureForActivity(activity pub.Activity, pubKeyID string, privkey *rsa.PrivateKey, destination *url.URL) (signatureHeader string, digestHeader string, dateHeader string) {
|
||||||
// convert the activity into json bytes
|
// convert the activity into json bytes
|
||||||
|
|
|
@ -0,0 +1,121 @@
|
||||||
|
// 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 testrig
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestStructs encapsulates structs needed to
|
||||||
|
// run one test independently. 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 a
|
||||||
|
// 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
|
||||||
|
Common *common.Processor
|
||||||
|
Processor *processing.Processor
|
||||||
|
HTTPClient *MockHTTPClient
|
||||||
|
TypeConverter *typeutils.Converter
|
||||||
|
EmailSender email.Sender
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupTestStructs(
|
||||||
|
rMediaPath string,
|
||||||
|
rTemplatePath string,
|
||||||
|
) *TestStructs {
|
||||||
|
state := state.State{}
|
||||||
|
|
||||||
|
state.Caches.Init()
|
||||||
|
|
||||||
|
db := NewTestDB(&state)
|
||||||
|
state.DB = db
|
||||||
|
|
||||||
|
storage := NewInMemoryStorage()
|
||||||
|
state.Storage = storage
|
||||||
|
typeconverter := typeutils.NewConverter(&state)
|
||||||
|
visFilter := visibility.NewFilter(&state)
|
||||||
|
intFilter := interaction.NewFilter(&state)
|
||||||
|
|
||||||
|
StartTimelines(
|
||||||
|
&state,
|
||||||
|
visFilter,
|
||||||
|
typeconverter,
|
||||||
|
)
|
||||||
|
|
||||||
|
httpClient := NewMockHTTPClient(nil, rMediaPath)
|
||||||
|
httpClient.TestRemotePeople = NewTestFediPeople()
|
||||||
|
httpClient.TestRemoteStatuses = NewTestFediStatuses()
|
||||||
|
|
||||||
|
transportController := NewTestTransportController(&state, httpClient)
|
||||||
|
mediaManager := NewTestMediaManager(&state)
|
||||||
|
federator := NewTestFederator(&state, transportController, mediaManager)
|
||||||
|
oauthServer := NewTestOauthServer(db)
|
||||||
|
emailSender := NewEmailSender(rTemplatePath, nil)
|
||||||
|
|
||||||
|
common := common.New(
|
||||||
|
&state,
|
||||||
|
mediaManager,
|
||||||
|
typeconverter,
|
||||||
|
federator,
|
||||||
|
visFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
processor := processing.NewProcessor(
|
||||||
|
cleaner.New(&state),
|
||||||
|
typeconverter,
|
||||||
|
federator,
|
||||||
|
oauthServer,
|
||||||
|
mediaManager,
|
||||||
|
&state,
|
||||||
|
emailSender,
|
||||||
|
visFilter,
|
||||||
|
intFilter,
|
||||||
|
)
|
||||||
|
|
||||||
|
StartWorkers(&state, processor.Workers())
|
||||||
|
|
||||||
|
StandardDBSetup(db, nil)
|
||||||
|
StandardStorageSetup(storage, rMediaPath)
|
||||||
|
|
||||||
|
return &TestStructs{
|
||||||
|
State: &state,
|
||||||
|
Common: &common,
|
||||||
|
Processor: processor,
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
TypeConverter: typeconverter,
|
||||||
|
EmailSender: emailSender,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TearDownTestStructs(testStructs *TestStructs) {
|
||||||
|
StandardDBTeardown(testStructs.State.DB)
|
||||||
|
StandardStorageTeardown(testStructs.State.Storage)
|
||||||
|
StopWorkers(testStructs.State)
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
"@reduxjs/toolkit": "^1.8.6",
|
"@reduxjs/toolkit": "^1.8.6",
|
||||||
"ariakit": "^2.0.0-next.41",
|
"ariakit": "^2.0.0-next.41",
|
||||||
"get-by-dot": "^1.0.2",
|
"get-by-dot": "^1.0.2",
|
||||||
|
"html-to-text": "^9.0.5",
|
||||||
"is-valid-domain": "^0.1.6",
|
"is-valid-domain": "^0.1.6",
|
||||||
"js-file-download": "^0.4.12",
|
"js-file-download": "^0.4.12",
|
||||||
"langs": "^2.0.0",
|
"langs": "^2.0.0",
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
"@browserify/envify": "^6.0.0",
|
"@browserify/envify": "^6.0.0",
|
||||||
"@browserify/uglifyify": "^6.0.0",
|
"@browserify/uglifyify": "^6.0.0",
|
||||||
"@joepie91/eslint-config": "^1.1.1",
|
"@joepie91/eslint-config": "^1.1.1",
|
||||||
|
"@types/html-to-text": "^9.0.4",
|
||||||
"@types/is-valid-domain": "^0.0.2",
|
"@types/is-valid-domain": "^0.0.2",
|
||||||
"@types/papaparse": "^5.3.9",
|
"@types/papaparse": "^5.3.9",
|
||||||
"@types/parse-link-header": "^2.0.3",
|
"@types/parse-link-header": "^2.0.3",
|
||||||
|
|
|
@ -220,7 +220,7 @@ function StatusMediaEntry({ media }: { media: MediaAttachment }) {
|
||||||
|
|
||||||
function StatusFooter({ status }: { status: StatusType }) {
|
function StatusFooter({ status }: { status: StatusType }) {
|
||||||
return (
|
return (
|
||||||
<aside className="status-info" aria-hidden="true">
|
<aside className="status-info">
|
||||||
<dl className="status-stats">
|
<dl className="status-stats">
|
||||||
<div className="stats-grouping">
|
<div className="stats-grouping">
|
||||||
<div className="stats-item published-at text-cutoff">
|
<div className="stats-item published-at text-cutoff">
|
||||||
|
|
|
@ -114,7 +114,7 @@ const extended = gtsApi.injectEndpoints({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: `/api/v1/admin/accounts/${id}/${approve_or_reject}`,
|
url: `/api/v1/admin/accounts/${id}/${approve_or_reject}`,
|
||||||
asForm: true,
|
asForm: true,
|
||||||
body: approve_or_reject === "reject" ?? formData,
|
body: approve_or_reject === "reject" && formData,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
// Do an optimistic update on this account to mark it approved
|
// Do an optimistic update on this account to mark it approved
|
||||||
|
|
|
@ -77,8 +77,8 @@ const extended = gtsApi.injectEndpoints({
|
||||||
}),
|
}),
|
||||||
invalidatesTags: (res) =>
|
invalidatesTags: (res) =>
|
||||||
res
|
res
|
||||||
? [{ type: "Report", id: "LIST" }, { type: "Report", id: res.id }]
|
? [{ type: "Report", id: "TRANSFORMED" }, { type: "Report", id: res.id }]
|
||||||
: [{ type: "Report", id: "LIST" }]
|
: [{ type: "Report", id: "TRANSFORMED" }]
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
|
@ -168,6 +168,7 @@ export const gtsApi = createApi({
|
||||||
"HTTPHeaderAllows",
|
"HTTPHeaderAllows",
|
||||||
"HTTPHeaderBlocks",
|
"HTTPHeaderBlocks",
|
||||||
"DefaultInteractionPolicies",
|
"DefaultInteractionPolicies",
|
||||||
|
"InteractionRequest",
|
||||||
],
|
],
|
||||||
endpoints: (build) => ({
|
endpoints: (build) => ({
|
||||||
instanceV1: build.query<InstanceV1, void>({
|
instanceV1: build.query<InstanceV1, void>({
|
||||||
|
|
|
@ -0,0 +1,97 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
InteractionRequest,
|
||||||
|
SearchInteractionRequestsParams,
|
||||||
|
SearchInteractionRequestsResp,
|
||||||
|
} from "../../types/interaction";
|
||||||
|
import { gtsApi } from "../gts-api";
|
||||||
|
import parse from "parse-link-header";
|
||||||
|
|
||||||
|
const extended = gtsApi.injectEndpoints({
|
||||||
|
endpoints: (build) => ({
|
||||||
|
getInteractionRequest: build.query<InteractionRequest, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "GET",
|
||||||
|
url: `/api/v1/interaction_requests/${id}`,
|
||||||
|
}),
|
||||||
|
providesTags: (_result, _error, id) => [
|
||||||
|
{ type: 'InteractionRequest', id }
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
|
||||||
|
searchInteractionRequests: build.query<SearchInteractionRequestsResp, SearchInteractionRequestsParams>({
|
||||||
|
query: (form) => {
|
||||||
|
const params = new(URLSearchParams);
|
||||||
|
Object.entries(form).forEach(([k, v]) => {
|
||||||
|
if (v !== undefined) {
|
||||||
|
params.append(k, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let query = "";
|
||||||
|
if (params.size !== 0) {
|
||||||
|
query = `?${params.toString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
url: `/api/v1/interaction_requests${query}`
|
||||||
|
};
|
||||||
|
},
|
||||||
|
// Headers required for paging.
|
||||||
|
transformResponse: (apiResp: InteractionRequest[], meta) => {
|
||||||
|
const requests = apiResp;
|
||||||
|
const linksStr = meta?.response?.headers.get("Link");
|
||||||
|
const links = parse(linksStr);
|
||||||
|
return { requests, links };
|
||||||
|
},
|
||||||
|
providesTags: [{ type: "InteractionRequest", id: "TRANSFORMED" }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
approveInteractionRequest: build.mutation<InteractionRequest, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/interaction_requests/${id}/authorize`,
|
||||||
|
}),
|
||||||
|
invalidatesTags: (res) =>
|
||||||
|
res
|
||||||
|
? [{ type: "InteractionRequest", id: "TRANSFORMED" }, { type: "InteractionRequest", id: res.id }]
|
||||||
|
: [{ type: "InteractionRequest", id: "TRANSFORMED" }]
|
||||||
|
}),
|
||||||
|
|
||||||
|
rejectInteractionRequest: build.mutation<any, string>({
|
||||||
|
query: (id) => ({
|
||||||
|
method: "POST",
|
||||||
|
url: `/api/v1/interaction_requests/${id}/reject`,
|
||||||
|
}),
|
||||||
|
invalidatesTags: (res) =>
|
||||||
|
res
|
||||||
|
? [{ type: "InteractionRequest", id: "TRANSFORMED" }, { type: "InteractionRequest", id: res.id }]
|
||||||
|
: [{ type: "InteractionRequest", id: "TRANSFORMED" }]
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export const {
|
||||||
|
useGetInteractionRequestQuery,
|
||||||
|
useLazySearchInteractionRequestsQuery,
|
||||||
|
useApproveInteractionRequestMutation,
|
||||||
|
useRejectInteractionRequestMutation,
|
||||||
|
} = extended;
|
|
@ -17,6 +17,10 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { Links } from "parse-link-header";
|
||||||
|
import { Account } from "./account";
|
||||||
|
import { Status } from "./status";
|
||||||
|
|
||||||
export interface DefaultInteractionPolicies {
|
export interface DefaultInteractionPolicies {
|
||||||
direct: InteractionPolicy;
|
direct: InteractionPolicy;
|
||||||
private: InteractionPolicy;
|
private: InteractionPolicy;
|
||||||
|
@ -61,3 +65,81 @@ export {
|
||||||
PolicyValueAuthor,
|
PolicyValueAuthor,
|
||||||
PolicyValueMe,
|
PolicyValueMe,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interaction request targeting a status by an account.
|
||||||
|
*/
|
||||||
|
export interface InteractionRequest {
|
||||||
|
/**
|
||||||
|
* ID of the request.
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
/**
|
||||||
|
* Type of interaction being requested.
|
||||||
|
*/
|
||||||
|
type: "favourite" | "reply" | "reblog";
|
||||||
|
/**
|
||||||
|
* Time when the request was created.
|
||||||
|
*/
|
||||||
|
created_at: string;
|
||||||
|
/**
|
||||||
|
* Account that created the request.
|
||||||
|
*/
|
||||||
|
account: Account;
|
||||||
|
/**
|
||||||
|
* Status being interacted with.
|
||||||
|
*/
|
||||||
|
status: Status;
|
||||||
|
/**
|
||||||
|
* Replying status, if type = "reply".
|
||||||
|
*/
|
||||||
|
reply?: Status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parameters for GET to /api/v1/interaction_requests.
|
||||||
|
*/
|
||||||
|
export interface SearchInteractionRequestsParams {
|
||||||
|
/**
|
||||||
|
* If set, show only requests targeting the given status_id.
|
||||||
|
*/
|
||||||
|
status_id?: string;
|
||||||
|
/**
|
||||||
|
* If true or not set, include favourites in the results.
|
||||||
|
*/
|
||||||
|
favourites?: boolean;
|
||||||
|
/**
|
||||||
|
* If true or not set, include replies in the results.
|
||||||
|
*/
|
||||||
|
replies?: boolean;
|
||||||
|
/**
|
||||||
|
* If true or not set, include reblogs in the results.
|
||||||
|
*/
|
||||||
|
reblogs?: boolean;
|
||||||
|
/**
|
||||||
|
* If set, show only requests older (ie., lower) than the given ID.
|
||||||
|
* Request with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
max_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only requests newer (ie., higher) than the given ID.
|
||||||
|
* Request with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
since_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, show only requests *immediately newer* than the given ID.
|
||||||
|
* Request with the given ID will not be included in response.
|
||||||
|
*/
|
||||||
|
min_id?: string;
|
||||||
|
/**
|
||||||
|
* If set, limit returned requests to this number.
|
||||||
|
* Else, fall back to GtS API defaults.
|
||||||
|
*/
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchInteractionRequestsResp {
|
||||||
|
requests: InteractionRequest[];
|
||||||
|
links: Links | null;
|
||||||
|
}
|
||||||
|
|
|
@ -1317,10 +1317,10 @@ button.tab-button {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
dt, dd {
|
dt, dd, span {
|
||||||
/*
|
/*
|
||||||
Make sure any fa icons used in keys
|
Make sure any fa icons used in keys
|
||||||
or values are properly aligned.
|
or values etc. are properly aligned.
|
||||||
*/
|
*/
|
||||||
.fa {
|
.fa {
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
|
@ -1516,6 +1516,60 @@ button.tab-button {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.interaction-requests-view {
|
||||||
|
.interaction-request {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
color: $fg;
|
||||||
|
|
||||||
|
.info-list {
|
||||||
|
border: none;
|
||||||
|
|
||||||
|
.info-list-entry {
|
||||||
|
grid-template-columns: max(20%, 8rem) 1fr;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
> .mutation-button
|
||||||
|
> button {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.interaction-request-detail {
|
||||||
|
.overview {
|
||||||
|
margin-top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thread .status .status-info {
|
||||||
|
border-bottom-left-radius: $br;
|
||||||
|
border-bottom-right-radius: $br;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (orientation: portrait) {
|
@media screen and (orientation: portrait) {
|
||||||
.reports .report .byline {
|
.reports .report .byline {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useMemo } from "react";
|
||||||
|
import { useLocation, useParams } from "wouter";
|
||||||
|
import FormWithData from "../../../lib/form/form-with-data";
|
||||||
|
import BackButton from "../../../components/back-button";
|
||||||
|
import { useBaseUrl } from "../../../lib/navigation/util";
|
||||||
|
import { useApproveInteractionRequestMutation, useGetInteractionRequestQuery, useRejectInteractionRequestMutation } from "../../../lib/query/user/interactions";
|
||||||
|
import { InteractionRequest } from "../../../lib/types/interaction";
|
||||||
|
import { useIcon, useNoun, useVerbed } from "./util";
|
||||||
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
import { Status } from "../../../components/status";
|
||||||
|
|
||||||
|
export default function InteractionRequestDetail({ }) {
|
||||||
|
const params: { reqId: string } = useParams();
|
||||||
|
const baseUrl = useBaseUrl();
|
||||||
|
const backLocation: String = history.state?.backLocation ?? `~${baseUrl}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="interaction-request-detail">
|
||||||
|
<h1><BackButton to={backLocation}/> Interaction Request Details</h1>
|
||||||
|
<FormWithData
|
||||||
|
dataQuery={useGetInteractionRequestQuery}
|
||||||
|
queryArg={params.reqId}
|
||||||
|
DataForm={InteractionRequestDetailForm}
|
||||||
|
{...{ backLocation: backLocation }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function InteractionRequestDetailForm({ data: req, backLocation }: { data: InteractionRequest, backLocation: string }) {
|
||||||
|
const [ _location, setLocation ] = useLocation();
|
||||||
|
|
||||||
|
const [ approve, approveResult ] = useApproveInteractionRequestMutation();
|
||||||
|
const [ reject, rejectResult ] = useRejectInteractionRequestMutation();
|
||||||
|
|
||||||
|
const verbed = useVerbed(req.type);
|
||||||
|
const noun = useNoun(req.type);
|
||||||
|
const icon = useIcon(req.type);
|
||||||
|
|
||||||
|
const strap = useMemo(() => {
|
||||||
|
return "@" + req.account.acct + " " + verbed + " your post.";
|
||||||
|
}, [req.account, verbed]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<span className="overview">
|
||||||
|
<i
|
||||||
|
className={`fa fa-fw ${icon}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/> <strong>{strap}</strong>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<h2>You wrote:</h2>
|
||||||
|
<div className="thread">
|
||||||
|
<Status status={req.status} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ req.reply && <>
|
||||||
|
<h2>They replied:</h2>
|
||||||
|
<div className="thread">
|
||||||
|
<Status status={req.reply} />
|
||||||
|
</div>
|
||||||
|
</> }
|
||||||
|
|
||||||
|
<div className="action-buttons">
|
||||||
|
<MutationButton
|
||||||
|
label={`Accept ${noun}`}
|
||||||
|
title={`Accept ${noun}`}
|
||||||
|
type="button"
|
||||||
|
className="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
approve(req.id);
|
||||||
|
setLocation(backLocation);
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
showError={false}
|
||||||
|
result={approveResult}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MutationButton
|
||||||
|
label={`Reject ${noun}`}
|
||||||
|
title={`Reject ${noun}`}
|
||||||
|
type="button"
|
||||||
|
className="button danger"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
reject(req.id);
|
||||||
|
setLocation(backLocation);
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
showError={false}
|
||||||
|
result={rejectResult}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,36 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import InteractionRequestsSearchForm from "./search";
|
||||||
|
|
||||||
|
export default function InteractionRequests() {
|
||||||
|
return (
|
||||||
|
<div className="interaction-requests-view">
|
||||||
|
<div className="form-section-docs">
|
||||||
|
<h1>Interaction Requests</h1>
|
||||||
|
<p>
|
||||||
|
On this page you can search through interaction requests
|
||||||
|
targeting your statuses, and approve or reject them.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<InteractionRequestsSearchForm />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,251 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { ReactNode, useEffect, useMemo } from "react";
|
||||||
|
|
||||||
|
import { useBoolInput, useTextInput } from "../../../lib/form";
|
||||||
|
import { PageableList } from "../../../components/pageable-list";
|
||||||
|
import MutationButton from "../../../components/form/mutation-button";
|
||||||
|
import { useLocation, useSearch } from "wouter";
|
||||||
|
import { useApproveInteractionRequestMutation, useLazySearchInteractionRequestsQuery, useRejectInteractionRequestMutation } from "../../../lib/query/user/interactions";
|
||||||
|
import { InteractionRequest } from "../../../lib/types/interaction";
|
||||||
|
import { Checkbox } from "../../../components/form/inputs";
|
||||||
|
import { useContent, useIcon, useNoun, useVerbed } from "./util";
|
||||||
|
|
||||||
|
function defaultTrue(urlQueryVal: string | null): boolean {
|
||||||
|
if (urlQueryVal === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlQueryVal.toLowerCase() !== "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function InteractionRequestsSearchForm() {
|
||||||
|
const [ location, setLocation ] = useLocation();
|
||||||
|
const search = useSearch();
|
||||||
|
const urlQueryParams = useMemo(() => new URLSearchParams(search), [search]);
|
||||||
|
const [ searchReqs, searchRes ] = useLazySearchInteractionRequestsQuery();
|
||||||
|
|
||||||
|
// Populate search form using values from
|
||||||
|
// urlQueryParams, to allow paging.
|
||||||
|
const form = {
|
||||||
|
statusID: useTextInput("status_id", {
|
||||||
|
defaultValue: urlQueryParams.get("status_id") ?? ""
|
||||||
|
}),
|
||||||
|
likes: useBoolInput("favourites", {
|
||||||
|
defaultValue: defaultTrue(urlQueryParams.get("favourites"))
|
||||||
|
}),
|
||||||
|
replies: useBoolInput("replies", {
|
||||||
|
defaultValue: defaultTrue(urlQueryParams.get("replies"))
|
||||||
|
}),
|
||||||
|
boosts: useBoolInput("reblogs", {
|
||||||
|
defaultValue: defaultTrue(urlQueryParams.get("reblogs"))
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
// On mount, trigger search.
|
||||||
|
useEffect(() => {
|
||||||
|
searchReqs(Object.fromEntries(urlQueryParams), true);
|
||||||
|
}, [urlQueryParams, searchReqs]);
|
||||||
|
|
||||||
|
// Rather than triggering the search directly,
|
||||||
|
// the "submit" button changes the location
|
||||||
|
// based on form field params, and lets the
|
||||||
|
// useEffect hook above actually do the search.
|
||||||
|
function submitQuery(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Parse query parameters.
|
||||||
|
const entries = Object.entries(form).map(([k, v]) => {
|
||||||
|
// Take only defined form fields.
|
||||||
|
if (v.value === undefined) {
|
||||||
|
return null;
|
||||||
|
} else if (typeof v.value === "string" && v.value.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [[k, v.value.toString()]];
|
||||||
|
}).flatMap(kv => {
|
||||||
|
// Remove any nulls.
|
||||||
|
return kv !== null ? kv : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchParams = new URLSearchParams(entries);
|
||||||
|
setLocation(location + "?" + searchParams.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location to return to when user clicks
|
||||||
|
// "back" on the interaction req detail view.
|
||||||
|
const backLocation = location + (urlQueryParams.size > 0 ? `?${urlQueryParams}` : "");
|
||||||
|
|
||||||
|
// Function to map an item to a list entry.
|
||||||
|
function itemToEntry(req: InteractionRequest): ReactNode {
|
||||||
|
return (
|
||||||
|
<ReqsListEntry
|
||||||
|
key={req.id}
|
||||||
|
req={req}
|
||||||
|
linkTo={`/${req.id}`}
|
||||||
|
backLocation={backLocation}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form
|
||||||
|
onSubmit={submitQuery}
|
||||||
|
// Prevent password managers
|
||||||
|
// trying to fill in fields.
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
label="Include likes"
|
||||||
|
field={form.likes}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Include replies"
|
||||||
|
field={form.replies}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Include boosts"
|
||||||
|
field={form.boosts}
|
||||||
|
/>
|
||||||
|
<MutationButton
|
||||||
|
disabled={false}
|
||||||
|
label={"Search"}
|
||||||
|
result={searchRes}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
<PageableList
|
||||||
|
isLoading={searchRes.isLoading}
|
||||||
|
isFetching={searchRes.isFetching}
|
||||||
|
isSuccess={searchRes.isSuccess}
|
||||||
|
items={searchRes.data?.requests}
|
||||||
|
itemToEntry={itemToEntry}
|
||||||
|
isError={searchRes.isError}
|
||||||
|
error={searchRes.error}
|
||||||
|
emptyMessage={<b>No interaction requests found that match your query.</b>}
|
||||||
|
prevNextLinks={searchRes.data?.links}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ReqsListEntryProps {
|
||||||
|
req: InteractionRequest;
|
||||||
|
linkTo: string;
|
||||||
|
backLocation: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ReqsListEntry({ req, linkTo, backLocation }: ReqsListEntryProps) {
|
||||||
|
const [ _location, setLocation ] = useLocation();
|
||||||
|
|
||||||
|
const [ approve, approveResult ] = useApproveInteractionRequestMutation();
|
||||||
|
const [ reject, rejectResult ] = useRejectInteractionRequestMutation();
|
||||||
|
|
||||||
|
const verbed = useVerbed(req.type);
|
||||||
|
const noun = useNoun(req.type);
|
||||||
|
const icon = useIcon(req.type);
|
||||||
|
|
||||||
|
const strap = useMemo(() => {
|
||||||
|
return "@" + req.account.acct + " " + verbed + " your post.";
|
||||||
|
}, [req.account, verbed]);
|
||||||
|
|
||||||
|
const label = useMemo(() => {
|
||||||
|
return noun + " from @" + req.account.acct;
|
||||||
|
}, [req.account, noun]);
|
||||||
|
|
||||||
|
const ourContent = useContent(req.status);
|
||||||
|
const theirContent = useContent(req.reply);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`pseudolink entry interaction-request`}
|
||||||
|
aria-label={label}
|
||||||
|
title={label}
|
||||||
|
onClick={() => {
|
||||||
|
// When clicking on a request, direct
|
||||||
|
// to the detail view for that request.
|
||||||
|
setLocation(linkTo, {
|
||||||
|
// Store the back location in history so
|
||||||
|
// the detail view can use it to return to
|
||||||
|
// this page (including query parameters).
|
||||||
|
state: { backLocation: backLocation }
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
role="link"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span className="text-cutoff">
|
||||||
|
<i
|
||||||
|
className={`fa fa-fw ${icon}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
/> <strong>{strap}</strong>
|
||||||
|
</span>
|
||||||
|
<dl className="info-list">
|
||||||
|
<div className="info-list-entry">
|
||||||
|
<dt>You wrote:</dt>
|
||||||
|
<dd className="text-cutoff">
|
||||||
|
{ourContent}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
{ req.type === "reply" &&
|
||||||
|
<div className="info-list-entry">
|
||||||
|
<dt>They wrote:</dt>
|
||||||
|
<dd className="text-cutoff">
|
||||||
|
{theirContent}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</dl>
|
||||||
|
<div className="action-buttons">
|
||||||
|
<MutationButton
|
||||||
|
label="Accept"
|
||||||
|
title={`Accept ${noun}`}
|
||||||
|
type="button"
|
||||||
|
className="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
approve(req.id);
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
showError={false}
|
||||||
|
result={approveResult}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MutationButton
|
||||||
|
label="Reject"
|
||||||
|
title={`Reject ${noun}`}
|
||||||
|
type="button"
|
||||||
|
className="button danger"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
reject(req.id);
|
||||||
|
}}
|
||||||
|
disabled={false}
|
||||||
|
showError={false}
|
||||||
|
result={rejectResult}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
import sanitize from "sanitize-html";
|
||||||
|
import { compile, HtmlToTextOptions } from "html-to-text";
|
||||||
|
import { Status } from "../../../lib/types/status";
|
||||||
|
|
||||||
|
// Options for converting HTML statuses
|
||||||
|
// to plaintext representations.
|
||||||
|
const convertOptions: HtmlToTextOptions = {
|
||||||
|
selectors: [
|
||||||
|
// Don't fancy format links, just use their text value.
|
||||||
|
{ selector: 'a', options: { ignoreHref: true } },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const convertHTML = compile(convertOptions);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert input status to plaintext representation.
|
||||||
|
* @param status
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function useContent(status: Status | undefined): string {
|
||||||
|
return useMemo(() => {
|
||||||
|
if (!status) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.content.length === 0) {
|
||||||
|
return "[no content set]";
|
||||||
|
} else {
|
||||||
|
// HTML has already been through
|
||||||
|
// the instance sanitizer by now,
|
||||||
|
// but do it again just in case.
|
||||||
|
const content = sanitize(status.content);
|
||||||
|
|
||||||
|
// Return plaintext of sanitized HTML.
|
||||||
|
return convertHTML(content);
|
||||||
|
}
|
||||||
|
}, [status]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useVerbed(type: "favourite" | "reply" | "reblog"): string {
|
||||||
|
return useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case "favourite":
|
||||||
|
return "liked";
|
||||||
|
case "reply":
|
||||||
|
return "replied to";
|
||||||
|
case "reblog":
|
||||||
|
return "boosted";
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNoun(type: "favourite" | "reply" | "reblog"): string {
|
||||||
|
return useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case "favourite":
|
||||||
|
return "Like";
|
||||||
|
case "reply":
|
||||||
|
return "Reply";
|
||||||
|
case "reblog":
|
||||||
|
return "Boost";
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useIcon(type: "favourite" | "reply" | "reblog"): string {
|
||||||
|
return useMemo(() => {
|
||||||
|
switch (type) {
|
||||||
|
case "favourite":
|
||||||
|
return "fa-star";
|
||||||
|
case "reply":
|
||||||
|
return "fa-reply";
|
||||||
|
case "reblog":
|
||||||
|
return "fa-retweet";
|
||||||
|
}
|
||||||
|
}, [type]);
|
||||||
|
}
|
|
@ -43,6 +43,11 @@ export default function UserMenu() {
|
||||||
itemUrl="posts"
|
itemUrl="posts"
|
||||||
icon="fa-paper-plane"
|
icon="fa-paper-plane"
|
||||||
/>
|
/>
|
||||||
|
<MenuItem
|
||||||
|
name="Interaction Requests"
|
||||||
|
itemUrl="interaction_requests"
|
||||||
|
icon="fa-commenting-o"
|
||||||
|
/>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
name="Email & Password"
|
name="Email & Password"
|
||||||
itemUrl="emailpassword"
|
itemUrl="emailpassword"
|
||||||
|
|
|
@ -26,6 +26,8 @@ import UserMigration from "./migration";
|
||||||
import PostSettings from "./posts";
|
import PostSettings from "./posts";
|
||||||
import EmailPassword from "./emailpassword";
|
import EmailPassword from "./emailpassword";
|
||||||
import ExportImport from "./export-import";
|
import ExportImport from "./export-import";
|
||||||
|
import InteractionRequests from "./interactions";
|
||||||
|
import InteractionRequestDetail from "./interactions/detail";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* - /settings/user/profile
|
* - /settings/user/profile
|
||||||
|
@ -33,6 +35,7 @@ import ExportImport from "./export-import";
|
||||||
* - /settings/user/emailpassword
|
* - /settings/user/emailpassword
|
||||||
* - /settings/user/migration
|
* - /settings/user/migration
|
||||||
* - /settings/user/export-import
|
* - /settings/user/export-import
|
||||||
|
* - /settings/users/interaction_requests
|
||||||
*/
|
*/
|
||||||
export default function UserRouter() {
|
export default function UserRouter() {
|
||||||
const baseUrl = useBaseUrl();
|
const baseUrl = useBaseUrl();
|
||||||
|
@ -52,6 +55,31 @@ export default function UserRouter() {
|
||||||
<Route><Redirect to="/profile" /></Route>
|
<Route><Redirect to="/profile" /></Route>
|
||||||
</Switch>
|
</Switch>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
|
<InteractionRequestsRouter />
|
||||||
|
</Router>
|
||||||
|
</BaseUrlContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - /settings/users/interaction_requests/search
|
||||||
|
* - /settings/users/interaction_requests/{reqId}
|
||||||
|
*/
|
||||||
|
function InteractionRequestsRouter() {
|
||||||
|
const parentUrl = useBaseUrl();
|
||||||
|
const thisBase = "/interaction_requests";
|
||||||
|
const absBase = parentUrl + thisBase;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BaseUrlContext.Provider value={absBase}>
|
||||||
|
<Router base={thisBase}>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<Switch>
|
||||||
|
<Route path="/search" component={InteractionRequests} />
|
||||||
|
<Route path="/:reqId" component={InteractionRequestDetail} />
|
||||||
|
<Route><Redirect to="/search"/></Route>
|
||||||
|
</Switch>
|
||||||
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
</BaseUrlContext.Provider>
|
</BaseUrlContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1411,6 +1411,14 @@
|
||||||
redux-thunk "^2.4.2"
|
redux-thunk "^2.4.2"
|
||||||
reselect "^4.1.8"
|
reselect "^4.1.8"
|
||||||
|
|
||||||
|
"@selderee/plugin-htmlparser2@^0.11.0":
|
||||||
|
version "0.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz#d5b5e29a7ba6d3958a1972c7be16f4b2c188c517"
|
||||||
|
integrity sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==
|
||||||
|
dependencies:
|
||||||
|
domhandler "^5.0.3"
|
||||||
|
selderee "^0.11.0"
|
||||||
|
|
||||||
"@tsconfig/node10@^1.0.7":
|
"@tsconfig/node10@^1.0.7":
|
||||||
version "1.0.9"
|
version "1.0.9"
|
||||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
||||||
|
@ -1439,6 +1447,11 @@
|
||||||
"@types/react" "*"
|
"@types/react" "*"
|
||||||
hoist-non-react-statics "^3.3.0"
|
hoist-non-react-statics "^3.3.0"
|
||||||
|
|
||||||
|
"@types/html-to-text@^9.0.4":
|
||||||
|
version "9.0.4"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/html-to-text/-/html-to-text-9.0.4.tgz#4a83dd8ae8bfa91457d0b1ffc26f4d0537eff58c"
|
||||||
|
integrity sha512-pUY3cKH/Nm2yYrEmDlPR1mR7yszjGx4DrwPjQ702C4/D5CwHuZTgZdIdwPkRbcuhs7BAh2L5rg3CL5cbRiGTCQ==
|
||||||
|
|
||||||
"@types/http-proxy@^1.17.8":
|
"@types/http-proxy@^1.17.8":
|
||||||
version "1.17.12"
|
version "1.17.12"
|
||||||
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.12.tgz#86e849e9eeae0362548803c37a0a1afc616bd96b"
|
resolved "https://registry.yarnpkg.com/@types/http-proxy/-/http-proxy-1.17.12.tgz#86e849e9eeae0362548803c37a0a1afc616bd96b"
|
||||||
|
@ -3004,7 +3017,7 @@ deep-is@^0.1.3, deep-is@~0.1.3:
|
||||||
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831"
|
||||||
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
|
||||||
|
|
||||||
deepmerge@^4.2.2:
|
deepmerge@^4.2.2, deepmerge@^4.3.1:
|
||||||
version "4.3.1"
|
version "4.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||||
|
@ -4078,12 +4091,23 @@ hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
react-is "^16.7.0"
|
react-is "^16.7.0"
|
||||||
|
|
||||||
|
html-to-text@^9.0.5:
|
||||||
|
version "9.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/html-to-text/-/html-to-text-9.0.5.tgz#6149a0f618ae7a0db8085dca9bbf96d32bb8368d"
|
||||||
|
integrity sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==
|
||||||
|
dependencies:
|
||||||
|
"@selderee/plugin-htmlparser2" "^0.11.0"
|
||||||
|
deepmerge "^4.3.1"
|
||||||
|
dom-serializer "^2.0.0"
|
||||||
|
htmlparser2 "^8.0.2"
|
||||||
|
selderee "^0.11.0"
|
||||||
|
|
||||||
htmlescape@^1.1.0:
|
htmlescape@^1.1.0:
|
||||||
version "1.1.1"
|
version "1.1.1"
|
||||||
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
|
resolved "https://registry.yarnpkg.com/htmlescape/-/htmlescape-1.1.1.tgz#3a03edc2214bca3b66424a3e7959349509cb0351"
|
||||||
integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==
|
integrity sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==
|
||||||
|
|
||||||
htmlparser2@^8.0.0:
|
htmlparser2@^8.0.0, htmlparser2@^8.0.2:
|
||||||
version "8.0.2"
|
version "8.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-8.0.2.tgz#f002151705b383e62433b5cf466f5b716edaec21"
|
||||||
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
integrity sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==
|
||||||
|
@ -4696,6 +4720,11 @@ langs@^2.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/langs/-/langs-2.0.0.tgz#00c32ce48152a49a614450b9ba2632ab58a0a364"
|
resolved "https://registry.yarnpkg.com/langs/-/langs-2.0.0.tgz#00c32ce48152a49a614450b9ba2632ab58a0a364"
|
||||||
integrity sha512-v4pxOBEQVN1WBTfB1crhTtxzNLZU9HPWgadlwzWKISJtt6Ku/CnpBrwVy+jFv8StjxsPfwPFzO0CMwdZLJ0/BA==
|
integrity sha512-v4pxOBEQVN1WBTfB1crhTtxzNLZU9HPWgadlwzWKISJtt6Ku/CnpBrwVy+jFv8StjxsPfwPFzO0CMwdZLJ0/BA==
|
||||||
|
|
||||||
|
leac@^0.6.0:
|
||||||
|
version "0.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
|
||||||
|
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
|
||||||
|
|
||||||
levn@^0.4.1:
|
levn@^0.4.1:
|
||||||
version "0.4.1"
|
version "0.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"
|
||||||
|
@ -5261,6 +5290,14 @@ parse-srcset@^1.0.2:
|
||||||
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
resolved "https://registry.yarnpkg.com/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1"
|
||||||
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
integrity sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==
|
||||||
|
|
||||||
|
parseley@^0.12.0:
|
||||||
|
version "0.12.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/parseley/-/parseley-0.12.1.tgz#4afd561d50215ebe259e3e7a853e62f600683aef"
|
||||||
|
integrity sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==
|
||||||
|
dependencies:
|
||||||
|
leac "^0.6.0"
|
||||||
|
peberminta "^0.9.0"
|
||||||
|
|
||||||
parseurl@~1.3.3:
|
parseurl@~1.3.3:
|
||||||
version "1.3.3"
|
version "1.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||||
|
@ -5317,6 +5354,11 @@ pbkdf2@^3.0.3:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
sha.js "^2.4.8"
|
sha.js "^2.4.8"
|
||||||
|
|
||||||
|
peberminta@^0.9.0:
|
||||||
|
version "0.9.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
|
||||||
|
integrity sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==
|
||||||
|
|
||||||
photoswipe-dynamic-caption-plugin@^1.2.7:
|
photoswipe-dynamic-caption-plugin@^1.2.7:
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2"
|
resolved "https://registry.yarnpkg.com/photoswipe-dynamic-caption-plugin/-/photoswipe-dynamic-caption-plugin-1.2.7.tgz#53aa5059f1c4dccc8aa36196ff3e09baa5e537c2"
|
||||||
|
@ -5966,6 +6008,13 @@ scope-analyzer@^2.0.1:
|
||||||
estree-is-function "^1.0.0"
|
estree-is-function "^1.0.0"
|
||||||
get-assigned-identifiers "^1.1.0"
|
get-assigned-identifiers "^1.1.0"
|
||||||
|
|
||||||
|
selderee@^0.11.0:
|
||||||
|
version "0.11.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/selderee/-/selderee-0.11.0.tgz#6af0c7983e073ad3e35787ffe20cefd9daf0ec8a"
|
||||||
|
integrity sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==
|
||||||
|
dependencies:
|
||||||
|
parseley "^0.12.0"
|
||||||
|
|
||||||
semver@^6.1.0, semver@^6.3.1:
|
semver@^6.1.0, semver@^6.3.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
|
|
Loading…
Reference in New Issue