Compare commits
5 Commits
9a66fa0ef6
...
505af76bae
Author | SHA1 | Date |
---|---|---|
tobi | 505af76bae | |
kim | f456bd3401 | |
kim | 3554991444 | |
Vyr Cossont | 45f4afe60e | |
dependabot[bot] | a0d066844f |
|
@ -1,5 +1,9 @@
|
||||||
basePath: /
|
basePath: /
|
||||||
definitions:
|
definitions:
|
||||||
|
FilterAction:
|
||||||
|
title: FilterAction is the action to apply to statuses matching a filter.
|
||||||
|
type: string
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
InstanceConfigurationEmojis:
|
InstanceConfigurationEmojis:
|
||||||
properties:
|
properties:
|
||||||
emoji_size_limit:
|
emoji_size_limit:
|
||||||
|
@ -1037,6 +1041,60 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
x-go-name: FilterContext
|
x-go-name: FilterContext
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
filterKeyword:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
description: The ID of the filter keyword entry in the database.
|
||||||
|
type: string
|
||||||
|
x-go-name: ID
|
||||||
|
keyword:
|
||||||
|
description: The text to be filtered.
|
||||||
|
example: fnord
|
||||||
|
type: string
|
||||||
|
x-go-name: Keyword
|
||||||
|
whole_word:
|
||||||
|
description: Should the filter consider word boundaries?
|
||||||
|
example: true
|
||||||
|
type: boolean
|
||||||
|
x-go-name: WholeWord
|
||||||
|
title: FilterKeyword represents text to filter within a v2 filter.
|
||||||
|
type: object
|
||||||
|
x-go-name: FilterKeyword
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
filterResult:
|
||||||
|
properties:
|
||||||
|
filter:
|
||||||
|
$ref: '#/definitions/filterV2'
|
||||||
|
keyword_matches:
|
||||||
|
description: The keywords within the filter that were matched.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-go-name: KeywordMatches
|
||||||
|
status_matches:
|
||||||
|
description: The status IDs within the filter that were matched.
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
x-go-name: StatusMatches
|
||||||
|
title: FilterResult is returned along with a filtered status to explain why it was filtered.
|
||||||
|
type: object
|
||||||
|
x-go-name: FilterResult
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
filterStatus:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
description: The ID of the filter status entry in the database.
|
||||||
|
type: string
|
||||||
|
x-go-name: ID
|
||||||
|
phrase:
|
||||||
|
description: The status ID to be filtered.
|
||||||
|
type: string
|
||||||
|
x-go-name: StatusID
|
||||||
|
title: FilterStatus represents a single status to filter within a v2 filter.
|
||||||
|
type: object
|
||||||
|
x-go-name: FilterStatus
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
filterV1:
|
filterV1:
|
||||||
description: |-
|
description: |-
|
||||||
Note that v1 filters are mapped to v2 filters and v2 filter keywords internally.
|
Note that v1 filters are mapped to v2 filters and v2 filter keywords internally.
|
||||||
|
@ -1086,6 +1144,52 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
x-go-name: FilterV1
|
x-go-name: FilterV1
|
||||||
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
|
filterV2:
|
||||||
|
description: v2 filters have names and can include multiple phrases and status IDs to filter.
|
||||||
|
properties:
|
||||||
|
context:
|
||||||
|
description: The contexts in which the filter should be applied.
|
||||||
|
example:
|
||||||
|
- home
|
||||||
|
- public
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/filterContext'
|
||||||
|
minItems: 1
|
||||||
|
type: array
|
||||||
|
uniqueItems: true
|
||||||
|
x-go-name: Context
|
||||||
|
expires_at:
|
||||||
|
description: When the filter should no longer be applied. Null if the filter does not expire.
|
||||||
|
example: "2024-02-01T02:57:49Z"
|
||||||
|
type: string
|
||||||
|
x-go-name: ExpiresAt
|
||||||
|
filter_action:
|
||||||
|
$ref: '#/definitions/FilterAction'
|
||||||
|
id:
|
||||||
|
description: The ID of the filter in the database.
|
||||||
|
type: string
|
||||||
|
x-go-name: ID
|
||||||
|
keywords:
|
||||||
|
description: The keywords grouped under this filter.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/filterKeyword'
|
||||||
|
type: array
|
||||||
|
x-go-name: Keywords
|
||||||
|
statuses:
|
||||||
|
description: The statuses grouped under this filter.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/filterStatus'
|
||||||
|
type: array
|
||||||
|
x-go-name: Statuses
|
||||||
|
title:
|
||||||
|
description: The name of the filter.
|
||||||
|
example: Linux Words
|
||||||
|
type: string
|
||||||
|
x-go-name: Title
|
||||||
|
title: FilterV2 represents a user-defined filter for determining which statuses should not be shown to the user.
|
||||||
|
type: object
|
||||||
|
x-go-name: FilterV2
|
||||||
|
x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model
|
||||||
headerFilter:
|
headerFilter:
|
||||||
properties:
|
properties:
|
||||||
created_at:
|
created_at:
|
||||||
|
@ -2118,6 +2222,12 @@ definitions:
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
x-go-name: FavouritesCount
|
x-go-name: FavouritesCount
|
||||||
|
filtered:
|
||||||
|
description: A list of filters that matched this status and why they matched, if there are any such filters.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/filterResult'
|
||||||
|
type: array
|
||||||
|
x-go-name: Filtered
|
||||||
id:
|
id:
|
||||||
description: ID of the status.
|
description: ID of the status.
|
||||||
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
|
@ -2321,6 +2431,12 @@ definitions:
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
x-go-name: FavouritesCount
|
x-go-name: FavouritesCount
|
||||||
|
filtered:
|
||||||
|
description: A list of filters that matched this status and why they matched, if there are any such filters.
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/filterResult'
|
||||||
|
type: array
|
||||||
|
x-go-name: Filtered
|
||||||
id:
|
id:
|
||||||
description: ID of the status.
|
description: ID of the status.
|
||||||
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
example: 01FBVD42CQ3ZEEVMW180SBX03B
|
||||||
|
|
4
go.mod
4
go.mod
|
@ -22,7 +22,7 @@ require (
|
||||||
codeberg.org/gruf/go-runners v1.6.2
|
codeberg.org/gruf/go-runners v1.6.2
|
||||||
codeberg.org/gruf/go-sched v1.2.3
|
codeberg.org/gruf/go-sched v1.2.3
|
||||||
codeberg.org/gruf/go-store/v2 v2.2.4
|
codeberg.org/gruf/go-store/v2 v2.2.4
|
||||||
codeberg.org/gruf/go-structr v0.8.0
|
codeberg.org/gruf/go-structr v0.8.2
|
||||||
codeberg.org/superseriousbusiness/exif-terminator v0.7.0
|
codeberg.org/superseriousbusiness/exif-terminator v0.7.0
|
||||||
github.com/DmitriyVTitov/size v1.5.0
|
github.com/DmitriyVTitov/size v1.5.0
|
||||||
github.com/KimMachineGun/automemlimit v0.6.0
|
github.com/KimMachineGun/automemlimit v0.6.0
|
||||||
|
@ -75,7 +75,7 @@ require (
|
||||||
golang.org/x/crypto v0.22.0
|
golang.org/x/crypto v0.22.0
|
||||||
golang.org/x/image v0.16.0
|
golang.org/x/image v0.16.0
|
||||||
golang.org/x/net v0.24.0
|
golang.org/x/net v0.24.0
|
||||||
golang.org/x/oauth2 v0.19.0
|
golang.org/x/oauth2 v0.20.0
|
||||||
golang.org/x/text v0.15.0
|
golang.org/x/text v0.15.0
|
||||||
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
|
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
|
8
go.sum
8
go.sum
|
@ -74,8 +74,8 @@ codeberg.org/gruf/go-sched v1.2.3 h1:H5ViDxxzOBR3uIyGBCf0eH8b1L8wMybOXcdtUUTXZHk
|
||||||
codeberg.org/gruf/go-sched v1.2.3/go.mod h1:vT9uB6KWFIIwnG9vcPY2a0alYNoqdL1mSzRM8I+PK7A=
|
codeberg.org/gruf/go-sched v1.2.3/go.mod h1:vT9uB6KWFIIwnG9vcPY2a0alYNoqdL1mSzRM8I+PK7A=
|
||||||
codeberg.org/gruf/go-store/v2 v2.2.4 h1:8HO1Jh2gg7boQKA3hsDAIXd9zwieu5uXwDXEcTOD9js=
|
codeberg.org/gruf/go-store/v2 v2.2.4 h1:8HO1Jh2gg7boQKA3hsDAIXd9zwieu5uXwDXEcTOD9js=
|
||||||
codeberg.org/gruf/go-store/v2 v2.2.4/go.mod h1:zI4VWe5CpXAktYMtaBMrgA5QmO0sQH53LBRvfn1huys=
|
codeberg.org/gruf/go-store/v2 v2.2.4/go.mod h1:zI4VWe5CpXAktYMtaBMrgA5QmO0sQH53LBRvfn1huys=
|
||||||
codeberg.org/gruf/go-structr v0.8.0 h1:aZ+ziv2R6zTU16PW7B2d349wY9Du3mObc3hCeUIqtME=
|
codeberg.org/gruf/go-structr v0.8.2 h1:0zH5HuOZWTVOGqIq8o5W1jRi944CQWdPXUWZfKUbF0o=
|
||||||
codeberg.org/gruf/go-structr v0.8.0/go.mod h1:K1FXkUyO6N/JKt8aWqyQ8rtW7Z9ZmXKWP8mFAQ2OJjE=
|
codeberg.org/gruf/go-structr v0.8.2/go.mod h1:K1FXkUyO6N/JKt8aWqyQ8rtW7Z9ZmXKWP8mFAQ2OJjE=
|
||||||
codeberg.org/superseriousbusiness/exif-terminator v0.7.0 h1:Y6VApSXhKqExG0H2hZ2JelRK4xmWdjDQjn13CpEfzko=
|
codeberg.org/superseriousbusiness/exif-terminator v0.7.0 h1:Y6VApSXhKqExG0H2hZ2JelRK4xmWdjDQjn13CpEfzko=
|
||||||
codeberg.org/superseriousbusiness/exif-terminator v0.7.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
|
codeberg.org/superseriousbusiness/exif-terminator v0.7.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
@ -756,8 +756,8 @@ golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4Iltr
|
||||||
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||||
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
|
golang.org/x/oauth2 v0.20.0 h1:4mQdhULixXKP1rwYBW0vAijoXnkTG0BLCDRzfe1idMo=
|
||||||
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
|
golang.org/x/oauth2 v0.20.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
|
|
@ -0,0 +1,34 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
// FilterResult is returned along with a filtered status to explain why it was filtered.
|
||||||
|
//
|
||||||
|
// swagger:model filterResult
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - filters
|
||||||
|
type FilterResult struct {
|
||||||
|
// The filter that was matched.
|
||||||
|
Filter FilterV2 `json:"filter"`
|
||||||
|
// The keywords within the filter that were matched.
|
||||||
|
KeywordMatches []string `json:"keyword_matches"`
|
||||||
|
// The status IDs within the filter that were matched.
|
||||||
|
StatusMatches []string `json:"status_matches"`
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
// FilterV2 represents a user-defined filter for determining which statuses should not be shown to the user.
|
||||||
|
// v2 filters have names and can include multiple phrases and status IDs to filter.
|
||||||
|
//
|
||||||
|
// swagger:model filterV2
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - filters
|
||||||
|
type FilterV2 struct {
|
||||||
|
// The ID of the filter in the database.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// The name of the filter.
|
||||||
|
//
|
||||||
|
// Example: Linux Words
|
||||||
|
Title string `json:"title"`
|
||||||
|
// The contexts in which the filter should be applied.
|
||||||
|
//
|
||||||
|
// Minimum items: 1
|
||||||
|
// Unique: true
|
||||||
|
// Enum:
|
||||||
|
// - home
|
||||||
|
// - notifications
|
||||||
|
// - public
|
||||||
|
// - thread
|
||||||
|
// - account
|
||||||
|
// Example: ["home", "public"]
|
||||||
|
Context []FilterContext `json:"context"`
|
||||||
|
// When the filter should no longer be applied. Null if the filter does not expire.
|
||||||
|
//
|
||||||
|
// Example: 2024-02-01T02:57:49Z
|
||||||
|
ExpiresAt *string `json:"expires_at"`
|
||||||
|
// The action to be taken when a status matches this filter.
|
||||||
|
// Enum:
|
||||||
|
// - warn
|
||||||
|
// - hide
|
||||||
|
FilterAction FilterAction `json:"filter_action"`
|
||||||
|
// The keywords grouped under this filter.
|
||||||
|
Keywords []FilterKeyword `json:"keywords"`
|
||||||
|
// The statuses grouped under this filter.
|
||||||
|
Statuses []FilterStatus `json:"statuses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterAction is the action to apply to statuses matching a filter.
|
||||||
|
type FilterAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FilterActionNone filters should not exist, except internally, for partially constructed or invalid filters.
|
||||||
|
FilterActionNone FilterAction = ""
|
||||||
|
// FilterActionWarn filters will include this status in API results with a warning.
|
||||||
|
FilterActionWarn FilterAction = "warn"
|
||||||
|
// FilterActionHide filters will remove this status from API results.
|
||||||
|
FilterActionHide FilterAction = "hide"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FilterKeyword represents text to filter within a v2 filter.
|
||||||
|
//
|
||||||
|
// swagger:model filterKeyword
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - filters
|
||||||
|
type FilterKeyword struct {
|
||||||
|
// The ID of the filter keyword entry in the database.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// The text to be filtered.
|
||||||
|
//
|
||||||
|
// Example: fnord
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
// Should the filter consider word boundaries?
|
||||||
|
//
|
||||||
|
// Example: true
|
||||||
|
WholeWord bool `json:"whole_word"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterStatus represents a single status to filter within a v2 filter.
|
||||||
|
//
|
||||||
|
// swagger:model filterStatus
|
||||||
|
//
|
||||||
|
// ---
|
||||||
|
// tags:
|
||||||
|
// - filters
|
||||||
|
type FilterStatus struct {
|
||||||
|
// The ID of the filter status entry in the database.
|
||||||
|
ID string `json:"id"`
|
||||||
|
// The status ID to be filtered.
|
||||||
|
StatusID string `json:"phrase"`
|
||||||
|
}
|
|
@ -100,6 +100,8 @@ type Status struct {
|
||||||
// so the user may redraft from the source text without the client having to reverse-engineer
|
// so the user may redraft from the source text without the client having to reverse-engineer
|
||||||
// the original text from the HTML content.
|
// the original text from the HTML content.
|
||||||
Text string `json:"text,omitempty"`
|
Text string `json:"text,omitempty"`
|
||||||
|
// A list of filters that matched this status and why they matched, if there are any such filters.
|
||||||
|
Filtered []FilterResult `json:"filtered,omitempty"`
|
||||||
|
|
||||||
// Additional fields not exposed via JSON
|
// Additional fields not exposed via JSON
|
||||||
// (used only internally for templating etc).
|
// (used only internally for templating etc).
|
||||||
|
|
|
@ -148,15 +148,15 @@ type GTSCaches struct {
|
||||||
// Tag provides access to the gtsmodel Tag database cache.
|
// Tag provides access to the gtsmodel Tag database cache.
|
||||||
Tag StructCache[*gtsmodel.Tag]
|
Tag StructCache[*gtsmodel.Tag]
|
||||||
|
|
||||||
|
// ThreadMute provides access to the gtsmodel ThreadMute database cache.
|
||||||
|
ThreadMute StructCache[*gtsmodel.ThreadMute]
|
||||||
|
|
||||||
// Token provides access to the gtsmodel Token database cache.
|
// Token provides access to the gtsmodel Token database cache.
|
||||||
Token StructCache[*gtsmodel.Token]
|
Token StructCache[*gtsmodel.Token]
|
||||||
|
|
||||||
// Tombstone provides access to the gtsmodel Tombstone database cache.
|
// Tombstone provides access to the gtsmodel Tombstone database cache.
|
||||||
Tombstone StructCache[*gtsmodel.Tombstone]
|
Tombstone StructCache[*gtsmodel.Tombstone]
|
||||||
|
|
||||||
// ThreadMute provides access to the gtsmodel ThreadMute database cache.
|
|
||||||
ThreadMute StructCache[*gtsmodel.ThreadMute]
|
|
||||||
|
|
||||||
// User provides access to the gtsmodel User database cache.
|
// User provides access to the gtsmodel User database cache.
|
||||||
User StructCache[*gtsmodel.User]
|
User StructCache[*gtsmodel.User]
|
||||||
|
|
||||||
|
|
|
@ -172,6 +172,8 @@ func totalOfRatios() float64 {
|
||||||
return 0 +
|
return 0 +
|
||||||
config.GetCacheAccountMemRatio() +
|
config.GetCacheAccountMemRatio() +
|
||||||
config.GetCacheAccountNoteMemRatio() +
|
config.GetCacheAccountNoteMemRatio() +
|
||||||
|
config.GetCacheAccountSettingsMemRatio() +
|
||||||
|
config.GetCacheAccountStatsMemRatio() +
|
||||||
config.GetCacheApplicationMemRatio() +
|
config.GetCacheApplicationMemRatio() +
|
||||||
config.GetCacheBlockMemRatio() +
|
config.GetCacheBlockMemRatio() +
|
||||||
config.GetCacheBlockIDsMemRatio() +
|
config.GetCacheBlockIDsMemRatio() +
|
||||||
|
@ -179,17 +181,21 @@ func totalOfRatios() float64 {
|
||||||
config.GetCacheClientMemRatio() +
|
config.GetCacheClientMemRatio() +
|
||||||
config.GetCacheEmojiMemRatio() +
|
config.GetCacheEmojiMemRatio() +
|
||||||
config.GetCacheEmojiCategoryMemRatio() +
|
config.GetCacheEmojiCategoryMemRatio() +
|
||||||
|
config.GetCacheFilterMemRatio() +
|
||||||
|
config.GetCacheFilterKeywordMemRatio() +
|
||||||
|
config.GetCacheFilterStatusMemRatio() +
|
||||||
config.GetCacheFollowMemRatio() +
|
config.GetCacheFollowMemRatio() +
|
||||||
config.GetCacheFollowIDsMemRatio() +
|
config.GetCacheFollowIDsMemRatio() +
|
||||||
config.GetCacheFollowRequestMemRatio() +
|
config.GetCacheFollowRequestMemRatio() +
|
||||||
config.GetCacheFollowRequestIDsMemRatio() +
|
config.GetCacheFollowRequestIDsMemRatio() +
|
||||||
config.GetCacheInReplyToIDsMemRatio() +
|
|
||||||
config.GetCacheInstanceMemRatio() +
|
config.GetCacheInstanceMemRatio() +
|
||||||
|
config.GetCacheInReplyToIDsMemRatio() +
|
||||||
config.GetCacheListMemRatio() +
|
config.GetCacheListMemRatio() +
|
||||||
config.GetCacheListEntryMemRatio() +
|
config.GetCacheListEntryMemRatio() +
|
||||||
config.GetCacheMarkerMemRatio() +
|
config.GetCacheMarkerMemRatio() +
|
||||||
config.GetCacheMediaMemRatio() +
|
config.GetCacheMediaMemRatio() +
|
||||||
config.GetCacheMentionMemRatio() +
|
config.GetCacheMentionMemRatio() +
|
||||||
|
config.GetCacheMoveMemRatio() +
|
||||||
config.GetCacheNotificationMemRatio() +
|
config.GetCacheNotificationMemRatio() +
|
||||||
config.GetCachePollMemRatio() +
|
config.GetCachePollMemRatio() +
|
||||||
config.GetCachePollVoteMemRatio() +
|
config.GetCachePollVoteMemRatio() +
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
// GoToSocial
|
||||||
|
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
// Package status represents status filters managed by the user through the API.
|
||||||
|
package status
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrHideStatus indicates that a status has been filtered and should not be returned at all.
|
||||||
|
var ErrHideStatus = errors.New("hide status")
|
||||||
|
|
||||||
|
// FilterContext determines the filters that apply to a given status or list of statuses.
|
||||||
|
type FilterContext string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// FilterContextNone means no filters should be applied.
|
||||||
|
// There are no filters with this context; it's for internal use only.
|
||||||
|
FilterContextNone FilterContext = ""
|
||||||
|
// FilterContextHome means this status is being filtered as part of a home or list timeline.
|
||||||
|
FilterContextHome FilterContext = "home"
|
||||||
|
// FilterContextNotifications means this status is being filtered as part of the notifications timeline.
|
||||||
|
FilterContextNotifications FilterContext = "notifications"
|
||||||
|
// FilterContextPublic means this status is being filtered as part of a public or tag timeline.
|
||||||
|
FilterContextPublic FilterContext = "public"
|
||||||
|
// FilterContextThread means this status is being filtered as part of a thread's context.
|
||||||
|
FilterContextThread FilterContext = "thread"
|
||||||
|
// FilterContextAccount means this status is being filtered as part of an account's statuses.
|
||||||
|
FilterContextAccount FilterContext = "account"
|
||||||
|
)
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -74,7 +75,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert the status.
|
// Convert the status.
|
||||||
item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount)
|
item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error converting bookmarked status to api: %s", err)
|
log.Errorf(ctx, "error converting bookmarked status to api: %s", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -96,9 +97,15 @@ func (p *Processor) StatusesGet(
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, s := range filtered {
|
for _, s := range filtered {
|
||||||
// Convert filtered statuses to API statuses.
|
// Convert filtered statuses to API statuses.
|
||||||
item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount)
|
item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error convering to api status: %v", err)
|
log.Errorf(ctx, "error convering to api status: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -184,7 +185,7 @@ func (p *Processor) GetAPIStatus(
|
||||||
apiStatus *apimodel.Status,
|
apiStatus *apimodel.Status,
|
||||||
errWithCode gtserror.WithCode,
|
errWithCode gtserror.WithCode,
|
||||||
) {
|
) {
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting status: %w", err)
|
err = gtserror.Newf("error converting status: %w", err)
|
||||||
return nil, gtserror.NewErrorInternalError(err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
@ -192,87 +193,6 @@ func (p *Processor) GetAPIStatus(
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetVisibleAPIStatuses converts an array of gtsmodel.Status (inputted by next function) into
|
|
||||||
// API model statuses, checking first for visibility. Please note that all errors will be
|
|
||||||
// logged at ERROR level, but will not be returned. Callers are likely to run into show-stopping
|
|
||||||
// errors in the lead-up to this function, whereas calling this should not be a show-stopper.
|
|
||||||
func (p *Processor) GetVisibleAPIStatuses(
|
|
||||||
ctx context.Context,
|
|
||||||
requester *gtsmodel.Account,
|
|
||||||
next func(int) *gtsmodel.Status,
|
|
||||||
length int,
|
|
||||||
) []*apimodel.Status {
|
|
||||||
return p.getVisibleAPIStatuses(ctx, 3, requester, next, length)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetVisibleAPIStatusesPaged is functionally equivalent to GetVisibleAPIStatuses(),
|
|
||||||
// except the statuses are returned as a converted slice of statuses as interface{}.
|
|
||||||
func (p *Processor) GetVisibleAPIStatusesPaged(
|
|
||||||
ctx context.Context,
|
|
||||||
requester *gtsmodel.Account,
|
|
||||||
next func(int) *gtsmodel.Status,
|
|
||||||
length int,
|
|
||||||
) []interface{} {
|
|
||||||
statuses := p.getVisibleAPIStatuses(ctx, 3, requester, next, length)
|
|
||||||
if len(statuses) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
items := make([]interface{}, len(statuses))
|
|
||||||
for i, status := range statuses {
|
|
||||||
items[i] = status
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *Processor) getVisibleAPIStatuses(
|
|
||||||
ctx context.Context,
|
|
||||||
calldepth int, // used to skip wrapping func above these's names
|
|
||||||
requester *gtsmodel.Account,
|
|
||||||
next func(int) *gtsmodel.Status,
|
|
||||||
length int,
|
|
||||||
) []*apimodel.Status {
|
|
||||||
// Start new log entry with
|
|
||||||
// the above calling func's name.
|
|
||||||
l := log.
|
|
||||||
WithContext(ctx).
|
|
||||||
WithField("caller", log.Caller(calldepth+1))
|
|
||||||
|
|
||||||
// Preallocate slice according to expected length.
|
|
||||||
statuses := make([]*apimodel.Status, 0, length)
|
|
||||||
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
// Get next status.
|
|
||||||
status := next(i)
|
|
||||||
if status == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check whether this status is visible to requesting account.
|
|
||||||
visible, err := p.filter.StatusVisible(ctx, requester, status)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorf("error checking status visibility: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
|
||||||
// Not visible to requester.
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert the status to an API model representation.
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorf("error converting status: %v", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append API model to return slice.
|
|
||||||
statuses = append(statuses, apiStatus)
|
|
||||||
}
|
|
||||||
|
|
||||||
return statuses
|
|
||||||
}
|
|
||||||
|
|
||||||
// InvalidateTimelinedStatus is a shortcut function for invalidating the cached
|
// InvalidateTimelinedStatus is a shortcut function for invalidating the cached
|
||||||
// representation one status in the home timeline and all list timelines of the
|
// 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
|
// given accountID. It should only be called in cases where a status update
|
||||||
|
|
|
@ -21,6 +21,7 @@ import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -113,7 +114,7 @@ func (p *Processor) packageStatuses(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err)
|
log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
@ -280,7 +281,15 @@ func TopoSort(apiStatuses []*apimodel.Status, targetAccountID string) {
|
||||||
|
|
||||||
// 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) {
|
||||||
return p.contextGet(ctx, requestingAccount, targetStatusID, p.converter.StatusToAPIStatus)
|
filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
convert := func(ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) {
|
||||||
|
return p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextThread, filters)
|
||||||
|
}
|
||||||
|
return p.contextGet(ctx, requestingAccount, targetStatusID, convert)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebContextGet is like ContextGet, but is explicitly
|
// WebContextGet is like ContextGet, but is explicitly
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
"github.com/superseriousbusiness/gotosocial/internal/stream"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||||
)
|
)
|
||||||
|
@ -39,7 +40,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() {
|
||||||
suite.NoError(errWithCode)
|
suite.NoError(errWithCode)
|
||||||
|
|
||||||
editedStatus := suite.testStatuses["remote_account_1_status_1"]
|
editedStatus := suite.testStatuses["remote_account_1_status_1"]
|
||||||
apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(context.Background(), editedStatus, account)
|
apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(context.Background(), editedStatus, account, statusfilter.FilterContextNotifications, nil)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.streamProcessor.StatusUpdate(context.Background(), account, apiStatus, stream.TimelineHome)
|
suite.streamProcessor.StatusUpdate(context.Background(), account, apiStatus, stream.TimelineHome)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
@ -54,7 +55,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error convering to api status: %v", err)
|
log.Errorf(ctx, "error convering to api status: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -98,7 +99,13 @@ func HomeTimelineStatusPrepare(state *state.State, converter *typeutils.Converte
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return converter.StatusToAPIStatus(ctx, status, requestingAccount)
|
filters, err := state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,6 +23,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -110,7 +111,13 @@ func ListTimelineStatusPrepare(state *state.State, converter *typeutils.Converte
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return converter.StatusToAPIStatus(ctx, status, requestingAccount)
|
filters, err := state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -43,6 +43,12 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma
|
||||||
return util.EmptyPageableResponse(), nil
|
return util.EmptyPageableResponse(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters, err := p.state.DB.GetFiltersForAccountID(ctx, authed.Account.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", authed.Account.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
items = make([]interface{}, 0, count)
|
items = make([]interface{}, 0, count)
|
||||||
nextMaxIDValue string
|
nextMaxIDValue string
|
||||||
|
@ -70,7 +76,7 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
item, err := p.converter.NotificationToAPINotification(ctx, n)
|
item, err := p.converter.NotificationToAPINotification(ctx, n, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err)
|
log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err)
|
||||||
continue
|
continue
|
||||||
|
@ -104,7 +110,13 @@ func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Accou
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif)
|
filters, err := p.state.DB.GetFiltersForAccountID(ctx, account.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", account.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
return nil, gtserror.NewErrorNotFound(err)
|
return nil, gtserror.NewErrorNotFound(err)
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -46,6 +47,16 @@ func (p *Processor) PublicTimelineGet(
|
||||||
items = make([]any, 0, limit)
|
items = make([]any, 0, limit)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var filters []*gtsmodel.Filter
|
||||||
|
if requester != nil {
|
||||||
|
var err error
|
||||||
|
filters, err = p.state.DB.GetFiltersForAccountID(ctx, requester.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requester.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Try a few times to select appropriate public
|
// Try a few times to select appropriate public
|
||||||
// statuses from the db, paging up or down to
|
// statuses from the db, paging up or down to
|
||||||
// reattempt if nothing suitable is found.
|
// reattempt if nothing suitable is found.
|
||||||
|
@ -87,7 +98,10 @@ outer:
|
||||||
continue inner
|
continue inner
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requester)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requester, statusfilter.FilterContextPublic, filters)
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error converting to api status: %v", err)
|
log.Errorf(ctx, "error converting to api status: %v", err)
|
||||||
continue inner
|
continue inner
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
|
@ -111,6 +112,12 @@ func (p *Processor) packageTagResponse(
|
||||||
prevMinIDValue = statuses[0].ID
|
prevMinIDValue = statuses[0].ID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAcct.ID)
|
||||||
|
if err != nil {
|
||||||
|
err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAcct.ID, err)
|
||||||
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, s := range statuses {
|
for _, s := range statuses {
|
||||||
timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s)
|
timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -122,7 +129,10 @@ func (p *Processor) packageTagResponse(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct)
|
apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct, statusfilter.FilterContextPublic, filters)
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf(ctx, "error converting to api status: %v", err)
|
log.Errorf(ctx, "error converting to api status: %v", err)
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -28,6 +28,7 @@ import (
|
||||||
"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"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
|
@ -154,6 +155,8 @@ func (suite *FromClientAPITestSuite) statusJSON(
|
||||||
ctx,
|
ctx,
|
||||||
status,
|
status,
|
||||||
requestingAccount,
|
requestingAccount,
|
||||||
|
statusfilter.FilterContextNone,
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
|
@ -258,7 +261,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
|
||||||
suite.FailNow("timed out waiting for new status notification")
|
suite.FailNow("timed out waiting for new status notification")
|
||||||
}
|
}
|
||||||
|
|
||||||
apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif)
|
apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
suite.FailNow(err.Error())
|
suite.FailNow(err.Error())
|
||||||
}
|
}
|
||||||
|
|
|
@ -467,7 +467,12 @@ func (s *Surface) Notify(
|
||||||
unlock()
|
unlock()
|
||||||
|
|
||||||
// Stream notification to the user.
|
// Stream notification to the user.
|
||||||
apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif)
|
filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return gtserror.Newf("error converting notification to api representation: %w", err)
|
return gtserror.Newf("error converting notification to api representation: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,6 +22,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
@ -111,6 +112,11 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters, err := s.State.DB.GetFiltersForAccountID(ctx, follow.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.Newf("couldn't retrieve filters for account %s: %w", follow.AccountID, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Add status to any relevant lists
|
// Add status to any relevant lists
|
||||||
// for this follow, if applicable.
|
// for this follow, if applicable.
|
||||||
s.listTimelineStatusForFollow(
|
s.listTimelineStatusForFollow(
|
||||||
|
@ -118,6 +124,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
|
||||||
status,
|
status,
|
||||||
follow,
|
follow,
|
||||||
&errs,
|
&errs,
|
||||||
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add status to home timeline for owner
|
// Add status to home timeline for owner
|
||||||
|
@ -129,6 +136,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers(
|
||||||
follow.Account,
|
follow.Account,
|
||||||
status,
|
status,
|
||||||
stream.TimelineHome,
|
stream.TimelineHome,
|
||||||
|
filters,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Appendf("error home timelining status: %w", err)
|
errs.Appendf("error home timelining status: %w", err)
|
||||||
|
@ -180,6 +188,7 @@ func (s *Surface) listTimelineStatusForFollow(
|
||||||
status *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
follow *gtsmodel.Follow,
|
follow *gtsmodel.Follow,
|
||||||
errs *gtserror.MultiError,
|
errs *gtserror.MultiError,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) {
|
) {
|
||||||
// To put this status in appropriate list timelines,
|
// To put this status in appropriate list timelines,
|
||||||
// we need to get each listEntry that pertains to
|
// we need to get each listEntry that pertains to
|
||||||
|
@ -222,6 +231,7 @@ func (s *Surface) listTimelineStatusForFollow(
|
||||||
follow.Account,
|
follow.Account,
|
||||||
status,
|
status,
|
||||||
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
|
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
|
||||||
|
filters,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
|
errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
|
||||||
// implicit continue
|
// implicit continue
|
||||||
|
@ -332,6 +342,7 @@ func (s *Surface) timelineStatus(
|
||||||
account *gtsmodel.Account,
|
account *gtsmodel.Account,
|
||||||
status *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
streamType string,
|
streamType string,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
// Ingest status into given timeline using provided function.
|
// Ingest status into given timeline using provided function.
|
||||||
if inserted, err := ingest(ctx, timelineID, status); err != nil {
|
if inserted, err := ingest(ctx, timelineID, status); err != nil {
|
||||||
|
@ -343,7 +354,12 @@ func (s *Surface) timelineStatus(
|
||||||
}
|
}
|
||||||
|
|
||||||
// The status was inserted so stream it to the user.
|
// The status was inserted so stream it to the user.
|
||||||
apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account)
|
apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
|
||||||
|
status,
|
||||||
|
account,
|
||||||
|
statusfilter.FilterContextHome,
|
||||||
|
filters,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
||||||
return true, err
|
return true, err
|
||||||
|
@ -457,6 +473,11 @@ func (s *Surface) timelineStatusUpdateForFollowers(
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
filters, err := s.State.DB.GetFiltersForAccountID(ctx, follow.AccountID)
|
||||||
|
if err != nil {
|
||||||
|
return gtserror.Newf("couldn't retrieve filters for account %s: %w", follow.AccountID, err)
|
||||||
|
}
|
||||||
|
|
||||||
// Add status to any relevant lists
|
// Add status to any relevant lists
|
||||||
// for this follow, if applicable.
|
// for this follow, if applicable.
|
||||||
s.listTimelineStatusUpdateForFollow(
|
s.listTimelineStatusUpdateForFollow(
|
||||||
|
@ -464,6 +485,7 @@ func (s *Surface) timelineStatusUpdateForFollowers(
|
||||||
status,
|
status,
|
||||||
follow,
|
follow,
|
||||||
&errs,
|
&errs,
|
||||||
|
filters,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Add status to home timeline for owner
|
// Add status to home timeline for owner
|
||||||
|
@ -473,6 +495,7 @@ func (s *Surface) timelineStatusUpdateForFollowers(
|
||||||
follow.Account,
|
follow.Account,
|
||||||
status,
|
status,
|
||||||
stream.TimelineHome,
|
stream.TimelineHome,
|
||||||
|
filters,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs.Appendf("error home timelining status: %w", err)
|
errs.Appendf("error home timelining status: %w", err)
|
||||||
|
@ -490,6 +513,7 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
|
||||||
status *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
follow *gtsmodel.Follow,
|
follow *gtsmodel.Follow,
|
||||||
errs *gtserror.MultiError,
|
errs *gtserror.MultiError,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) {
|
) {
|
||||||
// To put this status in appropriate list timelines,
|
// To put this status in appropriate list timelines,
|
||||||
// we need to get each listEntry that pertains to
|
// we need to get each listEntry that pertains to
|
||||||
|
@ -530,6 +554,7 @@ func (s *Surface) listTimelineStatusUpdateForFollow(
|
||||||
follow.Account,
|
follow.Account,
|
||||||
status,
|
status,
|
||||||
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
|
stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list
|
||||||
|
filters,
|
||||||
); err != nil {
|
); err != nil {
|
||||||
errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
|
errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err)
|
||||||
// implicit continue
|
// implicit continue
|
||||||
|
@ -544,8 +569,13 @@ func (s *Surface) timelineStreamStatusUpdate(
|
||||||
account *gtsmodel.Account,
|
account *gtsmodel.Account,
|
||||||
status *gtsmodel.Status,
|
status *gtsmodel.Status,
|
||||||
streamType string,
|
streamType string,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) error {
|
) error {
|
||||||
apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account)
|
apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account, statusfilter.FilterContextHome, filters)
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
// Don't put this status in the stream.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -24,6 +24,7 @@ import (
|
||||||
|
|
||||||
"codeberg.org/gruf/go-kv"
|
"codeberg.org/gruf/go-kv"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
)
|
)
|
||||||
|
@ -121,6 +122,12 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID
|
||||||
for e, entry := range toPrepare {
|
for e, entry := range toPrepare {
|
||||||
prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID)
|
prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
// This item has been filtered out by the requesting user's filters.
|
||||||
|
// Remove it and skip past it.
|
||||||
|
t.items.data.Remove(e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
if errors.Is(err, db.ErrNoEntries) {
|
if errors.Is(err, db.ErrNoEntries) {
|
||||||
// ErrNoEntries means something has been deleted,
|
// ErrNoEntries means something has been deleted,
|
||||||
// so we'll likely not be able to ever prepare this.
|
// so we'll likely not be able to ever prepare this.
|
||||||
|
|
|
@ -473,16 +473,19 @@ const (
|
||||||
|
|
||||||
type TypeUtilsTestSuite struct {
|
type TypeUtilsTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db db.DB
|
db db.DB
|
||||||
state state.State
|
state state.State
|
||||||
testAccounts map[string]*gtsmodel.Account
|
testAccounts map[string]*gtsmodel.Account
|
||||||
testStatuses map[string]*gtsmodel.Status
|
testStatuses map[string]*gtsmodel.Status
|
||||||
testAttachments map[string]*gtsmodel.MediaAttachment
|
testAttachments map[string]*gtsmodel.MediaAttachment
|
||||||
testPeople map[string]vocab.ActivityStreamsPerson
|
testPeople map[string]vocab.ActivityStreamsPerson
|
||||||
testEmojis map[string]*gtsmodel.Emoji
|
testEmojis map[string]*gtsmodel.Emoji
|
||||||
testReports map[string]*gtsmodel.Report
|
testReports map[string]*gtsmodel.Report
|
||||||
testMentions map[string]*gtsmodel.Mention
|
testMentions map[string]*gtsmodel.Mention
|
||||||
testPollVotes map[string]*gtsmodel.PollVote
|
testPollVotes map[string]*gtsmodel.PollVote
|
||||||
|
testFilters map[string]*gtsmodel.Filter
|
||||||
|
testFilterKeywords map[string]*gtsmodel.FilterKeyword
|
||||||
|
testFilterStatues map[string]*gtsmodel.FilterStatus
|
||||||
|
|
||||||
typeconverter *typeutils.Converter
|
typeconverter *typeutils.Converter
|
||||||
}
|
}
|
||||||
|
@ -506,6 +509,9 @@ func (suite *TypeUtilsTestSuite) SetupTest() {
|
||||||
suite.testReports = testrig.NewTestReports()
|
suite.testReports = testrig.NewTestReports()
|
||||||
suite.testMentions = testrig.NewTestMentions()
|
suite.testMentions = testrig.NewTestMentions()
|
||||||
suite.testPollVotes = testrig.NewTestPollVotes()
|
suite.testPollVotes = testrig.NewTestPollVotes()
|
||||||
|
suite.testFilters = testrig.NewTestFilters()
|
||||||
|
suite.testFilterKeywords = testrig.NewTestFilterKeywords()
|
||||||
|
suite.testFilterStatues = testrig.NewTestFilterStatuses()
|
||||||
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
suite.typeconverter = typeutils.NewConverter(&suite.state)
|
||||||
|
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, nil)
|
||||||
|
|
|
@ -22,17 +22,21 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/language"
|
"github.com/superseriousbusiness/gotosocial/internal/language"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/text"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
@ -684,12 +688,19 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor
|
||||||
// (frontend) representation for serialization on the API.
|
// (frontend) representation for serialization on the API.
|
||||||
//
|
//
|
||||||
// Requesting account can be nil.
|
// Requesting account can be nil.
|
||||||
|
//
|
||||||
|
// Filter context can be the empty string if these statuses are not being filtered.
|
||||||
|
//
|
||||||
|
// If there is a matching "hide" filter, the returned status will be nil with a ErrHideStatus error;
|
||||||
|
// callers need to handle that case by excluding it from results.
|
||||||
func (c *Converter) StatusToAPIStatus(
|
func (c *Converter) StatusToAPIStatus(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
s *gtsmodel.Status,
|
s *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) (*apimodel.Status, error) {
|
) (*apimodel.Status, error) {
|
||||||
apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount)
|
apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -704,6 +715,142 @@ func (c *Converter) StatusToAPIStatus(
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// statusToAPIFilterResults applies filters to a status and returns an API filter result object.
|
||||||
|
// The result may be nil if no filters matched.
|
||||||
|
// If the status should not be returned at all, it returns the ErrHideStatus error.
|
||||||
|
func (c *Converter) statusToAPIFilterResults(
|
||||||
|
ctx context.Context,
|
||||||
|
s *gtsmodel.Status,
|
||||||
|
requestingAccount *gtsmodel.Account,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
|
) ([]apimodel.FilterResult, error) {
|
||||||
|
if filterContext == "" || len(filters) == 0 || s.AccountID == requestingAccount.ID {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
filterResults := make([]apimodel.FilterResult, 0, len(filters))
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for _, filter := range filters {
|
||||||
|
if !filterAppliesInContext(filter, filterContext) {
|
||||||
|
// Filter doesn't apply to this context.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !filter.ExpiresAt.IsZero() && filter.ExpiresAt.Before(now) {
|
||||||
|
// Filter is expired.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// List all matching keywords.
|
||||||
|
keywordMatches := make([]string, 0, len(filter.Keywords))
|
||||||
|
fields := filterableTextFields(s)
|
||||||
|
for _, filterKeyword := range filter.Keywords {
|
||||||
|
wholeWord := util.PtrValueOr(filterKeyword.WholeWord, false)
|
||||||
|
wordBreak := ``
|
||||||
|
if wholeWord {
|
||||||
|
wordBreak = `\b`
|
||||||
|
}
|
||||||
|
re, err := regexp.Compile(`(?i)` + wordBreak + regexp.QuoteMeta(filterKeyword.Keyword) + wordBreak)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var isMatch bool
|
||||||
|
for _, field := range fields {
|
||||||
|
if re.MatchString(field) {
|
||||||
|
isMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isMatch {
|
||||||
|
keywordMatches = append(keywordMatches, filterKeyword.Keyword)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A status has only one ID. Not clear why this is a list in the Mastodon API.
|
||||||
|
statusMatches := make([]string, 0, 1)
|
||||||
|
for _, filterStatus := range filter.Statuses {
|
||||||
|
if s.ID == filterStatus.StatusID {
|
||||||
|
statusMatches = append(statusMatches, filterStatus.StatusID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keywordMatches) > 0 || len(statusMatches) > 0 {
|
||||||
|
switch filter.Action {
|
||||||
|
case gtsmodel.FilterActionWarn:
|
||||||
|
// Record what matched.
|
||||||
|
apiFilter, err := c.FilterToAPIFilterV2(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
filterResults = append(filterResults, apimodel.FilterResult{
|
||||||
|
Filter: *apiFilter,
|
||||||
|
KeywordMatches: keywordMatches,
|
||||||
|
StatusMatches: statusMatches,
|
||||||
|
})
|
||||||
|
|
||||||
|
case gtsmodel.FilterActionHide:
|
||||||
|
// Don't show this status. Immediate return.
|
||||||
|
return nil, statusfilter.ErrHideStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterResults, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterableTextFields returns all text from a status that we might want to filter on:
|
||||||
|
// - content
|
||||||
|
// - content warning
|
||||||
|
// - media descriptions
|
||||||
|
// - poll options
|
||||||
|
func filterableTextFields(s *gtsmodel.Status) []string {
|
||||||
|
fieldCount := 2 + len(s.Attachments)
|
||||||
|
if s.Poll != nil {
|
||||||
|
fieldCount += len(s.Poll.Options)
|
||||||
|
}
|
||||||
|
fields := make([]string, 0, fieldCount)
|
||||||
|
|
||||||
|
if s.Content != "" {
|
||||||
|
fields = append(fields, text.SanitizeToPlaintext(s.Content))
|
||||||
|
}
|
||||||
|
if s.ContentWarning != "" {
|
||||||
|
fields = append(fields, s.ContentWarning)
|
||||||
|
}
|
||||||
|
for _, attachment := range s.Attachments {
|
||||||
|
if attachment.Description != "" {
|
||||||
|
fields = append(fields, attachment.Description)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.Poll != nil {
|
||||||
|
for _, option := range s.Poll.Options {
|
||||||
|
if option != "" {
|
||||||
|
fields = append(fields, option)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterAppliesInContext returns whether a given filter applies in a given context.
|
||||||
|
func filterAppliesInContext(filter *gtsmodel.Filter, filterContext statusfilter.FilterContext) bool {
|
||||||
|
switch filterContext {
|
||||||
|
case statusfilter.FilterContextHome:
|
||||||
|
return util.PtrValueOr(filter.ContextHome, false)
|
||||||
|
case statusfilter.FilterContextNotifications:
|
||||||
|
return util.PtrValueOr(filter.ContextNotifications, false)
|
||||||
|
case statusfilter.FilterContextPublic:
|
||||||
|
return util.PtrValueOr(filter.ContextPublic, false)
|
||||||
|
case statusfilter.FilterContextThread:
|
||||||
|
return util.PtrValueOr(filter.ContextThread, false)
|
||||||
|
case statusfilter.FilterContextAccount:
|
||||||
|
return util.PtrValueOr(filter.ContextAccount, false)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// StatusToWebStatus converts a gts model status into an
|
// StatusToWebStatus converts a gts model status into an
|
||||||
// api representation suitable for serving into a web template.
|
// api representation suitable for serving into a web template.
|
||||||
//
|
//
|
||||||
|
@ -713,7 +860,7 @@ func (c *Converter) StatusToWebStatus(
|
||||||
s *gtsmodel.Status,
|
s *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
) (*apimodel.Status, error) {
|
) (*apimodel.Status, error) {
|
||||||
webStatus, err := c.statusToFrontend(ctx, s, requestingAccount)
|
webStatus, err := c.statusToFrontend(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -815,6 +962,8 @@ func (c *Converter) statusToFrontend(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
s *gtsmodel.Status,
|
s *gtsmodel.Status,
|
||||||
requestingAccount *gtsmodel.Account,
|
requestingAccount *gtsmodel.Account,
|
||||||
|
filterContext statusfilter.FilterContext,
|
||||||
|
filters []*gtsmodel.Filter,
|
||||||
) (*apimodel.Status, error) {
|
) (*apimodel.Status, error) {
|
||||||
// Try to populate status struct pointer fields.
|
// Try to populate status struct pointer fields.
|
||||||
// We can continue in many cases of partial failure,
|
// We can continue in many cases of partial failure,
|
||||||
|
@ -913,7 +1062,11 @@ func (c *Converter) statusToFrontend(
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.BoostOf != nil {
|
if s.BoostOf != nil {
|
||||||
reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount)
|
reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount, filterContext, filters)
|
||||||
|
if errors.Is(err, statusfilter.ErrHideStatus) {
|
||||||
|
// If we'd hide the original status, hide the boost.
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, gtserror.Newf("error converting boosted status: %w", err)
|
return nil, gtserror.Newf("error converting boosted status: %w", err)
|
||||||
}
|
}
|
||||||
|
@ -977,6 +1130,13 @@ func (c *Converter) statusToFrontend(
|
||||||
s.URL = s.URI
|
s.URL = s.URI
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply filters.
|
||||||
|
filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error applying filters: %w", err)
|
||||||
|
}
|
||||||
|
apiStatus.Filtered = filterResults
|
||||||
|
|
||||||
return apiStatus, nil
|
return apiStatus, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1252,7 +1412,7 @@ func (c *Converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmod
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotificationToAPINotification converts a gts notification into a api notification
|
// NotificationToAPINotification converts a gts notification into a api notification
|
||||||
func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error) {
|
func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification, filters []*gtsmodel.Filter) (*apimodel.Notification, error) {
|
||||||
if n.TargetAccount == nil {
|
if n.TargetAccount == nil {
|
||||||
tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID)
|
tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1293,7 +1453,7 @@ func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmod
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount)
|
apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount, statusfilter.FilterContextNotifications, filters)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err)
|
return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -1446,7 +1606,7 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, s := range r.Statuses {
|
for _, s := range r.Statuses {
|
||||||
status, err := c.StatusToAPIStatus(ctx, s, requestingAccount)
|
status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err)
|
||||||
}
|
}
|
||||||
|
@ -1687,6 +1847,55 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor
|
||||||
}
|
}
|
||||||
filter := filterKeyword.Filter
|
filter := filterKeyword.Filter
|
||||||
|
|
||||||
|
return &apimodel.FilterV1{
|
||||||
|
// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID.
|
||||||
|
ID: filterKeyword.ID,
|
||||||
|
Phrase: filterKeyword.Keyword,
|
||||||
|
Context: filterToAPIFilterContexts(filter),
|
||||||
|
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
||||||
|
ExpiresAt: filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt),
|
||||||
|
Irreversible: filter.Action == gtsmodel.FilterActionHide,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterToAPIFilterV2 converts one GTS model filter into an API v2 filter.
|
||||||
|
func (c *Converter) FilterToAPIFilterV2(ctx context.Context, filter *gtsmodel.Filter) (*apimodel.FilterV2, error) {
|
||||||
|
apiFilterKeywords := make([]apimodel.FilterKeyword, 0, len(filter.Keywords))
|
||||||
|
for _, filterKeyword := range filter.Keywords {
|
||||||
|
apiFilterKeywords = append(apiFilterKeywords, apimodel.FilterKeyword{
|
||||||
|
ID: filterKeyword.ID,
|
||||||
|
Keyword: filterKeyword.Keyword,
|
||||||
|
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
apiFilterStatuses := make([]apimodel.FilterStatus, 0, len(filter.Keywords))
|
||||||
|
for _, filterStatus := range filter.Statuses {
|
||||||
|
apiFilterStatuses = append(apiFilterStatuses, apimodel.FilterStatus{
|
||||||
|
ID: filterStatus.ID,
|
||||||
|
StatusID: filterStatus.StatusID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return &apimodel.FilterV2{
|
||||||
|
ID: filter.ID,
|
||||||
|
Title: filter.Title,
|
||||||
|
Context: filterToAPIFilterContexts(filter),
|
||||||
|
ExpiresAt: filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt),
|
||||||
|
FilterAction: filterActionToAPIFilterAction(filter.Action),
|
||||||
|
Keywords: apiFilterKeywords,
|
||||||
|
Statuses: apiFilterStatuses,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterExpiresAtToAPIFilterExpiresAt(expiresAt time.Time) *string {
|
||||||
|
if expiresAt.IsZero() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return util.Ptr(util.FormatISO8601(expiresAt))
|
||||||
|
}
|
||||||
|
|
||||||
|
func filterToAPIFilterContexts(filter *gtsmodel.Filter) []apimodel.FilterContext {
|
||||||
apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues)
|
apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues)
|
||||||
if util.PtrValueOr(filter.ContextHome, false) {
|
if util.PtrValueOr(filter.ContextHome, false) {
|
||||||
apiContexts = append(apiContexts, apimodel.FilterContextHome)
|
apiContexts = append(apiContexts, apimodel.FilterContextHome)
|
||||||
|
@ -1703,21 +1912,17 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor
|
||||||
if util.PtrValueOr(filter.ContextAccount, false) {
|
if util.PtrValueOr(filter.ContextAccount, false) {
|
||||||
apiContexts = append(apiContexts, apimodel.FilterContextAccount)
|
apiContexts = append(apiContexts, apimodel.FilterContextAccount)
|
||||||
}
|
}
|
||||||
|
return apiContexts
|
||||||
|
}
|
||||||
|
|
||||||
var expiresAt *string
|
func filterActionToAPIFilterAction(m gtsmodel.FilterAction) apimodel.FilterAction {
|
||||||
if !filter.ExpiresAt.IsZero() {
|
switch m {
|
||||||
expiresAt = util.Ptr(util.FormatISO8601(filter.ExpiresAt))
|
case gtsmodel.FilterActionWarn:
|
||||||
|
return apimodel.FilterActionWarn
|
||||||
|
case gtsmodel.FilterActionHide:
|
||||||
|
return apimodel.FilterActionHide
|
||||||
}
|
}
|
||||||
|
return apimodel.FilterActionNone
|
||||||
return &apimodel.FilterV1{
|
|
||||||
// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID.
|
|
||||||
ID: filterKeyword.ID,
|
|
||||||
Phrase: filterKeyword.Keyword,
|
|
||||||
Context: apiContexts,
|
|
||||||
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
|
||||||
ExpiresAt: expiresAt,
|
|
||||||
Irreversible: filter.Action == gtsmodel.FilterActionHide,
|
|
||||||
}, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
|
// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
|
||||||
|
|
|
@ -25,6 +25,7 @@ import (
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
@ -427,7 +428,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc
|
||||||
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
||||||
testStatus := suite.testStatuses["admin_account_status_1"]
|
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
requestingAccount := suite.testAccounts["local_account_1"]
|
requestingAccount := suite.testAccounts["local_account_1"]
|
||||||
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount)
|
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
b, err := json.MarshalIndent(apiStatus, "", " ")
|
b, err := json.MarshalIndent(apiStatus, "", " ")
|
||||||
|
@ -537,11 +538,186 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() {
|
||||||
}`, string(b))
|
}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that a status which is filtered with a warn filter by the requesting user has `filtered` set correctly.
|
||||||
|
func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() {
|
||||||
|
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
|
testStatus.Content += " fnord"
|
||||||
|
testStatus.Text += " fnord"
|
||||||
|
requestingAccount := suite.testAccounts["local_account_1"]
|
||||||
|
expectedMatchingFilter := suite.testFilters["local_account_1_filter_1"]
|
||||||
|
expectedMatchingFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||||
|
expectedMatchingFilterKeyword.Filter = expectedMatchingFilter
|
||||||
|
expectedMatchingFilter.Keywords = []*gtsmodel.FilterKeyword{expectedMatchingFilterKeyword}
|
||||||
|
requestingAccountFilters := []*gtsmodel.Filter{expectedMatchingFilter}
|
||||||
|
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
|
||||||
|
context.Background(),
|
||||||
|
testStatus,
|
||||||
|
requestingAccount,
|
||||||
|
statusfilter.FilterContextHome,
|
||||||
|
requestingAccountFilters,
|
||||||
|
)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
b, err := json.MarshalIndent(apiStatus, "", " ")
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{
|
||||||
|
"id": "01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
|
"created_at": "2021-10-20T11:36:45.000Z",
|
||||||
|
"in_reply_to_id": null,
|
||||||
|
"in_reply_to_account_id": null,
|
||||||
|
"sensitive": false,
|
||||||
|
"spoiler_text": "",
|
||||||
|
"visibility": "public",
|
||||||
|
"language": "en",
|
||||||
|
"uri": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
|
"url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R",
|
||||||
|
"replies_count": 1,
|
||||||
|
"reblogs_count": 0,
|
||||||
|
"favourites_count": 1,
|
||||||
|
"favourited": true,
|
||||||
|
"reblogged": false,
|
||||||
|
"muted": false,
|
||||||
|
"bookmarked": true,
|
||||||
|
"pinned": false,
|
||||||
|
"content": "hello world! #welcome ! first post on the instance :rainbow: ! fnord",
|
||||||
|
"reblog": null,
|
||||||
|
"application": {
|
||||||
|
"name": "superseriousbusiness",
|
||||||
|
"website": "https://superserious.business"
|
||||||
|
},
|
||||||
|
"account": {
|
||||||
|
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
|
||||||
|
"username": "admin",
|
||||||
|
"acct": "admin",
|
||||||
|
"display_name": "",
|
||||||
|
"locked": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2022-05-17T13:10:59.000Z",
|
||||||
|
"note": "",
|
||||||
|
"url": "http://localhost:8080/@admin",
|
||||||
|
"avatar": "",
|
||||||
|
"avatar_static": "",
|
||||||
|
"header": "http://localhost:8080/assets/default_header.png",
|
||||||
|
"header_static": "http://localhost:8080/assets/default_header.png",
|
||||||
|
"followers_count": 1,
|
||||||
|
"following_count": 1,
|
||||||
|
"statuses_count": 4,
|
||||||
|
"last_status_at": "2021-10-20T10:41:37.000Z",
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [],
|
||||||
|
"enable_rss": true,
|
||||||
|
"role": {
|
||||||
|
"name": "admin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"media_attachments": [
|
||||||
|
{
|
||||||
|
"id": "01F8MH6NEM8D7527KZAECTCR76",
|
||||||
|
"type": "image",
|
||||||
|
"url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
|
"text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
|
"preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg",
|
||||||
|
"remote_url": null,
|
||||||
|
"preview_remote_url": null,
|
||||||
|
"meta": {
|
||||||
|
"original": {
|
||||||
|
"width": 1200,
|
||||||
|
"height": 630,
|
||||||
|
"size": "1200x630",
|
||||||
|
"aspect": 1.9047619
|
||||||
|
},
|
||||||
|
"small": {
|
||||||
|
"width": 256,
|
||||||
|
"height": 134,
|
||||||
|
"size": "256x134",
|
||||||
|
"aspect": 1.9104477
|
||||||
|
},
|
||||||
|
"focus": {
|
||||||
|
"x": 0,
|
||||||
|
"y": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "Black and white image of some 50's style text saying: Welcome On Board",
|
||||||
|
"blurhash": "LNJRdVM{00Rj%Mayt7j[4nWBofRj"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"mentions": [],
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "welcome",
|
||||||
|
"url": "http://localhost:8080/tags/welcome"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"shortcode": "rainbow",
|
||||||
|
"url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
|
||||||
|
"static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png",
|
||||||
|
"visible_in_picker": true,
|
||||||
|
"category": "reactions"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"card": null,
|
||||||
|
"poll": null,
|
||||||
|
"text": "hello world! #welcome ! first post on the instance :rainbow: ! fnord",
|
||||||
|
"filtered": [
|
||||||
|
{
|
||||||
|
"filter": {
|
||||||
|
"id": "01HN26VM6KZTW1ANNRVSBMA461",
|
||||||
|
"title": "fnord",
|
||||||
|
"context": [
|
||||||
|
"home",
|
||||||
|
"public"
|
||||||
|
],
|
||||||
|
"expires_at": null,
|
||||||
|
"filter_action": "warn",
|
||||||
|
"keywords": [
|
||||||
|
{
|
||||||
|
"id": "01HN272TAVWAXX72ZX4M8JZ0PS",
|
||||||
|
"keyword": "fnord",
|
||||||
|
"whole_word": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"statuses": []
|
||||||
|
},
|
||||||
|
"keyword_matches": [
|
||||||
|
"fnord"
|
||||||
|
],
|
||||||
|
"status_matches": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a status which is filtered with a hide filter by the requesting user results in the ErrHideStatus error.
|
||||||
|
func (suite *InternalToFrontendTestSuite) TestHideFilteredStatusToFrontend() {
|
||||||
|
testStatus := suite.testStatuses["admin_account_status_1"]
|
||||||
|
testStatus.Content += " fnord"
|
||||||
|
testStatus.Text += " fnord"
|
||||||
|
requestingAccount := suite.testAccounts["local_account_1"]
|
||||||
|
expectedMatchingFilter := suite.testFilters["local_account_1_filter_1"]
|
||||||
|
expectedMatchingFilter.Action = gtsmodel.FilterActionHide
|
||||||
|
expectedMatchingFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||||
|
expectedMatchingFilterKeyword.Filter = expectedMatchingFilter
|
||||||
|
expectedMatchingFilter.Keywords = []*gtsmodel.FilterKeyword{expectedMatchingFilterKeyword}
|
||||||
|
requestingAccountFilters := []*gtsmodel.Filter{expectedMatchingFilter}
|
||||||
|
_, err := suite.typeconverter.StatusToAPIStatus(
|
||||||
|
context.Background(),
|
||||||
|
testStatus,
|
||||||
|
requestingAccount,
|
||||||
|
statusfilter.FilterContextHome,
|
||||||
|
requestingAccountFilters,
|
||||||
|
)
|
||||||
|
suite.ErrorIs(err, statusfilter.ErrHideStatus)
|
||||||
|
}
|
||||||
|
|
||||||
func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() {
|
func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() {
|
||||||
testStatus := suite.testStatuses["remote_account_2_status_1"]
|
testStatus := suite.testStatuses["remote_account_2_status_1"]
|
||||||
requestingAccount := suite.testAccounts["admin_account"]
|
requestingAccount := suite.testAccounts["admin_account"]
|
||||||
|
|
||||||
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount)
|
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
b, err := json.MarshalIndent(apiStatus, "", " ")
|
b, err := json.MarshalIndent(apiStatus, "", " ")
|
||||||
|
@ -774,7 +950,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage()
|
||||||
*testStatus = *suite.testStatuses["admin_account_status_1"]
|
*testStatus = *suite.testStatuses["admin_account_status_1"]
|
||||||
testStatus.Language = ""
|
testStatus.Language = ""
|
||||||
requestingAccount := suite.testAccounts["local_account_1"]
|
requestingAccount := suite.testAccounts["local_account_1"]
|
||||||
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount)
|
apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
b, err := json.MarshalIndent(apiStatus, "", " ")
|
b, err := json.MarshalIndent(apiStatus, "", " ")
|
||||||
|
|
|
@ -556,6 +556,12 @@ func (c *Cache[T]) Cap() int {
|
||||||
func (c *Cache[T]) store_value(index *Index, key Key, value T) {
|
func (c *Cache[T]) store_value(index *Index, key Key, value T) {
|
||||||
// Alloc new index item.
|
// Alloc new index item.
|
||||||
item := new_indexed_item()
|
item := new_indexed_item()
|
||||||
|
if cap(item.indexed) < len(c.indices) {
|
||||||
|
|
||||||
|
// Preallocate item indices slice to prevent Go auto
|
||||||
|
// allocating overlying large slices we don't need.
|
||||||
|
item.indexed = make([]*index_entry, 0, len(c.indices))
|
||||||
|
}
|
||||||
|
|
||||||
// Create COPY of value.
|
// Create COPY of value.
|
||||||
value = c.copy(value)
|
value = c.copy(value)
|
||||||
|
@ -622,6 +628,14 @@ func (c *Cache[T]) store_error(index *Index, key Key, err error) {
|
||||||
|
|
||||||
// Alloc new index item.
|
// Alloc new index item.
|
||||||
item := new_indexed_item()
|
item := new_indexed_item()
|
||||||
|
if cap(item.indexed) < len(c.indices) {
|
||||||
|
|
||||||
|
// Preallocate item indices slice to prevent Go auto
|
||||||
|
// allocating overlying large slices we don't need.
|
||||||
|
item.indexed = make([]*index_entry, 0, len(c.indices))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set error val.
|
||||||
item.data = err
|
item.data = err
|
||||||
|
|
||||||
// Append item to index.
|
// Append item to index.
|
||||||
|
|
|
@ -51,8 +51,12 @@ func (i *indexed_item) drop_index(entry *index_entry) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Unset tptr value to
|
||||||
|
// ensure GC can take it.
|
||||||
|
i.indexed[x] = nil
|
||||||
|
|
||||||
// Move all index entries down + reslice.
|
// Move all index entries down + reslice.
|
||||||
copy(i.indexed[x:], i.indexed[x+1:])
|
_ = copy(i.indexed[x:], i.indexed[x+1:])
|
||||||
i.indexed = i.indexed[:len(i.indexed)-1]
|
i.indexed = i.indexed[:len(i.indexed)-1]
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -276,6 +276,12 @@ func (q *Queue[T]) pop_n(n int, next func() *list_elem) []T {
|
||||||
|
|
||||||
func (q *Queue[T]) index(value T) *indexed_item {
|
func (q *Queue[T]) index(value T) *indexed_item {
|
||||||
item := new_indexed_item()
|
item := new_indexed_item()
|
||||||
|
if cap(item.indexed) < len(q.indices) {
|
||||||
|
|
||||||
|
// Preallocate item indices slice to prevent Go auto
|
||||||
|
// allocating overlying large slices we don't need.
|
||||||
|
item.indexed = make([]*index_entry, 0, len(q.indices))
|
||||||
|
}
|
||||||
|
|
||||||
// Set item value.
|
// Set item value.
|
||||||
item.data = value
|
item.data = value
|
||||||
|
|
|
@ -393,7 +393,7 @@ func ReuseTokenSource(t *Token, src TokenSource) TokenSource {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReuseTokenSource returns a TokenSource that acts in the same manner as the
|
// ReuseTokenSourceWithExpiry returns a TokenSource that acts in the same manner as the
|
||||||
// TokenSource returned by ReuseTokenSource, except the expiry buffer is
|
// TokenSource returned by ReuseTokenSource, except the expiry buffer is
|
||||||
// configurable. The expiration time of a token is calculated as
|
// configurable. The expiration time of a token is calculated as
|
||||||
// t.Expiry.Add(-earlyExpiry).
|
// t.Expiry.Add(-earlyExpiry).
|
||||||
|
|
|
@ -62,7 +62,7 @@ codeberg.org/gruf/go-sched
|
||||||
## explicit; go 1.19
|
## explicit; go 1.19
|
||||||
codeberg.org/gruf/go-store/v2/storage
|
codeberg.org/gruf/go-store/v2/storage
|
||||||
codeberg.org/gruf/go-store/v2/util
|
codeberg.org/gruf/go-store/v2/util
|
||||||
# codeberg.org/gruf/go-structr v0.8.0
|
# codeberg.org/gruf/go-structr v0.8.2
|
||||||
## explicit; go 1.21
|
## explicit; go 1.21
|
||||||
codeberg.org/gruf/go-structr
|
codeberg.org/gruf/go-structr
|
||||||
# codeberg.org/superseriousbusiness/exif-terminator v0.7.0
|
# codeberg.org/superseriousbusiness/exif-terminator v0.7.0
|
||||||
|
@ -1056,7 +1056,7 @@ golang.org/x/net/ipv6
|
||||||
golang.org/x/net/proxy
|
golang.org/x/net/proxy
|
||||||
golang.org/x/net/publicsuffix
|
golang.org/x/net/publicsuffix
|
||||||
golang.org/x/net/trace
|
golang.org/x/net/trace
|
||||||
# golang.org/x/oauth2 v0.19.0
|
# golang.org/x/oauth2 v0.20.0
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
golang.org/x/oauth2
|
golang.org/x/oauth2
|
||||||
golang.org/x/oauth2/internal
|
golang.org/x/oauth2/internal
|
||||||
|
|
|
@ -17,7 +17,9 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import { SerializedError } from "@reduxjs/toolkit";
|
||||||
|
import { FetchBaseQueryError } from "@reduxjs/toolkit/query";
|
||||||
|
import React, { ReactNode } from "react";
|
||||||
|
|
||||||
function ErrorFallback({ error, resetErrorBoundary }) {
|
function ErrorFallback({ error, resetErrorBoundary }) {
|
||||||
return (
|
return (
|
||||||
|
@ -44,39 +46,70 @@ function ErrorFallback({ error, resetErrorBoundary }) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Error({ error }) {
|
interface GtsError {
|
||||||
/* eslint-disable-next-line no-console */
|
/**
|
||||||
console.error("Rendering error:", error);
|
* Error message returned from the API.
|
||||||
let message;
|
*/
|
||||||
|
error: string;
|
||||||
|
|
||||||
if (error.data != undefined) { // RTK Query error with data
|
/**
|
||||||
if (error.status) {
|
* For OAuth errors: description of the error.
|
||||||
message = (<>
|
*/
|
||||||
<b>{error.status}:</b> {error.data.error}
|
error_description?: string;
|
||||||
{error.data.error_description &&
|
}
|
||||||
<p>
|
|
||||||
{error.data.error_description}
|
interface ErrorProps {
|
||||||
</p>
|
error: FetchBaseQueryError | SerializedError | Error | undefined;
|
||||||
}
|
|
||||||
</>);
|
/**
|
||||||
} else {
|
* Optional function to clear the error.
|
||||||
message = error.data.error;
|
* If provided, rendered error will have
|
||||||
}
|
* a "dismiss" button.
|
||||||
} else if (error.name != undefined || error.type != undefined) { // JS error
|
*/
|
||||||
message = (<>
|
reset?: () => void;
|
||||||
<b>{error.type && error.name}:</b> {error.message}
|
}
|
||||||
</>);
|
|
||||||
} else if (error.status && typeof error.error == "string") {
|
function Error({ error, reset }: ErrorProps) {
|
||||||
message = (<>
|
if (error === undefined) {
|
||||||
<b>{error.status}:</b> {error.error}
|
return null;
|
||||||
</>);
|
}
|
||||||
|
|
||||||
|
/* eslint-disable-next-line no-console */
|
||||||
|
console.error("caught error: ", error);
|
||||||
|
|
||||||
|
let message: ReactNode;
|
||||||
|
if ("status" in error) {
|
||||||
|
// RTK Query error with data.
|
||||||
|
const gtsError = error.data as GtsError;
|
||||||
|
const errMsg = gtsError.error_description ?? gtsError.error;
|
||||||
|
message = <>Code {error.status} {errMsg}</>;
|
||||||
} else {
|
} else {
|
||||||
message = error.message ?? error;
|
// SerializedError or Error.
|
||||||
|
const errMsg = error.message ?? JSON.stringify(error);
|
||||||
|
message = (
|
||||||
|
<>{error.name && `${error.name}: `}{errMsg}</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = "error";
|
||||||
|
if (reset) {
|
||||||
|
className += " with-dismiss";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="error">
|
<div className={className}>
|
||||||
{message}
|
<span>{message}</span>
|
||||||
|
{ reset &&
|
||||||
|
<span
|
||||||
|
className="dismiss"
|
||||||
|
onClick={reset}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
<span>Dismiss</span>
|
||||||
|
<i className="fa fa-fw fa-close" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ import type {
|
||||||
RadioFormInputHook,
|
RadioFormInputHook,
|
||||||
TextFormInputHook,
|
TextFormInputHook,
|
||||||
} from "../../lib/form/types";
|
} from "../../lib/form/types";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
export interface TextInputProps extends React.DetailedHTMLProps<
|
export interface TextInputProps extends React.DetailedHTMLProps<
|
||||||
React.InputHTMLAttributes<HTMLInputElement>,
|
React.InputHTMLAttributes<HTMLInputElement>,
|
||||||
|
@ -92,22 +93,25 @@ export interface FileInputProps extends React.DetailedHTMLProps<
|
||||||
|
|
||||||
export function FileInput({ label, field, ...props }: FileInputProps) {
|
export function FileInput({ label, field, ...props }: FileInputProps) {
|
||||||
const { onChange, ref, infoComponent } = field;
|
const { onChange, ref, infoComponent } = field;
|
||||||
|
const id = nanoid();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="form-field file">
|
<div className="form-field file">
|
||||||
<label>
|
<label className="label-label" htmlFor={id}>
|
||||||
<div className="label">{label}</div>
|
{label}
|
||||||
<div className="file-input button">Browse</div>
|
|
||||||
{infoComponent}
|
|
||||||
{/* <a onClick={removeFile("header")}>remove</a> */}
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
className="hidden"
|
|
||||||
onChange={onChange}
|
|
||||||
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
|
<label className="label-button" htmlFor={id}>
|
||||||
|
<div className="file-input button">Browse</div>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
type="file"
|
||||||
|
className="hidden"
|
||||||
|
onChange={onChange}
|
||||||
|
ref={ref ? ref as RefObject<HTMLInputElement> : undefined}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{infoComponent}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -51,9 +51,9 @@ export default function MutationButton({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={wrapperClassName}>
|
<div className={wrapperClassName ? wrapperClassName : "mutation-button"}>
|
||||||
{(showError && targetsThisButton && result.error) &&
|
{(showError && targetsThisButton && result.error) &&
|
||||||
<Error error={result.error} />
|
<Error error={result.error} reset={result.reset} />
|
||||||
}
|
}
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
|
@ -27,6 +27,7 @@ import type {
|
||||||
HookOpts,
|
HookOpts,
|
||||||
FileFormInputHook,
|
FileFormInputHook,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { Error as ErrorC } from "../../components/error";
|
||||||
|
|
||||||
const _default = undefined;
|
const _default = undefined;
|
||||||
export default function useFileInput(
|
export default function useFileInput(
|
||||||
|
@ -41,6 +42,15 @@ export default function useFileInput(
|
||||||
const [imageURL, setImageURL] = useState<string>();
|
const [imageURL, setImageURL] = useState<string>();
|
||||||
const [info, setInfo] = useState<React.JSX.Element>();
|
const [info, setInfo] = useState<React.JSX.Element>();
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
if (imageURL) {
|
||||||
|
URL.revokeObjectURL(imageURL);
|
||||||
|
}
|
||||||
|
setImageURL(undefined);
|
||||||
|
setFile(undefined);
|
||||||
|
setInfo(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
function onChange(e: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const files = e.target.files;
|
const files = e.target.files;
|
||||||
if (!files) {
|
if (!files) {
|
||||||
|
@ -59,25 +69,18 @@ export default function useFileInput(
|
||||||
setImageURL(URL.createObjectURL(file));
|
setImageURL(URL.createObjectURL(file));
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = prettierBytes(file.size);
|
const sizePrettier = prettierBytes(file.size);
|
||||||
if (maxSize && file.size > maxSize) {
|
if (maxSize && file.size > maxSize) {
|
||||||
size = <span className="error-text">{size}</span>;
|
const maxSizePrettier = prettierBytes(maxSize);
|
||||||
|
setInfo(
|
||||||
|
<ErrorC
|
||||||
|
error={new Error(`file size ${sizePrettier} is larger than max size ${maxSizePrettier}`)}
|
||||||
|
reset={(reset)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setInfo(<>{file.name} ({sizePrettier})</>);
|
||||||
}
|
}
|
||||||
|
|
||||||
setInfo(
|
|
||||||
<>
|
|
||||||
{file.name} ({size})
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function reset() {
|
|
||||||
if (imageURL) {
|
|
||||||
URL.revokeObjectURL(imageURL);
|
|
||||||
}
|
|
||||||
setImageURL(undefined);
|
|
||||||
setFile(undefined);
|
|
||||||
setInfo(undefined);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const infoComponent = (
|
const infoComponent = (
|
||||||
|
|
|
@ -257,33 +257,37 @@ input, select, textarea {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.with-dismiss {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.dismiss {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
align-items: center;
|
||||||
|
align-self: stretch;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.mutation-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messagebutton, .messagebutton > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
div.padded {
|
|
||||||
margin-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
button, .button {
|
|
||||||
white-space: nowrap;
|
|
||||||
margin-right: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.messagebutton > div {
|
|
||||||
button, .button {
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notImplemented {
|
.notImplemented {
|
||||||
border: 2px solid rgb(70, 79, 88);
|
border: 2px solid rgb(70, 79, 88);
|
||||||
background: repeating-linear-gradient(
|
background: repeating-linear-gradient(
|
||||||
|
@ -500,12 +504,29 @@ form {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field.file label {
|
.form-field.file {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
|
grid-template-rows: auto auto;
|
||||||
|
grid-template-areas:
|
||||||
|
"label-label label-label"
|
||||||
|
"label-button file-info"
|
||||||
|
;
|
||||||
|
|
||||||
|
.label-label {
|
||||||
|
grid-area: label-label;
|
||||||
|
}
|
||||||
|
|
||||||
.label {
|
.label-button {
|
||||||
grid-column: 1 / span 2;
|
grid-area: label-button;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-info {
|
||||||
|
grid-area: file-info;
|
||||||
|
.error {
|
||||||
|
padding: 0.1rem;
|
||||||
|
line-height: 1.4rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@
|
||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useMemo, useEffect } from "react";
|
import React, { useMemo, useEffect, ReactNode } from "react";
|
||||||
import { useFileInput, useComboBoxInput } from "../../../../lib/form";
|
import { useFileInput, useComboBoxInput } from "../../../../lib/form";
|
||||||
import useShortcode from "./use-shortcode";
|
import useShortcode from "./use-shortcode";
|
||||||
import useFormSubmit from "../../../../lib/form/submit";
|
import useFormSubmit from "../../../../lib/form/submit";
|
||||||
|
@ -27,52 +27,74 @@ import FakeToot from "../../../../components/fake-toot";
|
||||||
import MutationButton from "../../../../components/form/mutation-button";
|
import MutationButton from "../../../../components/form/mutation-button";
|
||||||
import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
|
import { useAddEmojiMutation } from "../../../../lib/query/admin/custom-emoji";
|
||||||
import { useInstanceV1Query } from "../../../../lib/query/gts-api";
|
import { useInstanceV1Query } from "../../../../lib/query/gts-api";
|
||||||
|
import prettierBytes from "prettier-bytes";
|
||||||
|
|
||||||
export default function NewEmojiForm() {
|
export default function NewEmojiForm() {
|
||||||
const shortcode = useShortcode();
|
|
||||||
|
|
||||||
const { data: instance } = useInstanceV1Query();
|
const { data: instance } = useInstanceV1Query();
|
||||||
const emojiMaxSize = useMemo(() => {
|
const emojiMaxSize = useMemo(() => {
|
||||||
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
|
return instance?.configuration?.emojis?.emoji_size_limit ?? 50 * 1024;
|
||||||
}, [instance]);
|
}, [instance]);
|
||||||
|
|
||||||
const image = useFileInput("image", {
|
const prettierMaxSize = useMemo(() => {
|
||||||
withPreview: true,
|
return prettierBytes(emojiMaxSize);
|
||||||
maxSize: emojiMaxSize
|
}, [emojiMaxSize]);
|
||||||
});
|
|
||||||
|
|
||||||
const category = useComboBoxInput("category");
|
const form = {
|
||||||
|
shortcode: useShortcode(),
|
||||||
|
image: useFileInput("image", {
|
||||||
|
withPreview: true,
|
||||||
|
maxSize: emojiMaxSize
|
||||||
|
}),
|
||||||
|
category: useComboBoxInput("category"),
|
||||||
|
};
|
||||||
|
|
||||||
const [submitForm, result] = useFormSubmit({
|
const [submitForm, result] = useFormSubmit(
|
||||||
shortcode, image, category
|
form,
|
||||||
}, useAddEmojiMutation());
|
useAddEmojiMutation(),
|
||||||
|
{
|
||||||
|
changedOnly: false,
|
||||||
|
// On submission, reset form values
|
||||||
|
// no matter what the result was.
|
||||||
|
onFinish: (_res) => {
|
||||||
|
form.shortcode.reset();
|
||||||
|
form.image.reset();
|
||||||
|
form.category.reset();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shortcode.value === undefined || shortcode.value.length == 0) {
|
// If shortcode has not been entered yet, but an image file
|
||||||
if (image.value != undefined) {
|
// has been submitted, suggest a shortcode based on filename.
|
||||||
let [name, _ext] = image.value.name.split(".");
|
if (
|
||||||
shortcode.setter(name);
|
(form.shortcode.value === undefined || form.shortcode.value.length === 0) &&
|
||||||
}
|
form.image.value !== undefined
|
||||||
|
) {
|
||||||
|
let [name, _ext] = form.image.value.name.split(".");
|
||||||
|
form.shortcode.setter(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* We explicitly don't want to have 'shortcode' as a dependency here
|
// We explicitly don't want to have 'shortcode' as a
|
||||||
because we only want to change the shortcode to the filename if the field is empty
|
// dependency here because we only want to change the
|
||||||
at the moment the file is selected, not some time after when the field is emptied
|
// shortcode to the filename if the field is empty at
|
||||||
*/
|
// the moment the file is selected, not some time after
|
||||||
/* eslint-disable-next-line react-hooks/exhaustive-deps */
|
// when the field is emptied.
|
||||||
}, [image.value]);
|
//
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [form.image.value]);
|
||||||
|
|
||||||
let emojiOrShortcode;
|
let emojiOrShortcode: ReactNode;
|
||||||
|
if (form.image.previewValue !== undefined) {
|
||||||
if (image.previewValue != undefined) {
|
emojiOrShortcode = (
|
||||||
emojiOrShortcode = <img
|
<img
|
||||||
className="emoji"
|
className="emoji"
|
||||||
src={image.previewValue}
|
src={form.image.previewValue}
|
||||||
title={`:${shortcode.value}:`}
|
title={`:${form.shortcode.value}:`}
|
||||||
alt={shortcode.value}
|
alt={form.shortcode.value}
|
||||||
/>;
|
/>
|
||||||
} else if (shortcode.value !== undefined && shortcode.value.length > 0) {
|
);
|
||||||
emojiOrShortcode = `:${shortcode.value}:`;
|
} else if (form.shortcode.value !== undefined && form.shortcode.value.length > 0) {
|
||||||
|
emojiOrShortcode = `:${form.shortcode.value}:`;
|
||||||
} else {
|
} else {
|
||||||
emojiOrShortcode = `:your_emoji_here:`;
|
emojiOrShortcode = `:your_emoji_here:`;
|
||||||
}
|
}
|
||||||
|
@ -87,22 +109,23 @@ export default function NewEmojiForm() {
|
||||||
|
|
||||||
<form onSubmit={submitForm} className="form-flex">
|
<form onSubmit={submitForm} className="form-flex">
|
||||||
<FileInput
|
<FileInput
|
||||||
field={image}
|
field={form.image}
|
||||||
|
label={`Image file: png, gif, or static webp; max size ${prettierMaxSize}`}
|
||||||
accept="image/png,image/gif,image/webp"
|
accept="image/png,image/gif,image/webp"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
field={shortcode}
|
field={form.shortcode}
|
||||||
label="Shortcode, must be unique among the instance's local emoji"
|
label="Shortcode, must be unique among the instance's local emoji"
|
||||||
|
{...{pattern: "^\\w{2,30}$"}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<CategorySelect
|
<CategorySelect
|
||||||
field={category}
|
field={form.category}
|
||||||
children={[]}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MutationButton
|
<MutationButton
|
||||||
disabled={image.previewValue === undefined}
|
disabled={form.image.previewValue === undefined || form.shortcode.value?.length === 0}
|
||||||
label="Upload emoji"
|
label="Upload emoji"
|
||||||
result={result}
|
result={result}
|
||||||
/>
|
/>
|
||||||
|
|
Loading…
Reference in New Issue