mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
[feature] Status thread mute/unmute functionality (#2278)
* add db models + functions for keeping track of threads * give em the old linty testy * create, remove, check mutes * swagger * testerino * test mute/unmute via api * add info log about new index creation * thread + allow muting of any remote statuses that mention a local account * IsStatusThreadMutedBy -> IsThreadMutedByAccount * use common processing functions in status processor * set = NULL * favee! * get rekt darlings, darlings get rekt * testrig please, have mercy muy liege
This commit is contained in:
parent
27f4659139
commit
c7b6cd7770
|
@ -6527,6 +6527,44 @@ paths:
|
||||||
summary: View accounts that have faved/starred/liked the target status.
|
summary: View accounts that have faved/starred/liked the target status.
|
||||||
tags:
|
tags:
|
||||||
- statuses
|
- statuses
|
||||||
|
/api/v1/statuses/{id}/mute:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Target status must belong to you or mention you.
|
||||||
|
|
||||||
|
Status thread mutes and unmutes are idempotent. If you already mute a thread, muting it again just means it stays muted and you'll get 200 OK back.
|
||||||
|
operationId: statusMute
|
||||||
|
parameters:
|
||||||
|
- description: Target status ID.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The now-muted status.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
"400":
|
||||||
|
description: bad request; you're not part of the target status thread
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:mutes
|
||||||
|
summary: Mute a status's thread. This prevents notifications from being created for future replies, likes, boosts etc in the thread of which the target status is a part.
|
||||||
|
tags:
|
||||||
|
- statuses
|
||||||
/api/v1/statuses/{id}/pin:
|
/api/v1/statuses/{id}/pin:
|
||||||
post:
|
post:
|
||||||
description: |-
|
description: |-
|
||||||
|
@ -6703,6 +6741,44 @@ paths:
|
||||||
summary: Unstar/unlike/unfavourite the given status.
|
summary: Unstar/unlike/unfavourite the given status.
|
||||||
tags:
|
tags:
|
||||||
- statuses
|
- statuses
|
||||||
|
/api/v1/statuses/{id}/unmute:
|
||||||
|
post:
|
||||||
|
description: |-
|
||||||
|
Target status must belong to you or mention you.
|
||||||
|
|
||||||
|
Status thread mutes and unmutes are idempotent. If you already unmuted a thread, unmuting it again just means it stays unmuted and you'll get 200 OK back.
|
||||||
|
operationId: statusUnmute
|
||||||
|
parameters:
|
||||||
|
- description: Target status ID.
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: The now-unmuted status.
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/status'
|
||||||
|
"400":
|
||||||
|
description: bad request; you're not part of the target status thread
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
|
security:
|
||||||
|
- OAuth2 Bearer:
|
||||||
|
- write:mutes
|
||||||
|
summary: Unmute a status's thread. This reenables notifications for future replies, likes, boosts etc in the thread of which the target status is a part.
|
||||||
|
tags:
|
||||||
|
- statuses
|
||||||
/api/v1/statuses/{id}/unpin:
|
/api/v1/statuses/{id}/unpin:
|
||||||
post:
|
post:
|
||||||
operationId: statusUnpin
|
operationId: statusUnpin
|
||||||
|
@ -7336,6 +7412,7 @@ securityDefinitions:
|
||||||
read:follows: grant read access to follows
|
read:follows: grant read access to follows
|
||||||
read:lists: grant read access to lists
|
read:lists: grant read access to lists
|
||||||
read:media: grant read access to media
|
read:media: grant read access to media
|
||||||
|
read:mutes: grant read access to mutes
|
||||||
read:notifications: grants read access to notifications
|
read:notifications: grants read access to notifications
|
||||||
read:search: grant read access to searches
|
read:search: grant read access to searches
|
||||||
read:statuses: grants read access to statuses
|
read:statuses: grants read access to statuses
|
||||||
|
@ -7347,6 +7424,7 @@ securityDefinitions:
|
||||||
write:follows: grants write access to follows
|
write:follows: grants write access to follows
|
||||||
write:lists: grants write access to lists
|
write:lists: grants write access to lists
|
||||||
write:media: grants write access to media
|
write:media: grants write access to media
|
||||||
|
write:mutes: grants write access to mutes
|
||||||
write:statuses: grants write access to statuses
|
write:statuses: grants write access to statuses
|
||||||
write:user: grants write access to user-level info
|
write:user: grants write access to user-level info
|
||||||
tokenUrl: https://example.org/oauth/token
|
tokenUrl: https://example.org/oauth/token
|
||||||
|
|
|
@ -39,6 +39,7 @@
|
||||||
// read:follows: grant read access to follows
|
// read:follows: grant read access to follows
|
||||||
// read:lists: grant read access to lists
|
// read:lists: grant read access to lists
|
||||||
// read:media: grant read access to media
|
// read:media: grant read access to media
|
||||||
|
// read:mutes: grant read access to mutes
|
||||||
// read:search: grant read access to searches
|
// read:search: grant read access to searches
|
||||||
// read:statuses: grants read access to statuses
|
// read:statuses: grants read access to statuses
|
||||||
// read:streaming: grants read access to streaming api
|
// read:streaming: grants read access to streaming api
|
||||||
|
@ -50,6 +51,7 @@
|
||||||
// write:follows: grants write access to follows
|
// write:follows: grants write access to follows
|
||||||
// write:lists: grants write access to lists
|
// write:lists: grants write access to lists
|
||||||
// write:media: grants write access to media
|
// write:media: grants write access to media
|
||||||
|
// write:mutes: grants write access to mutes
|
||||||
// write:statuses: grants write access to statuses
|
// write:statuses: grants write access to statuses
|
||||||
// write:user: grants write access to user-level info
|
// write:user: grants write access to user-level info
|
||||||
// admin: grants admin access to everything
|
// admin: grants admin access to everything
|
||||||
|
|
|
@ -91,6 +91,10 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
||||||
attachHandler(http.MethodPost, PinPath, m.StatusPinPOSTHandler)
|
attachHandler(http.MethodPost, PinPath, m.StatusPinPOSTHandler)
|
||||||
attachHandler(http.MethodPost, UnpinPath, m.StatusUnpinPOSTHandler)
|
attachHandler(http.MethodPost, UnpinPath, m.StatusUnpinPOSTHandler)
|
||||||
|
|
||||||
|
// mute stuff
|
||||||
|
attachHandler(http.MethodPost, MutePath, m.StatusMutePOSTHandler)
|
||||||
|
attachHandler(http.MethodPost, UnmutePath, m.StatusUnmutePOSTHandler)
|
||||||
|
|
||||||
// reblog stuff
|
// reblog stuff
|
||||||
attachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler)
|
attachHandler(http.MethodPost, ReblogPath, m.StatusBoostPOSTHandler)
|
||||||
attachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler)
|
attachHandler(http.MethodPost, UnreblogPath, m.StatusUnboostPOSTHandler)
|
||||||
|
|
99
internal/api/client/statuses/statusmute.go
Normal file
99
internal/api/client/statuses/statusmute.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// 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 statuses
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusMutePOSTHandler swagger:operation POST /api/v1/statuses/{id}/mute statusMute
|
||||||
|
//
|
||||||
|
// Mute a status's thread. This prevents notifications from being created for future replies, likes, boosts etc in the thread of which the target status is a part.
|
||||||
|
//
|
||||||
|
// Target status must belong to you or mention you.
|
||||||
|
//
|
||||||
|
// Status thread mutes and unmutes are idempotent. If you already mute a thread, muting it again just means it stays muted and you'll get 200 OK back.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - statuses
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// parameters:
|
||||||
|
// -
|
||||||
|
// name: id
|
||||||
|
// type: string
|
||||||
|
// description: Target status ID.
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:mutes
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// name: status
|
||||||
|
// description: The now-muted status.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/status"
|
||||||
|
// '400':
|
||||||
|
// description: bad request; you're not part of the target status thread
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
|
func (m *Module) StatusMutePOSTHandler(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
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStatusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiStatus, errWithCode := m.processor.Status().MuteCreate(c.Request.Context(), authed.Account, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apiStatus)
|
||||||
|
}
|
217
internal/api/client/statuses/statusmute_test.go
Normal file
217
internal/api/client/statuses/statusmute_test.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
// 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 statuses_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/statuses"
|
||||||
|
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatusMuteTestSuite struct {
|
||||||
|
StatusStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusMuteTestSuite) post(path string, handler func(*gin.Context), targetStatusID string) (int, string) {
|
||||||
|
t := suite.testTokens["local_account_1"]
|
||||||
|
oauthToken := oauth.DBTokenToToken(t)
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPost, path, nil) // the endpoint we're hitting
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
// normally the router would populate these params from the path values,
|
||||||
|
// but because we're calling the function directly, we need to set them manually.
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: apiutil.IDKey,
|
||||||
|
Value: targetStatusID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(ctx)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(result.Body)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
indented := bytes.Buffer{}
|
||||||
|
if err := json.Indent(&indented, b, "", " "); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return recorder.Code, indented.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *StatusMuteTestSuite) TestMuteUnmuteStatus() {
|
||||||
|
var (
|
||||||
|
targetStatus = suite.testStatuses["local_account_1_status_1"]
|
||||||
|
path = fmt.Sprintf("http://localhost:8080/api%s", strings.ReplaceAll(statuses.MutePath, ":id", targetStatus.ID))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mute the status, ensure `muted` is `true`.
|
||||||
|
code, muted := suite.post(path, suite.statusModule.StatusMutePOSTHandler, targetStatus.ID)
|
||||||
|
suite.Equal(http.StatusOK, code)
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"created_at": "2021-10-20T10:40:37.000Z",
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"sensitive": true,
|
||||||
|
"spoiler_text": "introduction post",
|
||||||
|
"visibility": "public",
|
||||||
|
"language": "en",
|
||||||
|
"uri": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"replies_count": 2,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": true,
|
||||||
|
"bookmarked": false,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "hello everyone!",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "really cool gts application",
|
||||||
|
"website": "https://reallycool.app"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
|
"username": "the_mighty_zork",
|
||||||
|
"acct": "the_mighty_zork",
|
||||||
|
"display_name": "original zork (he/they)",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-20T11:09:18.000Z",
|
||||||
|
"note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||||
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"followers_count": 2,
|
||||||
|
"following_count": 2,
|
||||||
|
"statuses_count": 5,
|
||||||
|
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"role": {
|
||||||
|
"name": "user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"tags": [],
|
||||||
|
"emojis": [],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "hello everyone!"
|
||||||
|
}`, muted)
|
||||||
|
|
||||||
|
// Unmute the status, ensure `muted` is `false`.
|
||||||
|
code, unmuted := suite.post(path, suite.statusModule.StatusUnmutePOSTHandler, targetStatus.ID)
|
||||||
|
suite.Equal(http.StatusOK, code)
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"created_at": "2021-10-20T10:40:37.000Z",
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"sensitive": true,
|
||||||
|
"spoiler_text": "introduction post",
|
||||||
|
"visibility": "public",
|
||||||
|
"language": "en",
|
||||||
|
"uri": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"url": "http://localhost:8080/@the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
"replies_count": 2,
|
||||||
|
"reblogs_count": 1,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"favourited": false,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": false,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "hello everyone!",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "really cool gts application",
|
||||||
|
"website": "https://reallycool.app"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
|
"username": "the_mighty_zork",
|
||||||
|
"acct": "the_mighty_zork",
|
||||||
|
"display_name": "original zork (he/they)",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-20T11:09:18.000Z",
|
||||||
|
"note": "\u003cp\u003ehey yo this is my profile!\u003c/p\u003e",
|
||||||
|
"url": "http://localhost:8080/@the_mighty_zork",
|
||||||
|
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
|
||||||
|
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
|
||||||
|
"followers_count": 2,
|
||||||
|
"following_count": 2,
|
||||||
|
"statuses_count": 5,
|
||||||
|
"last_status_at": "2022-05-20T11:37:55.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"role": {
|
||||||
|
"name": "user"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media_attachments": [],
|
||||||
|
"mentions": [],
|
||||||
|
"tags": [],
|
||||||
|
"emojis": [],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "hello everyone!"
|
||||||
|
}`, unmuted)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusMuteTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(StatusMuteTestSuite))
|
||||||
|
}
|
99
internal/api/client/statuses/statusunmute.go
Normal file
99
internal/api/client/statuses/statusunmute.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
// 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 statuses
|
||||||
|
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StatusUnmutePOSTHandler swagger:operation POST /api/v1/statuses/{id}/unmute statusUnmute
|
||||||
|
//
|
||||||
|
// Unmute a status's thread. This reenables notifications for future replies, likes, boosts etc in the thread of which the target status is a part.
|
||||||
|
//
|
||||||
|
// Target status must belong to you or mention you.
|
||||||
|
//
|
||||||
|
// Status thread mutes and unmutes are idempotent. If you already unmuted a thread, unmuting it again just means it stays unmuted and you'll get 200 OK back.
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - statuses
|
||||||
|
//
|
||||||
|
// produces:
|
||||||
|
// - application/json
|
||||||
|
//
|
||||||
|
// parameters:
|
||||||
|
// -
|
||||||
|
// name: id
|
||||||
|
// type: string
|
||||||
|
// description: Target status ID.
|
||||||
|
// in: path
|
||||||
|
// required: true
|
||||||
|
//
|
||||||
|
// security:
|
||||||
|
// - OAuth2 Bearer:
|
||||||
|
// - write:mutes
|
||||||
|
//
|
||||||
|
// responses:
|
||||||
|
// '200':
|
||||||
|
// name: status
|
||||||
|
// description: The now-unmuted status.
|
||||||
|
// schema:
|
||||||
|
// "$ref": "#/definitions/status"
|
||||||
|
// '400':
|
||||||
|
// description: bad request; you're not part of the target status thread
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
|
func (m *Module) StatusUnmutePOSTHandler(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
|
||||||
|
}
|
||||||
|
|
||||||
|
targetStatusID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiStatus, errWithCode := m.processor.Status().MuteRemove(c.Request.Context(), authed.Account, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, apiStatus)
|
||||||
|
}
|
1
internal/cache/cache.go
vendored
1
internal/cache/cache.go
vendored
|
@ -248,6 +248,7 @@ func (c *Caches) Sweep(threshold float64) {
|
||||||
c.GTS.Status().Trim(threshold)
|
c.GTS.Status().Trim(threshold)
|
||||||
c.GTS.StatusFave().Trim(threshold)
|
c.GTS.StatusFave().Trim(threshold)
|
||||||
c.GTS.Tag().Trim(threshold)
|
c.GTS.Tag().Trim(threshold)
|
||||||
|
c.GTS.ThreadMute().Trim(threshold)
|
||||||
c.GTS.Tombstone().Trim(threshold)
|
c.GTS.Tombstone().Trim(threshold)
|
||||||
c.GTS.User().Trim(threshold)
|
c.GTS.User().Trim(threshold)
|
||||||
c.Visibility.Trim(threshold)
|
c.Visibility.Trim(threshold)
|
||||||
|
|
30
internal/cache/gts.go
vendored
30
internal/cache/gts.go
vendored
|
@ -57,6 +57,7 @@ type GTSCaches struct {
|
||||||
statusFave *result.Cache[*gtsmodel.StatusFave]
|
statusFave *result.Cache[*gtsmodel.StatusFave]
|
||||||
statusFaveIDs *SliceCache[string]
|
statusFaveIDs *SliceCache[string]
|
||||||
tag *result.Cache[*gtsmodel.Tag]
|
tag *result.Cache[*gtsmodel.Tag]
|
||||||
|
threadMute *result.Cache[*gtsmodel.ThreadMute]
|
||||||
tombstone *result.Cache[*gtsmodel.Tombstone]
|
tombstone *result.Cache[*gtsmodel.Tombstone]
|
||||||
user *result.Cache[*gtsmodel.User]
|
user *result.Cache[*gtsmodel.User]
|
||||||
|
|
||||||
|
@ -93,6 +94,7 @@ func (c *GTSCaches) Init() {
|
||||||
c.initStatus()
|
c.initStatus()
|
||||||
c.initStatusFave()
|
c.initStatusFave()
|
||||||
c.initTag()
|
c.initTag()
|
||||||
|
c.initThreadMute()
|
||||||
c.initStatusFaveIDs()
|
c.initStatusFaveIDs()
|
||||||
c.initTombstone()
|
c.initTombstone()
|
||||||
c.initUser()
|
c.initUser()
|
||||||
|
@ -249,6 +251,11 @@ func (c *GTSCaches) Tag() *result.Cache[*gtsmodel.Tag] {
|
||||||
return c.tag
|
return c.tag
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ThreadMute provides access to the gtsmodel ThreadMute database cache.
|
||||||
|
func (c *GTSCaches) ThreadMute() *result.Cache[*gtsmodel.ThreadMute] {
|
||||||
|
return c.threadMute
|
||||||
|
}
|
||||||
|
|
||||||
// StatusFaveIDs provides access to the status fave IDs list database cache.
|
// StatusFaveIDs provides access to the status fave IDs list database cache.
|
||||||
func (c *GTSCaches) StatusFaveIDs() *SliceCache[string] {
|
func (c *GTSCaches) StatusFaveIDs() *SliceCache[string] {
|
||||||
return c.statusFaveIDs
|
return c.statusFaveIDs
|
||||||
|
@ -712,6 +719,7 @@ func (c *GTSCaches) initStatus() {
|
||||||
{Name: "URI"},
|
{Name: "URI"},
|
||||||
{Name: "URL"},
|
{Name: "URL"},
|
||||||
{Name: "BoostOfID.AccountID"},
|
{Name: "BoostOfID.AccountID"},
|
||||||
|
{Name: "ThreadID", Multi: true},
|
||||||
}, func(s1 *gtsmodel.Status) *gtsmodel.Status {
|
}, func(s1 *gtsmodel.Status) *gtsmodel.Status {
|
||||||
s2 := new(gtsmodel.Status)
|
s2 := new(gtsmodel.Status)
|
||||||
*s2 = *s1
|
*s2 = *s1
|
||||||
|
@ -778,6 +786,28 @@ func (c *GTSCaches) initTag() {
|
||||||
c.tag.IgnoreErrors(ignoreErrors)
|
c.tag.IgnoreErrors(ignoreErrors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GTSCaches) initThreadMute() {
|
||||||
|
cap := calculateResultCacheMax(
|
||||||
|
sizeOfThreadMute(), // model in-mem size.
|
||||||
|
config.GetCacheThreadMuteMemRatio(),
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Infof(nil, "cache size = %d", cap)
|
||||||
|
|
||||||
|
c.threadMute = result.New([]result.Lookup{
|
||||||
|
{Name: "ID"},
|
||||||
|
{Name: "ThreadID", Multi: true},
|
||||||
|
{Name: "AccountID", Multi: true},
|
||||||
|
{Name: "ThreadID.AccountID"},
|
||||||
|
}, func(t1 *gtsmodel.ThreadMute) *gtsmodel.ThreadMute {
|
||||||
|
t2 := new(gtsmodel.ThreadMute)
|
||||||
|
*t2 = *t1
|
||||||
|
return t2
|
||||||
|
}, cap)
|
||||||
|
|
||||||
|
c.threadMute.IgnoreErrors(ignoreErrors)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GTSCaches) initTombstone() {
|
func (c *GTSCaches) initTombstone() {
|
||||||
// Calculate maximum cache size.
|
// Calculate maximum cache size.
|
||||||
cap := calculateResultCacheMax(
|
cap := calculateResultCacheMax(
|
||||||
|
|
11
internal/cache/size.go
vendored
11
internal/cache/size.go
vendored
|
@ -194,6 +194,7 @@ func totalOfRatios() float64 {
|
||||||
config.GetCacheStatusFaveMemRatio() +
|
config.GetCacheStatusFaveMemRatio() +
|
||||||
config.GetCacheStatusFaveIDsMemRatio() +
|
config.GetCacheStatusFaveIDsMemRatio() +
|
||||||
config.GetCacheTagMemRatio() +
|
config.GetCacheTagMemRatio() +
|
||||||
|
config.GetCacheThreadMuteMemRatio() +
|
||||||
config.GetCacheTombstoneMemRatio() +
|
config.GetCacheTombstoneMemRatio() +
|
||||||
config.GetCacheUserMemRatio() +
|
config.GetCacheUserMemRatio() +
|
||||||
config.GetCacheWebfingerMemRatio() +
|
config.GetCacheWebfingerMemRatio() +
|
||||||
|
@ -511,6 +512,16 @@ func sizeofTag() uintptr {
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sizeOfThreadMute() uintptr {
|
||||||
|
return uintptr(size.Of(>smodel.ThreadMute{
|
||||||
|
ID: exampleID,
|
||||||
|
CreatedAt: exampleTime,
|
||||||
|
UpdatedAt: exampleTime,
|
||||||
|
ThreadID: exampleID,
|
||||||
|
AccountID: exampleID,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
func sizeofTombstone() uintptr {
|
func sizeofTombstone() uintptr {
|
||||||
return uintptr(size.Of(>smodel.Tombstone{
|
return uintptr(size.Of(>smodel.Tombstone{
|
||||||
ID: exampleID,
|
ID: exampleID,
|
||||||
|
|
|
@ -207,6 +207,7 @@ type CacheConfiguration struct {
|
||||||
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
|
StatusFaveMemRatio float64 `name:"status-fave-mem-ratio"`
|
||||||
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
|
StatusFaveIDsMemRatio float64 `name:"status-fave-ids-mem-ratio"`
|
||||||
TagMemRatio float64 `name:"tag-mem-ratio"`
|
TagMemRatio float64 `name:"tag-mem-ratio"`
|
||||||
|
ThreadMuteMemRatio float64 `name:"thread-mute-mem-ratio"`
|
||||||
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
|
TombstoneMemRatio float64 `name:"tombstone-mem-ratio"`
|
||||||
UserMemRatio float64 `name:"user-mem-ratio"`
|
UserMemRatio float64 `name:"user-mem-ratio"`
|
||||||
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
|
WebfingerMemRatio float64 `name:"webfinger-mem-ratio"`
|
||||||
|
|
|
@ -174,6 +174,7 @@
|
||||||
StatusFaveMemRatio: 2,
|
StatusFaveMemRatio: 2,
|
||||||
StatusFaveIDsMemRatio: 3,
|
StatusFaveIDsMemRatio: 3,
|
||||||
TagMemRatio: 2,
|
TagMemRatio: 2,
|
||||||
|
ThreadMuteMemRatio: 0.2,
|
||||||
TombstoneMemRatio: 0.5,
|
TombstoneMemRatio: 0.5,
|
||||||
UserMemRatio: 0.25,
|
UserMemRatio: 0.25,
|
||||||
WebfingerMemRatio: 0.1,
|
WebfingerMemRatio: 0.1,
|
||||||
|
|
|
@ -3174,6 +3174,31 @@ func GetCacheTagMemRatio() float64 { return global.GetCacheTagMemRatio() }
|
||||||
// SetCacheTagMemRatio safely sets the value for global configuration 'Cache.TagMemRatio' field
|
// SetCacheTagMemRatio safely sets the value for global configuration 'Cache.TagMemRatio' field
|
||||||
func SetCacheTagMemRatio(v float64) { global.SetCacheTagMemRatio(v) }
|
func SetCacheTagMemRatio(v float64) { global.SetCacheTagMemRatio(v) }
|
||||||
|
|
||||||
|
// GetCacheThreadMuteMemRatio safely fetches the Configuration value for state's 'Cache.ThreadMuteMemRatio' field
|
||||||
|
func (st *ConfigState) GetCacheThreadMuteMemRatio() (v float64) {
|
||||||
|
st.mutex.RLock()
|
||||||
|
v = st.config.Cache.ThreadMuteMemRatio
|
||||||
|
st.mutex.RUnlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCacheThreadMuteMemRatio safely sets the Configuration value for state's 'Cache.ThreadMuteMemRatio' field
|
||||||
|
func (st *ConfigState) SetCacheThreadMuteMemRatio(v float64) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.Cache.ThreadMuteMemRatio = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheThreadMuteMemRatioFlag returns the flag name for the 'Cache.ThreadMuteMemRatio' field
|
||||||
|
func CacheThreadMuteMemRatioFlag() string { return "cache-thread-mute-mem-ratio" }
|
||||||
|
|
||||||
|
// GetCacheThreadMuteMemRatio safely fetches the value for global configuration 'Cache.ThreadMuteMemRatio' field
|
||||||
|
func GetCacheThreadMuteMemRatio() float64 { return global.GetCacheThreadMuteMemRatio() }
|
||||||
|
|
||||||
|
// SetCacheThreadMuteMemRatio safely sets the value for global configuration 'Cache.ThreadMuteMemRatio' field
|
||||||
|
func SetCacheThreadMuteMemRatio(v float64) { global.SetCacheThreadMuteMemRatio(v) }
|
||||||
|
|
||||||
// GetCacheTombstoneMemRatio safely fetches the Configuration value for state's 'Cache.TombstoneMemRatio' field
|
// GetCacheTombstoneMemRatio safely fetches the Configuration value for state's 'Cache.TombstoneMemRatio' field
|
||||||
func (st *ConfigState) GetCacheTombstoneMemRatio() (v float64) {
|
func (st *ConfigState) GetCacheTombstoneMemRatio() (v float64) {
|
||||||
st.mutex.RLock()
|
st.mutex.RLock()
|
||||||
|
|
|
@ -135,7 +135,7 @@ func (b *basicDB) CreateAllTables(ctx context.Context) error {
|
||||||
>smodel.StatusToEmoji{},
|
>smodel.StatusToEmoji{},
|
||||||
>smodel.StatusFave{},
|
>smodel.StatusFave{},
|
||||||
>smodel.StatusBookmark{},
|
>smodel.StatusBookmark{},
|
||||||
>smodel.StatusMute{},
|
>smodel.ThreadMute{},
|
||||||
>smodel.Tag{},
|
>smodel.Tag{},
|
||||||
>smodel.User{},
|
>smodel.User{},
|
||||||
>smodel.Emoji{},
|
>smodel.Emoji{},
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
>smodel.AccountToEmoji{},
|
>smodel.AccountToEmoji{},
|
||||||
>smodel.StatusToEmoji{},
|
>smodel.StatusToEmoji{},
|
||||||
>smodel.StatusToTag{},
|
>smodel.StatusToTag{},
|
||||||
|
>smodel.ThreadToStatus{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// DBService satisfies the DB interface
|
// DBService satisfies the DB interface
|
||||||
|
@ -79,6 +80,7 @@ type DBService struct {
|
||||||
db.StatusBookmark
|
db.StatusBookmark
|
||||||
db.StatusFave
|
db.StatusFave
|
||||||
db.Tag
|
db.Tag
|
||||||
|
db.Thread
|
||||||
db.Timeline
|
db.Timeline
|
||||||
db.User
|
db.User
|
||||||
db.Tombstone
|
db.Tombstone
|
||||||
|
@ -236,6 +238,10 @@ func NewBunDBService(ctx context.Context, state *state.State) (db.DB, error) {
|
||||||
conn: db,
|
conn: db,
|
||||||
state: state,
|
state: state,
|
||||||
},
|
},
|
||||||
|
Thread: &threadDB{
|
||||||
|
db: db,
|
||||||
|
state: state,
|
||||||
|
},
|
||||||
Timeline: &timelineDB{
|
Timeline: &timelineDB{
|
||||||
db: db,
|
db: db,
|
||||||
state: state,
|
state: state,
|
||||||
|
|
|
@ -53,6 +53,7 @@ type BunDBStandardTestSuite struct {
|
||||||
testAccountNotes map[string]*gtsmodel.AccountNote
|
testAccountNotes map[string]*gtsmodel.AccountNote
|
||||||
testMarkers map[string]*gtsmodel.Marker
|
testMarkers map[string]*gtsmodel.Marker
|
||||||
testRules map[string]*gtsmodel.Rule
|
testRules map[string]*gtsmodel.Rule
|
||||||
|
testThreads map[string]*gtsmodel.Thread
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
|
@ -75,6 +76,7 @@ func (suite *BunDBStandardTestSuite) SetupSuite() {
|
||||||
suite.testAccountNotes = testrig.NewTestAccountNotes()
|
suite.testAccountNotes = testrig.NewTestAccountNotes()
|
||||||
suite.testMarkers = testrig.NewTestMarkers()
|
suite.testMarkers = testrig.NewTestMarkers()
|
||||||
suite.testRules = testrig.NewTestRules()
|
suite.testRules = testrig.NewTestRules()
|
||||||
|
suite.testThreads = testrig.NewTestThreads()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *BunDBStandardTestSuite) SetupTest() {
|
func (suite *BunDBStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -0,0 +1,148 @@
|
||||||
|
// 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"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gtsmodel "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
"github.com/uptrace/bun/dialect"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
up := func(ctx context.Context, db *bun.DB) error {
|
||||||
|
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
|
||||||
|
// Create thread table.
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateTable().
|
||||||
|
Model(>smodel.Thread{}).
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create thread intermediate table.
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateTable().
|
||||||
|
Model(>smodel.ThreadToStatus{}).
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old pkey constraint from
|
||||||
|
// deprecated status mute table.
|
||||||
|
//
|
||||||
|
// This is only necessary with postgres.
|
||||||
|
if tx.Dialect().Name() == dialect.PG {
|
||||||
|
if _, err := tx.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"ALTER TABLE ? DROP CONSTRAINT IF EXISTS ?",
|
||||||
|
bun.Ident("status_mutes"),
|
||||||
|
bun.Safe("status_mutes_pkey"),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop old index.
|
||||||
|
if _, err := tx.
|
||||||
|
NewDropIndex().
|
||||||
|
Index("status_mutes_account_id_target_account_id_status_id_idx").
|
||||||
|
IfExists().
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop deprecated status mute table.
|
||||||
|
if _, err := tx.
|
||||||
|
NewDropTable().
|
||||||
|
Table("status_mutes").
|
||||||
|
IfExists().
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new thread mute table.
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateTable().
|
||||||
|
Model(>smodel.ThreadMute{}).
|
||||||
|
IfNotExists().
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info(ctx, "creating a new index on the statuses table, please wait and don't interrupt it (this may take a few minutes)")
|
||||||
|
|
||||||
|
// Update statuses to add thread ID column.
|
||||||
|
_, err := tx.ExecContext(
|
||||||
|
ctx,
|
||||||
|
"ALTER TABLE ? ADD COLUMN ? CHAR(26)",
|
||||||
|
bun.Ident("statuses"),
|
||||||
|
bun.Ident("thread_id"),
|
||||||
|
)
|
||||||
|
if err != nil && !(strings.Contains(err.Error(), "already exists") ||
|
||||||
|
strings.Contains(err.Error(), "duplicate column name") ||
|
||||||
|
strings.Contains(err.Error(), "SQLSTATE 42701")) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Index new + existing tables properly.
|
||||||
|
for table, indexes := range map[string]map[string][]string{
|
||||||
|
"threads": {
|
||||||
|
"threads_id_idx": {"id"},
|
||||||
|
},
|
||||||
|
"thread_mutes": {
|
||||||
|
"thread_mutes_id_idx": {"id"},
|
||||||
|
// Eg., check if target thread is muted by account.
|
||||||
|
"thread_mutes_thread_id_account_id_idx": {"thread_id", "account_id"},
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
// Eg., select all statuses in a thread.
|
||||||
|
"statuses_thread_id_idx": {"thread_id"},
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
for index, columns := range indexes {
|
||||||
|
if _, err := tx.
|
||||||
|
NewCreateIndex().
|
||||||
|
Table(table).
|
||||||
|
Index(index).
|
||||||
|
Column(columns...).
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -324,6 +324,23 @@ func (s *statusDB) PutStatus(ctx context.Context, status *gtsmodel.Status) error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the status is threaded, create
|
||||||
|
// link between thread and status.
|
||||||
|
if status.ThreadID != "" {
|
||||||
|
if _, err := tx.
|
||||||
|
NewInsert().
|
||||||
|
Model(>smodel.ThreadToStatus{
|
||||||
|
ThreadID: status.ThreadID,
|
||||||
|
StatusID: status.ID,
|
||||||
|
}).
|
||||||
|
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("thread_id"), bun.Ident("status_id")).
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, insert the status
|
// Finally, insert the status
|
||||||
_, err := tx.NewInsert().Model(status).Exec(ctx)
|
_, err := tx.NewInsert().Model(status).Exec(ctx)
|
||||||
return err
|
return err
|
||||||
|
@ -390,6 +407,23 @@ func (s *statusDB) UpdateStatus(ctx context.Context, status *gtsmodel.Status, co
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the status is threaded, create
|
||||||
|
// link between thread and status.
|
||||||
|
if status.ThreadID != "" {
|
||||||
|
if _, err := tx.
|
||||||
|
NewInsert().
|
||||||
|
Model(>smodel.ThreadToStatus{
|
||||||
|
ThreadID: status.ThreadID,
|
||||||
|
StatusID: status.ID,
|
||||||
|
}).
|
||||||
|
On("CONFLICT (?, ?) DO NOTHING", bun.Ident("thread_id"), bun.Ident("status_id")).
|
||||||
|
Exec(ctx); err != nil {
|
||||||
|
if !errors.Is(err, db.ErrAlreadyExists) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Finally, update the status
|
// Finally, update the status
|
||||||
_, err := tx.
|
_, err := tx.
|
||||||
NewUpdate().
|
NewUpdate().
|
||||||
|
@ -439,6 +473,17 @@ func (s *statusDB) DeleteStatusByID(ctx context.Context, id string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete links between this status
|
||||||
|
// and any threads it was a part of.
|
||||||
|
_, err = tx.
|
||||||
|
NewDelete().
|
||||||
|
TableExpr("? AS ?", bun.Ident("thread_to_statuses"), bun.Ident("thread_to_status")).
|
||||||
|
Where("? = ?", bun.Ident("thread_to_status.status_id"), id).
|
||||||
|
Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// delete the status itself
|
// delete the status itself
|
||||||
if _, err := tx.
|
if _, err := tx.
|
||||||
NewDelete().
|
NewDelete().
|
||||||
|
@ -634,16 +679,6 @@ func (s *statusDB) getStatusBoostIDs(ctx context.Context, statusID string) ([]st
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *statusDB) IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
|
||||||
q := s.db.
|
|
||||||
NewSelect().
|
|
||||||
TableExpr("? AS ?", bun.Ident("status_mutes"), bun.Ident("status_mute")).
|
|
||||||
Where("? = ?", bun.Ident("status_mute.status_id"), status.ID).
|
|
||||||
Where("? = ?", bun.Ident("status_mute.account_id"), accountID)
|
|
||||||
|
|
||||||
return s.db.Exists(ctx, q)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
func (s *statusDB) IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error) {
|
||||||
q := s.db.
|
q := s.db.
|
||||||
NewSelect().
|
NewSelect().
|
||||||
|
|
117
internal/db/bundb/thread.go
Normal file
117
internal/db/bundb/thread.go
Normal file
|
@ -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/>.
|
||||||
|
|
||||||
|
package bundb
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type threadDB struct {
|
||||||
|
db *DB
|
||||||
|
state *state.State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) PutThread(ctx context.Context, thread *gtsmodel.Thread) error {
|
||||||
|
_, err := t.db.
|
||||||
|
NewInsert().
|
||||||
|
Model(thread).
|
||||||
|
Exec(ctx)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) GetThreadMute(ctx context.Context, id string) (*gtsmodel.ThreadMute, error) {
|
||||||
|
return t.state.Caches.GTS.ThreadMute().Load("ID", func() (*gtsmodel.ThreadMute, error) {
|
||||||
|
var threadMute gtsmodel.ThreadMute
|
||||||
|
|
||||||
|
q := t.db.
|
||||||
|
NewSelect().
|
||||||
|
Model(&threadMute).
|
||||||
|
Where("? = ?", bun.Ident("thread_mute.id"), id)
|
||||||
|
|
||||||
|
if err := q.Scan(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &threadMute, nil
|
||||||
|
}, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) GetThreadMutedByAccount(
|
||||||
|
ctx context.Context,
|
||||||
|
threadID string,
|
||||||
|
accountID string,
|
||||||
|
) (*gtsmodel.ThreadMute, error) {
|
||||||
|
return t.state.Caches.GTS.ThreadMute().Load("ThreadID.AccountID", func() (*gtsmodel.ThreadMute, error) {
|
||||||
|
var threadMute gtsmodel.ThreadMute
|
||||||
|
|
||||||
|
q := t.db.
|
||||||
|
NewSelect().
|
||||||
|
Model(&threadMute).
|
||||||
|
Where("? = ?", bun.Ident("thread_mute.thread_id"), threadID).
|
||||||
|
Where("? = ?", bun.Ident("thread_mute.account_id"), accountID)
|
||||||
|
|
||||||
|
if err := q.Scan(ctx); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &threadMute, nil
|
||||||
|
}, threadID, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) IsThreadMutedByAccount(
|
||||||
|
ctx context.Context,
|
||||||
|
threadID string,
|
||||||
|
accountID string,
|
||||||
|
) (bool, error) {
|
||||||
|
if threadID == "" {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
mute, err := t.GetThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return (mute != nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) PutThreadMute(ctx context.Context, threadMute *gtsmodel.ThreadMute) error {
|
||||||
|
return t.state.Caches.GTS.ThreadMute().Store(threadMute, func() error {
|
||||||
|
_, err := t.db.NewInsert().Model(threadMute).Exec(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *threadDB) DeleteThreadMute(ctx context.Context, id string) error {
|
||||||
|
if _, err := t.db.
|
||||||
|
NewDelete().
|
||||||
|
TableExpr("? AS ?", bun.Ident("thread_mutes"), bun.Ident("thread_mute")).
|
||||||
|
Where("? = ?", bun.Ident("thread_mute.id"), id).Exec(ctx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
t.state.Caches.GTS.ThreadMute().Invalidate("ID", id)
|
||||||
|
return nil
|
||||||
|
}
|
91
internal/db/bundb/thread_test.go
Normal file
91
internal/db/bundb/thread_test.go
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
// 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"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ThreadTestSuite struct {
|
||||||
|
BunDBStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ThreadTestSuite) TestPutThread() {
|
||||||
|
suite.NoError(
|
||||||
|
suite.db.PutThread(
|
||||||
|
context.Background(),
|
||||||
|
>smodel.Thread{
|
||||||
|
ID: "01HCWK4HVQ4VGSS1G4VQP3AXZF",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ThreadTestSuite) TestMuteUnmuteThread() {
|
||||||
|
var (
|
||||||
|
threadID = suite.testThreads["local_account_1_status_1"].ID
|
||||||
|
accountID = suite.testAccounts["local_account_1"].ID
|
||||||
|
ctx = context.Background()
|
||||||
|
threadMute = >smodel.ThreadMute{
|
||||||
|
ID: "01HD3K14B62YJHH4RR0DSZ1EQ2",
|
||||||
|
ThreadID: threadID,
|
||||||
|
AccountID: accountID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mute the thread and ensure it's actually muted.
|
||||||
|
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
muted, err := suite.db.IsThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !muted {
|
||||||
|
suite.FailNow("", "expected thread %s to be muted by account %s", threadID, accountID)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = suite.db.GetThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unmute the thread and ensure it's actually unmuted.
|
||||||
|
if err := suite.db.DeleteThreadMute(ctx, threadMute.ID); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
muted, err = suite.db.IsThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if muted {
|
||||||
|
suite.FailNow("", "expected thread %s to not be muted by account %s", threadID, accountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestThreadTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(ThreadTestSuite))
|
||||||
|
}
|
|
@ -45,6 +45,7 @@ type DB interface {
|
||||||
StatusBookmark
|
StatusBookmark
|
||||||
StatusFave
|
StatusFave
|
||||||
Tag
|
Tag
|
||||||
|
Thread
|
||||||
Timeline
|
Timeline
|
||||||
User
|
User
|
||||||
Tombstone
|
Tombstone
|
||||||
|
|
|
@ -80,9 +80,6 @@ type Status interface {
|
||||||
// If onlyDirect is true, only the immediate children will be returned.
|
// If onlyDirect is true, only the immediate children will be returned.
|
||||||
GetStatusChildren(ctx context.Context, status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error)
|
GetStatusChildren(ctx context.Context, status *gtsmodel.Status, onlyDirect bool, minID string) ([]*gtsmodel.Status, error)
|
||||||
|
|
||||||
// IsStatusMutedBy checks if a given status has been muted by a given account ID
|
|
||||||
IsStatusMutedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
|
||||||
|
|
||||||
// IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
// IsStatusBookmarkedBy checks if a given status has been bookmarked by a given account ID
|
||||||
IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
IsStatusBookmarkedBy(ctx context.Context, status *gtsmodel.Status, accountID string) (bool, error)
|
||||||
}
|
}
|
||||||
|
|
48
internal/db/thread.go
Normal file
48
internal/db/thread.go
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
// 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 db
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Thread contains functions for getting/creating
|
||||||
|
// status threads and thread mutes in the database.
|
||||||
|
type Thread interface {
|
||||||
|
// PutThread inserts a new thread.
|
||||||
|
PutThread(ctx context.Context, thread *gtsmodel.Thread) error
|
||||||
|
|
||||||
|
// GetThreadMute gets a single threadMute by its ID.
|
||||||
|
GetThreadMute(ctx context.Context, id string) (*gtsmodel.ThreadMute, error)
|
||||||
|
|
||||||
|
// GetThreadMutedByAccount gets a threadMute targeting the
|
||||||
|
// given thread, created by the given accountID, if it exists.
|
||||||
|
GetThreadMutedByAccount(ctx context.Context, threadID string, accountID string) (*gtsmodel.ThreadMute, error)
|
||||||
|
|
||||||
|
// IsThreadMutedByAccount returns true if threadID is muted
|
||||||
|
// by given account. Empty thread ID will return false early.
|
||||||
|
IsThreadMutedByAccount(ctx context.Context, threadID string, accountID string) (bool, error)
|
||||||
|
|
||||||
|
// PutThreadMute inserts a new threadMute.
|
||||||
|
PutThreadMute(ctx context.Context, threadMute *gtsmodel.ThreadMute) error
|
||||||
|
|
||||||
|
// DeleteThreadMute deletes threadMute with the given ID.
|
||||||
|
DeleteThreadMute(ctx context.Context, id string) error
|
||||||
|
}
|
|
@ -24,6 +24,8 @@
|
||||||
"net/url"
|
"net/url"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
@ -293,6 +295,12 @@ func (d *Dereferencer) enrichStatus(
|
||||||
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
|
return nil, nil, gtserror.Newf("error populating mentions for status %s: %w", uri, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now that we know who this status replies to (handled by ASStatusToStatus)
|
||||||
|
// and who it mentions, we can add a ThreadID to it if necessary.
|
||||||
|
if err := d.threadStatus(ctx, latestStatus); err != nil {
|
||||||
|
return nil, nil, gtserror.Newf("error checking / creating threadID for status %s: %w", uri, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Ensure the status' tags are populated, (changes are expected / okay).
|
// Ensure the status' tags are populated, (changes are expected / okay).
|
||||||
if err := d.fetchStatusTags(ctx, latestStatus); err != nil {
|
if err := d.fetchStatusTags(ctx, latestStatus); err != nil {
|
||||||
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
return nil, nil, gtserror.Newf("error populating tags for status %s: %w", uri, err)
|
||||||
|
@ -410,6 +418,57 @@ func (d *Dereferencer) fetchStatusMentions(ctx context.Context, requestUser stri
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dereferencer) threadStatus(ctx context.Context, status *gtsmodel.Status) error {
|
||||||
|
if status.InReplyTo != nil {
|
||||||
|
if parentThreadID := status.InReplyTo.ThreadID; parentThreadID != "" {
|
||||||
|
// Simplest case: parent status
|
||||||
|
// is threaded, so inherit threadID.
|
||||||
|
status.ThreadID = parentThreadID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent wasn't threaded. If this
|
||||||
|
// status mentions a local account,
|
||||||
|
// we should thread it so that local
|
||||||
|
// account can mute it if they want.
|
||||||
|
mentionsLocal := slices.ContainsFunc(
|
||||||
|
status.Mentions,
|
||||||
|
func(m *gtsmodel.Mention) bool {
|
||||||
|
// If TargetAccount couldn't
|
||||||
|
// be deref'd, we know it's not
|
||||||
|
// a local account, so only
|
||||||
|
// check for non-nil accounts.
|
||||||
|
return m.TargetAccount != nil &&
|
||||||
|
m.TargetAccount.IsLocal()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !mentionsLocal {
|
||||||
|
// Status doesn't mention a
|
||||||
|
// local account, so we don't
|
||||||
|
// need to thread it.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status mentions a local account.
|
||||||
|
// Create a new thread and assign
|
||||||
|
// it to the status.
|
||||||
|
threadID := id.NewULID()
|
||||||
|
|
||||||
|
if err := d.state.DB.PutThread(
|
||||||
|
ctx,
|
||||||
|
>smodel.Thread{
|
||||||
|
ID: threadID,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
return gtserror.Newf("error inserting new thread in db: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status.ThreadID = threadID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dereferencer) fetchStatusTags(ctx context.Context, status *gtsmodel.Status) error {
|
func (d *Dereferencer) fetchStatusTags(ctx context.Context, status *gtsmodel.Status) error {
|
||||||
// Allocate new slice to take the yet-to-be determined tag IDs.
|
// Allocate new slice to take the yet-to-be determined tag IDs.
|
||||||
status.TagIDs = make([]string, len(status.Tags))
|
status.TagIDs = make([]string, len(status.Tags))
|
||||||
|
|
|
@ -20,6 +20,8 @@
|
||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,6 +56,7 @@ type Status struct {
|
||||||
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
BoostOfAccountID string `bun:"type:CHAR(26),nullzero"` // id of the account that owns the boosted status
|
||||||
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
BoostOf *Status `bun:"-"` // status that corresponds to boostOfID
|
||||||
BoostOfAccount *Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
BoostOfAccount *Account `bun:"rel:belongs-to"` // account that corresponds to boostOfAccountID
|
||||||
|
ThreadID string `bun:"type:CHAR(26),nullzero"` // id of the thread to which this status belongs; only set for remote statuses if a local account is involved at some point in the thread, otherwise null
|
||||||
ContentWarning string `bun:",nullzero"` // cw string for this status
|
ContentWarning string `bun:",nullzero"` // cw string for this status
|
||||||
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
Visibility Visibility `bun:",nullzero,notnull"` // visibility entry for this status
|
||||||
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
Sensitive *bool `bun:",nullzero,notnull,default:false"` // mark the status as sensitive?
|
||||||
|
@ -241,13 +244,15 @@ func (s *Status) EmojisUpToDate(other *Status) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// MentionsAccount returns whether status mentions the given account ID.
|
// MentionsAccount returns whether status mentions the given account ID.
|
||||||
func (s *Status) MentionsAccount(id string) bool {
|
func (s *Status) MentionsAccount(accountID string) bool {
|
||||||
for _, mention := range s.Mentions {
|
return slices.ContainsFunc(s.Mentions, func(m *Mention) bool {
|
||||||
if mention.TargetAccountID == id {
|
return m.TargetAccountID == accountID
|
||||||
return true
|
})
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false
|
// BelongsToAccount returns whether status belongs to the given account ID.
|
||||||
|
func (s *Status) BelongsToAccount(accountID string) bool {
|
||||||
|
return s.AccountID == accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags.
|
// StatusToTag is an intermediate struct to facilitate the many2many relationship between a status and one or more tags.
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// StatusMute refers to one account having muted the status of another account or its own.
|
// StatusMute IS DEPRECATED -- USE THREADMUTE INSTEAD NOW! THIS TABLE DOESN'T EXIST ANYMORE!
|
||||||
type StatusMute struct {
|
type StatusMute struct {
|
||||||
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
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
|
CreatedAt time.Time `bun:"type:timestamptz,nullzero,notnull,default:current_timestamp"` // when was item created
|
||||||
|
|
32
internal/gtsmodel/thread.go
Normal file
32
internal/gtsmodel/thread.go
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// Thread represents one thread of statuses.
|
||||||
|
// TODO: add more fields here if necessary.
|
||||||
|
type Thread struct {
|
||||||
|
ID string `bun:"type:CHAR(26),pk,nullzero,notnull,unique"` // id of this item in the database
|
||||||
|
StatusIDs []string `bun:"-"` // ids of statuses belonging to this thread (order not guaranteed)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ThreadToStatus is an intermediate struct to facilitate the
|
||||||
|
// many2many relationship between a thread and one or more statuses.
|
||||||
|
type ThreadToStatus struct {
|
||||||
|
ThreadID string `bun:"type:CHAR(26),unique:statusthread,nullzero,notnull"`
|
||||||
|
StatusID string `bun:"type:CHAR(26),unique:statusthread,nullzero,notnull"`
|
||||||
|
}
|
29
internal/gtsmodel/threadmute.go
Normal file
29
internal/gtsmodel/threadmute.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// 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"
|
||||||
|
|
||||||
|
// ThreadMute represents an account-level mute of a thread of statuses.
|
||||||
|
type ThreadMute 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
|
||||||
|
ThreadID string `bun:"type:CHAR(26),nullzero,notnull,unique:thread_mute_thread_id_account_id"` // ID of the muted thread
|
||||||
|
AccountID string `bun:"type:CHAR(26),nullzero,notnull,unique:thread_mute_thread_id_account_id"` // Account ID of the creator of this mute
|
||||||
|
}
|
|
@ -164,7 +164,7 @@ func NewProcessor(
|
||||||
processor.report = report.New(state, converter)
|
processor.report = report.New(state, converter)
|
||||||
processor.timeline = timeline.New(state, converter, filter)
|
processor.timeline = timeline.New(state, converter, filter)
|
||||||
processor.search = search.New(state, federator, converter, filter)
|
processor.search = search.New(state, federator, converter, filter)
|
||||||
processor.status = status.New(state, federator, converter, filter, parseMentionFunc)
|
processor.status = status.New(&commonProcessor, state, federator, converter, filter, parseMentionFunc)
|
||||||
processor.stream = streamProcessor
|
processor.stream = streamProcessor
|
||||||
processor.user = user.New(state, emailSender)
|
processor.user = user.New(state, emailSender)
|
||||||
|
|
||||||
|
|
|
@ -29,16 +29,31 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (p *Processor) getBookmarkableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, "", errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
|
||||||
|
return nil, "", gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, bookmarkID, nil
|
||||||
|
}
|
||||||
|
|
||||||
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
|
// BookmarkCreate adds a bookmark for the requestingAccount, targeting the given status (no-op if bookmark already exists).
|
||||||
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
|
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingBookmarkID != "" {
|
if existingBookmarkID != "" {
|
||||||
// Status is already bookmarked.
|
// Status is already bookmarked.
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and store a new bookmark.
|
// Create and store a new bookmark.
|
||||||
|
@ -57,24 +72,24 @@ func (p *Processor) BookmarkCreate(ctx context.Context, requestingAccount *gtsmo
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
||||||
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
|
// BookmarkRemove removes a bookmark for the requesting account, targeting the given status (no-op if bookmark doesn't exist).
|
||||||
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkTarget(ctx, requestingAccount, targetStatusID)
|
targetStatus, existingBookmarkID, errWithCode := p.getBookmarkableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingBookmarkID == "" {
|
if existingBookmarkID == "" {
|
||||||
// Status isn't bookmarked.
|
// Status isn't bookmarked.
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a bookmark to remove.
|
// We have a bookmark to remove.
|
||||||
|
@ -83,25 +98,10 @@ func (p *Processor) BookmarkRemove(ctx context.Context, requestingAccount *gtsmo
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
||||||
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) getBookmarkTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, string, gtserror.WithCode) {
|
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
|
||||||
if errWithCode != nil {
|
|
||||||
return nil, "", errWithCode
|
|
||||||
}
|
|
||||||
|
|
||||||
bookmarkID, err := p.state.DB.GetStatusBookmarkID(ctx, requestingAccount.ID, targetStatus.ID)
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
err = fmt.Errorf("getBookmarkTarget: error checking existing bookmark: %w", err)
|
|
||||||
return nil, "", gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetStatus, bookmarkID, nil
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -85,7 +85,7 @@ func (p *Processor) BoostCreate(ctx context.Context, requestingAccount *gtsmodel
|
||||||
TargetAccount: targetStatus.Account,
|
TargetAccount: targetStatus.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
return p.apiStatus(ctx, boostWrapperStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, boostWrapperStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
|
// BoostRemove processes the unboost/unreblog of a given status, returning the status if all is well.
|
||||||
|
@ -129,7 +129,7 @@ func (p *Processor) BoostRemove(ctx context.Context, requestingAccount *gtsmodel
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
// StatusBoostedBy returns a slice of accounts that have boosted the given status, filtered according to privacy settings.
|
||||||
|
|
|
@ -1,103 +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 status
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
|
||||||
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/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *Processor) apiStatus(ctx context.Context, targetStatus *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, gtserror.WithCode) {
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, targetStatus, requestingAccount)
|
|
||||||
if err != nil {
|
|
||||||
err = gtserror.Newf("error converting status %s to frontend representation: %w", targetStatus.ID, err)
|
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) getVisibleStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
|
|
||||||
targetStatus, err := p.state.DB.GetStatusByID(ctx, targetStatusID)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("getVisibleStatus: db error fetching status %s: %w", targetStatusID, err)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if requestingAccount != nil {
|
|
||||||
// Ensure the status is up-to-date.
|
|
||||||
p.federator.RefreshStatusAsync(ctx,
|
|
||||||
requestingAccount.Username,
|
|
||||||
targetStatus,
|
|
||||||
nil,
|
|
||||||
false,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, requestingAccount, targetStatus)
|
|
||||||
if err != nil {
|
|
||||||
err = fmt.Errorf("getVisibleStatus: error seeing if status %s is visible: %w", targetStatus.ID, err)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
err = fmt.Errorf("getVisibleStatus: status %s is not visible to requesting account", targetStatusID)
|
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetStatus, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// invalidateStatus is a shortcut function for invalidating the prepared/cached
|
|
||||||
// representation one status in the home timeline and all list timelines of the
|
|
||||||
// given accountID. It should only be called in cases where a status update
|
|
||||||
// does *not* need to be passed into the processor via the worker queue, since
|
|
||||||
// such invalidation will, in that case, be handled by the processor instead.
|
|
||||||
func (p *Processor) invalidateStatus(ctx context.Context, accountID string, statusID string) error {
|
|
||||||
// Get lists first + bail if this fails.
|
|
||||||
lists, err := p.state.DB.GetListsForAccountID(ctx, accountID)
|
|
||||||
if err != nil {
|
|
||||||
return gtserror.Newf("db error getting lists for account %s: %w", accountID, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
l := log.WithContext(ctx).WithFields(kv.Fields{
|
|
||||||
{"accountID", accountID},
|
|
||||||
{"statusID", statusID},
|
|
||||||
}...)
|
|
||||||
|
|
||||||
// Unprepare item from home + list timelines, just log
|
|
||||||
// if something goes wrong since this is not a showstopper.
|
|
||||||
|
|
||||||
if err := p.state.Timelines.Home.UnprepareItem(ctx, accountID, statusID); err != nil {
|
|
||||||
l.Errorf("error unpreparing item from home timeline: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, list := range lists {
|
|
||||||
if err := p.state.Timelines.List.UnprepareItem(ctx, list.ID, statusID); err != nil {
|
|
||||||
l.Errorf("error unpreparing item from list timeline %s: %v", list.ID, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -70,6 +70,10 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if errWithCode := p.processThreadID(ctx, status); errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil {
|
if errWithCode := p.processMediaIDs(ctx, form, requestingAccount.ID, status); errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -99,7 +103,7 @@ func (p *Processor) Create(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
OriginAccount: requestingAccount,
|
OriginAccount: requestingAccount,
|
||||||
})
|
})
|
||||||
|
|
||||||
return p.apiStatus(ctx, status, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
||||||
|
@ -141,12 +145,43 @@ func (p *Processor) processReplyToID(ctx context.Context, form *apimodel.Advance
|
||||||
|
|
||||||
// Set status fields from inReplyTo.
|
// Set status fields from inReplyTo.
|
||||||
status.InReplyToID = inReplyTo.ID
|
status.InReplyToID = inReplyTo.ID
|
||||||
|
status.InReplyTo = inReplyTo
|
||||||
status.InReplyToURI = inReplyTo.URI
|
status.InReplyToURI = inReplyTo.URI
|
||||||
status.InReplyToAccountID = inReplyTo.AccountID
|
status.InReplyToAccountID = inReplyTo.AccountID
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Processor) processThreadID(ctx context.Context, status *gtsmodel.Status) gtserror.WithCode {
|
||||||
|
// Status takes the thread ID
|
||||||
|
// of whatever it replies to.
|
||||||
|
if status.InReplyTo != nil {
|
||||||
|
status.ThreadID = status.InReplyTo.ThreadID
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status doesn't reply to anything,
|
||||||
|
// so it's a new local top-level status
|
||||||
|
// and therefore needs a thread ID.
|
||||||
|
threadID := id.NewULID()
|
||||||
|
|
||||||
|
if err := p.state.DB.PutThread(
|
||||||
|
ctx,
|
||||||
|
>smodel.Thread{
|
||||||
|
ID: threadID,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
err := gtserror.Newf("error inserting new thread in db: %w", err)
|
||||||
|
return gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Future replies to this status
|
||||||
|
// (if any) will inherit this thread ID.
|
||||||
|
status.ThreadID = threadID
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
func (p *Processor) processMediaIDs(ctx context.Context, form *apimodel.AdvancedStatusCreateForm, thisAccountID string, status *gtsmodel.Status) gtserror.WithCode {
|
||||||
if form.MediaIDs == nil {
|
if form.MediaIDs == nil {
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -45,7 +45,7 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse the status to API model BEFORE deleting it.
|
// Parse the status to API model BEFORE deleting it.
|
||||||
apiStatus, errWithCode := p.apiStatus(ctx, targetStatus, requestingAccount)
|
apiStatus, errWithCode := p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,16 +33,36 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (p *Processor) getFaveableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !*targetStatus.Likeable {
|
||||||
|
err := errors.New("status is not faveable")
|
||||||
|
return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
|
||||||
|
return nil, nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, fave, nil
|
||||||
|
}
|
||||||
|
|
||||||
// FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists).
|
// FaveCreate adds a fave for the requestingAccount, targeting the given status (no-op if fave already exists).
|
||||||
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
|
targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingFave != nil {
|
if existingFave != nil {
|
||||||
// Status is already faveed.
|
// Status is already faveed.
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create and store a new fave
|
// Create and store a new fave
|
||||||
|
@ -72,19 +92,19 @@ func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.
|
||||||
TargetAccount: targetStatus.Account,
|
TargetAccount: targetStatus.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
|
// FaveRemove removes a fave for the requesting account, targeting the given status (no-op if fave doesn't exist).
|
||||||
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, existingFave, errWithCode := p.getFaveTarget(ctx, requestingAccount, targetStatusID)
|
targetStatus, existingFave, errWithCode := p.getFaveableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingFave == nil {
|
if existingFave == nil {
|
||||||
// Status isn't faveed.
|
// Status isn't faveed.
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a fave to remove.
|
// We have a fave to remove.
|
||||||
|
@ -102,12 +122,12 @@ func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.
|
||||||
TargetAccount: targetStatus.Account,
|
TargetAccount: targetStatus.Account,
|
||||||
})
|
})
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
// FavedBy returns a slice of accounts that have liked the given status, filtered according to privacy settings.
|
||||||
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) ([]*apimodel.Account, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -145,23 +165,3 @@ func (p *Processor) FavedBy(ctx context.Context, requestingAccount *gtsmodel.Acc
|
||||||
|
|
||||||
return apiAccounts, nil
|
return apiAccounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Processor) getFaveTarget(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, *gtsmodel.StatusFave, gtserror.WithCode) {
|
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
|
||||||
if errWithCode != nil {
|
|
||||||
return nil, nil, errWithCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if !*targetStatus.Likeable {
|
|
||||||
err := errors.New("status is not faveable")
|
|
||||||
return nil, nil, gtserror.NewErrorForbidden(err, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
fave, err := p.state.DB.GetStatusFave(ctx, requestingAccount.ID, targetStatusID)
|
|
||||||
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
|
||||||
err = fmt.Errorf("getFaveTarget: error checking existing fave: %w", err)
|
|
||||||
return nil, nil, gtserror.NewErrorInternalError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetStatus, fave, nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -28,17 +28,17 @@
|
||||||
|
|
||||||
// Get gets the given status, taking account of privacy settings and blocks etc.
|
// Get gets the given status, taking account of privacy settings and blocks etc.
|
||||||
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
func (p *Processor) Get(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ContextGet returns the context (previous and following posts) from the given status ID.
|
// ContextGet returns the context (previous and following posts) from the given status ID.
|
||||||
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
|
146
internal/processing/status/mute.go
Normal file
146
internal/processing/status/mute.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
// 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 status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getMuteableStatus fetches targetStatusID status and
|
||||||
|
// ensures that requestingAccount can mute or unmute it.
|
||||||
|
//
|
||||||
|
// It checks:
|
||||||
|
// - Status exists and is visible to requester.
|
||||||
|
// - Status belongs to or mentions requesting account.
|
||||||
|
// - Status is not a boost.
|
||||||
|
// - Status has a thread ID.
|
||||||
|
func (p *Processor) getMuteableStatus(
|
||||||
|
ctx context.Context,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
targetStatusID string,
|
||||||
|
) (*gtsmodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
if !targetStatus.BelongsToAccount(requestingAccount.ID) &&
|
||||||
|
!targetStatus.MentionsAccount(requestingAccount.ID) {
|
||||||
|
err := gtserror.Newf("status %s does not belong to or mention account %s", targetStatusID, requestingAccount.ID)
|
||||||
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetStatus.BoostOfID != "" {
|
||||||
|
err := gtserror.New("cannot mute or unmute boosts")
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetStatus.ThreadID == "" {
|
||||||
|
err := gtserror.New("cannot mute or unmute status with no threadID")
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return targetStatus, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) MuteCreate(
|
||||||
|
ctx context.Context,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
targetStatusID string,
|
||||||
|
) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.getMuteableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
threadID = targetStatus.ThreadID
|
||||||
|
accountID = requestingAccount.ID
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if mute already exists for this thread ID.
|
||||||
|
threadMute, err := p.state.DB.GetThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Real db error.
|
||||||
|
err := gtserror.Newf("db error fetching mute of thread %s for account %s", threadID, accountID)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadMute != nil {
|
||||||
|
// Thread mute already exists.
|
||||||
|
// Our job here is done ("but you didn't do anything!").
|
||||||
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gotta create a mute.
|
||||||
|
if err := p.state.DB.PutThreadMute(ctx, >smodel.ThreadMute{
|
||||||
|
ID: id.NewULID(),
|
||||||
|
ThreadID: threadID,
|
||||||
|
AccountID: accountID,
|
||||||
|
}); err != nil {
|
||||||
|
err := gtserror.Newf("db error putting mute of thread %s for account %s", threadID, accountID)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Processor) MuteRemove(
|
||||||
|
ctx context.Context,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
targetStatusID string,
|
||||||
|
) (*apimodel.Status, gtserror.WithCode) {
|
||||||
|
targetStatus, errWithCode := p.getMuteableStatus(ctx, requestingAccount, targetStatusID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
return nil, errWithCode
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
threadID = targetStatus.ThreadID
|
||||||
|
accountID = requestingAccount.ID
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check if mute exists for this thread ID.
|
||||||
|
threadMute, err := p.state.DB.GetThreadMutedByAccount(ctx, threadID, accountID)
|
||||||
|
if err != nil && !errors.Is(err, db.ErrNoEntries) {
|
||||||
|
// Real db error.
|
||||||
|
err := gtserror.Newf("db error fetching mute of thread %s for account %s", threadID, accountID)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if threadMute == nil {
|
||||||
|
// Thread mute doesn't exist.
|
||||||
|
// Our job here is done ("but you didn't do anything!").
|
||||||
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gotta remove the mute.
|
||||||
|
if err := p.state.DB.DeleteThreadMute(ctx, threadMute.ID); err != nil {
|
||||||
|
err := gtserror.Newf("db error deleting mute of thread %s for account %s", threadID, accountID)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
|
}
|
|
@ -39,7 +39,7 @@
|
||||||
// - Status is public, unlisted, or followers-only.
|
// - Status is public, unlisted, or followers-only.
|
||||||
// - Status is not a boost.
|
// - Status is not a boost.
|
||||||
func (p *Processor) getPinnableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
|
func (p *Processor) getPinnableStatus(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*gtsmodel.Status, gtserror.WithCode) {
|
||||||
targetStatus, errWithCode := p.getVisibleStatus(ctx, requestingAccount, targetStatusID)
|
targetStatus, errWithCode := p.c.GetVisibleTargetStatus(ctx, requestingAccount, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
return nil, errWithCode
|
return nil, errWithCode
|
||||||
}
|
}
|
||||||
|
@ -99,12 +99,12 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
||||||
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
|
// PinRemove unpins the target status from the top of requestingAccount's profile, if possible.
|
||||||
|
@ -125,7 +125,7 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
}
|
}
|
||||||
|
|
||||||
if targetStatus.PinnedAt.IsZero() {
|
if targetStatus.PinnedAt.IsZero() {
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatus.PinnedAt = time.Time{}
|
targetStatus.PinnedAt = time.Time{}
|
||||||
|
@ -134,10 +134,10 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.invalidateStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
if err := p.c.InvalidateTimelinedStatus(ctx, requestingAccount.ID, targetStatusID); err != nil {
|
||||||
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
err = gtserror.Newf("error invalidating status from timelines: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.apiStatus(ctx, targetStatus, requestingAccount)
|
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
import (
|
import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/text"
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
|
@ -27,6 +28,9 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
type Processor struct {
|
type Processor struct {
|
||||||
|
// common processor logic
|
||||||
|
c *common.Processor
|
||||||
|
|
||||||
state *state.State
|
state *state.State
|
||||||
federator *federation.Federator
|
federator *federation.Federator
|
||||||
converter *typeutils.Converter
|
converter *typeutils.Converter
|
||||||
|
@ -36,8 +40,16 @@ type Processor struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new status processor.
|
// New returns a new status processor.
|
||||||
func New(state *state.State, federator *federation.Federator, converter *typeutils.Converter, filter *visibility.Filter, parseMention gtsmodel.ParseMentionFunc) Processor {
|
func New(
|
||||||
|
common *common.Processor,
|
||||||
|
state *state.State,
|
||||||
|
federator *federation.Federator,
|
||||||
|
converter *typeutils.Converter,
|
||||||
|
filter *visibility.Filter,
|
||||||
|
parseMention gtsmodel.ParseMentionFunc,
|
||||||
|
) Processor {
|
||||||
return Processor{
|
return Processor{
|
||||||
|
c: common,
|
||||||
state: state,
|
state: state,
|
||||||
federator: federator,
|
federator: federator,
|
||||||
converter: converter,
|
converter: converter,
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
"github.com/superseriousbusiness/gotosocial/internal/processing/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||||
|
@ -94,7 +95,9 @@ func (suite *StatusStandardTestSuite) SetupTest() {
|
||||||
suite.typeConverter,
|
suite.typeConverter,
|
||||||
)
|
)
|
||||||
|
|
||||||
suite.status = status.New(&suite.state, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(suite.db, suite.federator))
|
common := common.New(&suite.state, suite.typeConverter, suite.federator, filter)
|
||||||
|
|
||||||
|
suite.status = status.New(&common, &suite.state, suite.federator, suite.typeConverter, filter, processing.GetParseMentionFunc(suite.db, suite.federator))
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
|
||||||
|
|
|
@ -260,6 +260,11 @@ func (p *clientAPI) CreateLike(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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure fave populated.
|
||||||
|
if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||||
|
return gtserror.Newf("error populating status fave: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||||
return gtserror.Newf("error notifying fave: %w", err)
|
return gtserror.Newf("error notifying fave: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,6 +75,7 @@ func (suite *FromClientAPITestSuite) newStatus(
|
||||||
newStatus.InReplyToAccountID = replyToStatus.AccountID
|
newStatus.InReplyToAccountID = replyToStatus.AccountID
|
||||||
newStatus.InReplyToID = replyToStatus.ID
|
newStatus.InReplyToID = replyToStatus.ID
|
||||||
newStatus.InReplyToURI = replyToStatus.URI
|
newStatus.InReplyToURI = replyToStatus.URI
|
||||||
|
newStatus.ThreadID = replyToStatus.ThreadID
|
||||||
|
|
||||||
// Mention the replied-to account.
|
// Mention the replied-to account.
|
||||||
mention := >smodel.Mention{
|
mention := >smodel.Mention{
|
||||||
|
@ -324,6 +325,114 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
postingAccount = suite.testAccounts["admin_account"]
|
||||||
|
receivingAccount = suite.testAccounts["local_account_1"]
|
||||||
|
|
||||||
|
// Admin account posts a reply to zork.
|
||||||
|
// Normally zork would get a notification
|
||||||
|
// for this, but zork mutes this thread.
|
||||||
|
status = suite.newStatus(
|
||||||
|
ctx,
|
||||||
|
postingAccount,
|
||||||
|
gtsmodel.VisibilityPublic,
|
||||||
|
suite.testStatuses["local_account_1_status_1"],
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
threadMute = >smodel.ThreadMute{
|
||||||
|
ID: "01HD3KRMBB1M85QRWHD912QWRE",
|
||||||
|
ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
|
||||||
|
AccountID: receivingAccount.ID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store the thread mute before processing new status.
|
||||||
|
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the new status.
|
||||||
|
if err := suite.processor.Workers().ProcessFromClientAPI(
|
||||||
|
ctx,
|
||||||
|
messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ObjectNote,
|
||||||
|
APActivityType: ap.ActivityCreate,
|
||||||
|
GTSModel: status,
|
||||||
|
OriginAccount: postingAccount,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no notification received.
|
||||||
|
notif, err := suite.db.GetNotification(
|
||||||
|
ctx,
|
||||||
|
gtsmodel.NotificationMention,
|
||||||
|
receivingAccount.ID,
|
||||||
|
postingAccount.ID,
|
||||||
|
status.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.ErrorIs(err, db.ErrNoEntries)
|
||||||
|
suite.Nil(notif)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
|
||||||
|
var (
|
||||||
|
ctx = context.Background()
|
||||||
|
postingAccount = suite.testAccounts["admin_account"]
|
||||||
|
receivingAccount = suite.testAccounts["local_account_1"]
|
||||||
|
|
||||||
|
// Admin account boosts a status by zork.
|
||||||
|
// Normally zork would get a notification
|
||||||
|
// for this, but zork mutes this thread.
|
||||||
|
status = suite.newStatus(
|
||||||
|
ctx,
|
||||||
|
postingAccount,
|
||||||
|
gtsmodel.VisibilityPublic,
|
||||||
|
nil,
|
||||||
|
suite.testStatuses["local_account_1_status_1"],
|
||||||
|
)
|
||||||
|
threadMute = >smodel.ThreadMute{
|
||||||
|
ID: "01HD3KRMBB1M85QRWHD912QWRE",
|
||||||
|
ThreadID: suite.testStatuses["local_account_1_status_1"].ThreadID,
|
||||||
|
AccountID: receivingAccount.ID,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Store the thread mute before processing new status.
|
||||||
|
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the new status.
|
||||||
|
if err := suite.processor.Workers().ProcessFromClientAPI(
|
||||||
|
ctx,
|
||||||
|
messages.FromClientAPI{
|
||||||
|
APObjectType: ap.ActivityAnnounce,
|
||||||
|
APActivityType: ap.ActivityCreate,
|
||||||
|
GTSModel: status,
|
||||||
|
OriginAccount: postingAccount,
|
||||||
|
},
|
||||||
|
); err != nil {
|
||||||
|
suite.FailNow(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure no notification received.
|
||||||
|
notif, err := suite.db.GetNotification(
|
||||||
|
ctx,
|
||||||
|
gtsmodel.NotificationReblog,
|
||||||
|
receivingAccount.ID,
|
||||||
|
postingAccount.ID,
|
||||||
|
status.ID,
|
||||||
|
)
|
||||||
|
|
||||||
|
suite.ErrorIs(err, db.ErrNoEntries)
|
||||||
|
suite.Nil(notif)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
|
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
|
||||||
// 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)
|
||||||
|
|
|
@ -315,6 +315,11 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) err
|
||||||
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
|
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure fave populated.
|
||||||
|
if err := p.state.DB.PopulateStatusFave(ctx, fave); err != nil {
|
||||||
|
return gtserror.Newf("error populating status fave: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
if err := p.surface.notifyFave(ctx, fave); err != nil {
|
||||||
return gtserror.Newf("error notifying fave: %w", err)
|
return gtserror.Newf("error notifying fave: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,15 +28,39 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
)
|
)
|
||||||
|
|
||||||
// notifyMentions notifies each targeted account in
|
// notifyMentions iterates through mentions on the
|
||||||
// the given mentions that they have a new mention.
|
// given status, and notifies each mentioned account
|
||||||
|
// that they have a new mention.
|
||||||
func (s *surface) notifyMentions(
|
func (s *surface) notifyMentions(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mentions []*gtsmodel.Mention,
|
status *gtsmodel.Status,
|
||||||
) error {
|
) error {
|
||||||
errs := gtserror.NewMultiError(len(mentions))
|
var (
|
||||||
|
mentions = status.Mentions
|
||||||
|
errs = gtserror.NewMultiError(len(mentions))
|
||||||
|
)
|
||||||
|
|
||||||
for _, mention := range mentions {
|
for _, mention := range mentions {
|
||||||
|
// Ensure thread not muted
|
||||||
|
// by mentioned account.
|
||||||
|
muted, err := s.state.DB.IsThreadMutedByAccount(
|
||||||
|
ctx,
|
||||||
|
status.ThreadID,
|
||||||
|
mention.TargetAccountID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
errs.Append(err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if muted {
|
||||||
|
// This mentioned account
|
||||||
|
// has muted the thread.
|
||||||
|
// Don't pester them.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if err := s.notify(
|
if err := s.notify(
|
||||||
ctx,
|
ctx,
|
||||||
gtsmodel.NotificationMention,
|
gtsmodel.NotificationMention,
|
||||||
|
@ -114,6 +138,24 @@ func (s *surface) notifyFave(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure favee hasn't
|
||||||
|
// muted the thread.
|
||||||
|
muted, err := s.state.DB.IsThreadMutedByAccount(
|
||||||
|
ctx,
|
||||||
|
fave.Status.ThreadID,
|
||||||
|
fave.TargetAccountID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if muted {
|
||||||
|
// Boostee doesn't want
|
||||||
|
// notifs for this thread.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return s.notify(
|
return s.notify(
|
||||||
ctx,
|
ctx,
|
||||||
gtsmodel.NotificationFave,
|
gtsmodel.NotificationFave,
|
||||||
|
@ -134,11 +176,35 @@ func (s *surface) notifyAnnounce(
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if status.BoostOf == nil {
|
||||||
|
// No boosted status
|
||||||
|
// set, nothing to do.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if status.BoostOfAccountID == status.AccountID {
|
if status.BoostOfAccountID == status.AccountID {
|
||||||
// Self-boost, nothing to do.
|
// Self-boost, nothing to do.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure boostee hasn't
|
||||||
|
// muted the thread.
|
||||||
|
muted, err := s.state.DB.IsThreadMutedByAccount(
|
||||||
|
ctx,
|
||||||
|
status.BoostOf.ThreadID,
|
||||||
|
status.BoostOfAccountID,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if muted {
|
||||||
|
// Boostee doesn't want
|
||||||
|
// notifs for this thread.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
return s.notify(
|
return s.notify(
|
||||||
ctx,
|
ctx,
|
||||||
gtsmodel.NotificationReblog,
|
gtsmodel.NotificationReblog,
|
||||||
|
|
|
@ -67,7 +67,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify each local account that's mentioned by this status.
|
// Notify each local account that's mentioned by this status.
|
||||||
if err := s.notifyMentions(ctx, status.Mentions); err != nil {
|
if err := s.notifyMentions(ctx, status); err != nil {
|
||||||
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
|
return gtserror.Newf("error notifying status mentions for status %s: %w", status.ID, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -52,7 +52,7 @@ func (c *Converter) interactionsWithStatusForAccount(ctx context.Context, s *gts
|
||||||
}
|
}
|
||||||
si.Reblogged = reblogged
|
si.Reblogged = reblogged
|
||||||
|
|
||||||
muted, err := c.state.DB.IsStatusMutedBy(ctx, s, requestingAccount.ID)
|
muted, err := c.state.DB.IsThreadMutedByAccount(ctx, s.ThreadID, requestingAccount.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err)
|
return nil, fmt.Errorf("error checking if requesting account has muted status: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,6 +48,7 @@ EXPECT=$(cat << "EOF"
|
||||||
"status-fave-mem-ratio": 2,
|
"status-fave-mem-ratio": 2,
|
||||||
"status-mem-ratio": 5,
|
"status-mem-ratio": 5,
|
||||||
"tag-mem-ratio": 2,
|
"tag-mem-ratio": 2,
|
||||||
|
"thread-mute-mem-ratio": 0.2,
|
||||||
"tombstone-mem-ratio": 0.5,
|
"tombstone-mem-ratio": 0.5,
|
||||||
"user-mem-ratio": 0.25,
|
"user-mem-ratio": 0.25,
|
||||||
"visibility-mem-ratio": 2,
|
"visibility-mem-ratio": 2,
|
||||||
|
|
|
@ -49,8 +49,10 @@
|
||||||
>smodel.StatusToTag{},
|
>smodel.StatusToTag{},
|
||||||
>smodel.StatusFave{},
|
>smodel.StatusFave{},
|
||||||
>smodel.StatusBookmark{},
|
>smodel.StatusBookmark{},
|
||||||
>smodel.StatusMute{},
|
|
||||||
>smodel.Tag{},
|
>smodel.Tag{},
|
||||||
|
>smodel.Thread{},
|
||||||
|
>smodel.ThreadMute{},
|
||||||
|
>smodel.ThreadToStatus{},
|
||||||
>smodel.User{},
|
>smodel.User{},
|
||||||
>smodel.Emoji{},
|
>smodel.Emoji{},
|
||||||
>smodel.Instance{},
|
>smodel.Instance{},
|
||||||
|
@ -301,6 +303,18 @@ func StandardDBSetup(db db.DB, accounts map[string]*gtsmodel.Account) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, v := range NewTestThreads() {
|
||||||
|
if err := db.Put(ctx, v); err != nil {
|
||||||
|
log.Panic(nil, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range NewTestThreadToStatus() {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1318,6 +1318,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDF2Q4HV5QC161C4TGQ0M3",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1343,6 +1344,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
AccountID: "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDQ1C7APSEY34B1HFVHVX7",
|
||||||
ContentWarning: "open to see some puppies",
|
ContentWarning: "open to see some puppies",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1370,6 +1372,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToURI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
InReplyToURI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
Language: "en",
|
Language: "en",
|
||||||
|
@ -1396,6 +1399,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
InReplyToURI: "",
|
InReplyToURI: "",
|
||||||
BoostOfID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
BoostOfID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
BoostOfAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
BoostOfAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
|
ThreadID: "",
|
||||||
ContentWarning: "introduction post",
|
ContentWarning: "introduction post",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1420,6 +1424,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
ContentWarning: "introduction post",
|
ContentWarning: "introduction post",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1444,6 +1449,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDVTW3HQWSX66VJQ91Z1RH",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityUnlocked,
|
Visibility: gtsmodel.VisibilityUnlocked,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1468,6 +1474,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDY9PDNHDBDBBFTJKJY8XE",
|
||||||
ContentWarning: "test: you shouldn't be able to interact with this post in any way",
|
ContentWarning: "test: you shouldn't be able to interact with this post in any way",
|
||||||
Visibility: gtsmodel.VisibilityMutualsOnly,
|
Visibility: gtsmodel.VisibilityMutualsOnly,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1493,6 +1500,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE0H2GKH794Q7GDPANH91Q",
|
||||||
ContentWarning: "eye contact, trent reznor gif, cow",
|
ContentWarning: "eye contact, trent reznor gif, cow",
|
||||||
Visibility: gtsmodel.VisibilityMutualsOnly,
|
Visibility: gtsmodel.VisibilityMutualsOnly,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1518,6 +1526,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE1ERQSMMVWDD0BE491E2P",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityFollowersOnly,
|
Visibility: gtsmodel.VisibilityFollowersOnly,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1542,6 +1551,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE2Q24FWCZE41AS77SDFRZ",
|
||||||
ContentWarning: "introduction post",
|
ContentWarning: "introduction post",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1566,6 +1576,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE3P291Z3NJEJVFPW0K9ZQ",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1590,6 +1601,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||||
ContentWarning: "you won't be able to like or reply to this",
|
ContentWarning: "you won't be able to like or reply to this",
|
||||||
Visibility: gtsmodel.VisibilityUnlocked,
|
Visibility: gtsmodel.VisibilityUnlocked,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1614,6 +1626,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE5JXFPFP3P5W2QNHVVV27",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(true),
|
Sensitive: util.Ptr(true),
|
||||||
|
@ -1642,6 +1655,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
InReplyToAccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||||
InReplyToURI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
InReplyToURI: "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityPublic,
|
Visibility: gtsmodel.VisibilityPublic,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1669,6 +1683,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
InReplyToAccountID: "",
|
InReplyToAccountID: "",
|
||||||
InReplyToURI: "",
|
InReplyToURI: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE71MGRRDSHBKXFD5DDSWR",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityDirect,
|
Visibility: gtsmodel.VisibilityDirect,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1695,6 +1710,7 @@ func NewTestStatuses() map[string]*gtsmodel.Status {
|
||||||
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
AccountID: "01F8MH5NBDF2MV7CTC4Q5128HF",
|
||||||
InReplyToID: "",
|
InReplyToID: "",
|
||||||
BoostOfID: "",
|
BoostOfID: "",
|
||||||
|
ThreadID: "01HCWE7ZNC2SS4P05WA5QYED23",
|
||||||
ContentWarning: "",
|
ContentWarning: "",
|
||||||
Visibility: gtsmodel.VisibilityFollowersOnly,
|
Visibility: gtsmodel.VisibilityFollowersOnly,
|
||||||
Sensitive: util.Ptr(false),
|
Sensitive: util.Ptr(false),
|
||||||
|
@ -1767,6 +1783,111 @@ func NewTestStatusToTags() map[string]*gtsmodel.StatusToTag {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func NewTestThreads() map[string]*gtsmodel.Thread {
|
||||||
|
return map[string]*gtsmodel.Thread{
|
||||||
|
"admin_account_status_1": {
|
||||||
|
ID: "01HCWDF2Q4HV5QC161C4TGQ0M3",
|
||||||
|
},
|
||||||
|
"admin_account_status_2": {
|
||||||
|
ID: "01HCWDQ1C7APSEY34B1HFVHVX7",
|
||||||
|
},
|
||||||
|
"local_account_1_status_1": {
|
||||||
|
ID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
|
},
|
||||||
|
"local_account_1_status_2": {
|
||||||
|
ID: "01HCWDVTW3HQWSX66VJQ91Z1RH",
|
||||||
|
},
|
||||||
|
"local_account_1_status_3": {
|
||||||
|
ID: "01HCWDY9PDNHDBDBBFTJKJY8XE",
|
||||||
|
},
|
||||||
|
"local_account_1_status_4": {
|
||||||
|
ID: "01HCWE0H2GKH794Q7GDPANH91Q",
|
||||||
|
},
|
||||||
|
"local_account_1_status_5": {
|
||||||
|
ID: "01HCWE1ERQSMMVWDD0BE491E2P",
|
||||||
|
},
|
||||||
|
"local_account_2_status_1": {
|
||||||
|
ID: "01HCWE2Q24FWCZE41AS77SDFRZ",
|
||||||
|
},
|
||||||
|
"local_account_2_status_2": {
|
||||||
|
ID: "01HCWE3P291Z3NJEJVFPW0K9ZQ",
|
||||||
|
},
|
||||||
|
"local_account_2_status_3": {
|
||||||
|
ID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||||
|
},
|
||||||
|
"local_account_2_status_4": {
|
||||||
|
ID: "01HCWE5JXFPFP3P5W2QNHVVV27",
|
||||||
|
},
|
||||||
|
"local_account_2_status_6": {
|
||||||
|
ID: "01HCWE71MGRRDSHBKXFD5DDSWR",
|
||||||
|
},
|
||||||
|
"local_account_2_status_7": {
|
||||||
|
ID: "01HCWE7ZNC2SS4P05WA5QYED23",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTestThreadToStatus() []*gtsmodel.ThreadToStatus {
|
||||||
|
return []*gtsmodel.ThreadToStatus{
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDF2Q4HV5QC161C4TGQ0M3",
|
||||||
|
StatusID: "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDQ1C7APSEY34B1HFVHVX7",
|
||||||
|
StatusID: "01F8MHAAY43M6RJ473VQFCVH37",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
|
StatusID: "01FF25D5Q0DH7CHD57CTRS6WK0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
|
StatusID: "01F8MHAMCHF6Y650WCRSCP4WMY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDVTW3HQWSX66VJQ91Z1RH",
|
||||||
|
StatusID: "01F8MHAYFKS4KMXF8K5Y1C0KRN",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDY9PDNHDBDBBFTJKJY8XE",
|
||||||
|
StatusID: "01F8MHBBN8120SYH7D5S050MGK",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE0H2GKH794Q7GDPANH91Q",
|
||||||
|
StatusID: "01F8MH82FYRXD2RC6108DAJ5HB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE1ERQSMMVWDD0BE491E2P",
|
||||||
|
StatusID: "01FCTA44PW9H1TB328S9AQXKDS",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE2Q24FWCZE41AS77SDFRZ",
|
||||||
|
StatusID: "01F8MHBQCBTDKN6X5VHGMMN4MA",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE3P291Z3NJEJVFPW0K9ZQ",
|
||||||
|
StatusID: "01F8MHC0H0A7XHTVH5F596ZKBM",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE4P0EW9HBA5WHW97D5YV0",
|
||||||
|
StatusID: "01F8MHC8VWDRBQR0N1BATDDEM5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWDKKBWECZJQ93E262N36VN",
|
||||||
|
StatusID: "01FCQSQ667XHJ9AV9T27SJJSX5",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE71MGRRDSHBKXFD5DDSWR",
|
||||||
|
StatusID: "01FN3VJGFH10KR7S2PB0GFJZYG",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ThreadID: "01HCWE7ZNC2SS4P05WA5QYED23",
|
||||||
|
StatusID: "01G20ZM733MGN8J344T4ZDDFY1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// NewTestMentions returns a map of gts model mentions keyed by their name.
|
// NewTestMentions returns a map of gts model mentions keyed by their name.
|
||||||
func NewTestMentions() map[string]*gtsmodel.Mention {
|
func NewTestMentions() map[string]*gtsmodel.Mention {
|
||||||
return map[string]*gtsmodel.Mention{
|
return map[string]*gtsmodel.Mention{
|
||||||
|
|
Loading…
Reference in a new issue