mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-10-31 22:40:01 +00:00
[feature] Implement Filter API v2 (#2936)
* Use correct entity name * We support server-side filters now * Document filter v1 methods that can throw a 409 * Validate v1 filter phrase as filter title * Always check v1 filter API status codes in tests * Document keyword minimum requirement on filter API v1 * Make it possible to specify filter keyword update columns per filter keyword * Implement v2 filter API * Fix lint and tests * Update Swagger spec * Fix filter update test * Update Swagger spec *correctly* * Update actual files Swagger spec was generated from * Remove keywords_attributes and statuses_attributes * Add test for serialization of empty filter * More helpful messages when object is owned by wrong account
This commit is contained in:
parent
4db596b8b9
commit
61a8d36255
66 changed files with 5601 additions and 55 deletions
|
@ -1053,7 +1053,7 @@ definitions:
|
|||
type: string
|
||||
x-go-name: Keyword
|
||||
whole_word:
|
||||
description: Should the filter consider word boundaries?
|
||||
description: Should the filter keyword consider word boundaries?
|
||||
example: true
|
||||
type: boolean
|
||||
x-go-name: WholeWord
|
||||
|
@ -5971,6 +5971,7 @@ paths:
|
|||
Sample: fnord
|
||||
in: formData
|
||||
maxLength: 40
|
||||
minLength: 1
|
||||
name: phrase
|
||||
required: true
|
||||
type: string
|
||||
|
@ -6031,6 +6032,8 @@ paths:
|
|||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate keyword)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
|
@ -6045,7 +6048,7 @@ paths:
|
|||
delete:
|
||||
operationId: filterV1Delete
|
||||
parameters:
|
||||
- description: ID of the list
|
||||
- description: ID of the filter
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
|
@ -6120,6 +6123,7 @@ paths:
|
|||
Sample: fnord
|
||||
in: formData
|
||||
maxLength: 40
|
||||
minLength: 1
|
||||
name: phrase
|
||||
required: true
|
||||
type: string
|
||||
|
@ -6180,6 +6184,8 @@ paths:
|
|||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate keyword)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
|
@ -8759,6 +8765,547 @@ paths:
|
|||
summary: View + page through known accounts according to given filters.
|
||||
tags:
|
||||
- admin
|
||||
/api/v2/filters:
|
||||
get:
|
||||
operationId: filtersV2Get
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filters.
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/filterV2'
|
||||
type: array
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get all filters for the authenticated account.
|
||||
tags:
|
||||
- filters
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
operationId: filterV2Post
|
||||
parameters:
|
||||
- description: |-
|
||||
The name of the filter.
|
||||
|
||||
Sample: illuminati nonsense
|
||||
in: formData
|
||||
maxLength: 200
|
||||
minLength: 1
|
||||
name: title
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: multi
|
||||
description: |-
|
||||
The contexts in which the filter should be applied.
|
||||
|
||||
Sample: home, public
|
||||
enum:
|
||||
- home
|
||||
- notifications
|
||||
- public
|
||||
- thread
|
||||
- account
|
||||
in: formData
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
name: context[]
|
||||
required: true
|
||||
type: array
|
||||
uniqueItems: true
|
||||
- description: |-
|
||||
Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
|
||||
Sample: 86400
|
||||
in: formData
|
||||
name: expires_in
|
||||
type: number
|
||||
- default: warn
|
||||
description: |-
|
||||
The action to be taken when a status matches this filter.
|
||||
|
||||
Sample: warn
|
||||
enum:
|
||||
- warn
|
||||
- hide
|
||||
in: formData
|
||||
name: filter_action
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: New filter.
|
||||
schema:
|
||||
$ref: '#/definitions/filterV2'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate title, keyword, or status)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Create a single filter.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/{id}:
|
||||
delete:
|
||||
operationId: filterV2Delete
|
||||
parameters:
|
||||
- description: ID of the filter
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: filter deleted
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Delete a single filter with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
get:
|
||||
operationId: filterV2Get
|
||||
parameters:
|
||||
- description: ID of the filter
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filter.
|
||||
schema:
|
||||
$ref: '#/definitions/filterV2'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get a single filter with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
description: |-
|
||||
Note that this is actually closer to a PATCH operation:
|
||||
only provided fields will be updated, and omitted fields will remain set to previous values.
|
||||
operationId: filterV2Put
|
||||
parameters:
|
||||
- description: ID of the filter.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: |-
|
||||
The name of the filter.
|
||||
|
||||
Sample: illuminati nonsense
|
||||
in: formData
|
||||
maxLength: 200
|
||||
minLength: 1
|
||||
name: title
|
||||
required: true
|
||||
type: string
|
||||
- collectionFormat: multi
|
||||
description: |-
|
||||
The contexts in which the filter should be applied.
|
||||
|
||||
Sample: home, public
|
||||
enum:
|
||||
- home
|
||||
- notifications
|
||||
- public
|
||||
- thread
|
||||
- account
|
||||
in: formData
|
||||
items:
|
||||
type: string
|
||||
minItems: 1
|
||||
name: context[]
|
||||
required: true
|
||||
type: array
|
||||
uniqueItems: true
|
||||
- description: |-
|
||||
Number of seconds from now that the filter should expire.
|
||||
|
||||
Sample: 86400
|
||||
in: formData
|
||||
name: expires_in
|
||||
type: number
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated filter.
|
||||
schema:
|
||||
$ref: '#/definitions/filterV2'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate title, keyword, or status)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Update a single filter with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/{id}/keywords:
|
||||
get:
|
||||
operationId: filterKeywordsGet
|
||||
parameters:
|
||||
- description: ID of the filter
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filter keywords.
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/filterKeyword'
|
||||
type: array
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get all filter keywords for a given filter.
|
||||
tags:
|
||||
- filters
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
operationId: filterKeywordPost
|
||||
parameters:
|
||||
- description: ID of the filter to add the filtered status to.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: |-
|
||||
The text to be filtered
|
||||
|
||||
Sample: fnord
|
||||
in: formData
|
||||
maxLength: 40
|
||||
minLength: 1
|
||||
name: keyword
|
||||
required: true
|
||||
type: string
|
||||
- default: false
|
||||
description: |-
|
||||
Should the filter consider word boundaries?
|
||||
|
||||
Sample: true
|
||||
in: formData
|
||||
name: whole_word
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: New filter keyword.
|
||||
schema:
|
||||
$ref: '#/definitions/filterKeyword'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate keyword)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Add a filter keyword to an existing filter.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/{id}/statuses:
|
||||
get:
|
||||
operationId: filterStatusesGet
|
||||
parameters:
|
||||
- description: ID of the filter
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filter statuses.
|
||||
schema:
|
||||
items:
|
||||
$ref: '#/definitions/filterStatus'
|
||||
type: array
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get all filter statuses for a given filter.
|
||||
tags:
|
||||
- filters
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
operationId: filterStatusPost
|
||||
parameters:
|
||||
- description: ID of the filter to add the filtered status to.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: |-
|
||||
The ID of the status to filter.
|
||||
|
||||
Sample: 01HXA2NE0K8T1C70K90E74GYD0
|
||||
in: formData
|
||||
name: status_id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: New filter status.
|
||||
schema:
|
||||
$ref: '#/definitions/filterStatus'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate status)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Add a filter status to an existing filter.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/keywords/{id}:
|
||||
get:
|
||||
operationId: filterKeywordGet
|
||||
parameters:
|
||||
- description: ID of the filter keyword
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filter keyword.
|
||||
schema:
|
||||
$ref: '#/definitions/filterKeyword'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get a single filter keyword with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/keywords{id}:
|
||||
put:
|
||||
consumes:
|
||||
- application/json
|
||||
- application/xml
|
||||
- application/x-www-form-urlencoded
|
||||
operationId: filterKeywordPut
|
||||
parameters:
|
||||
- description: ID of the filter keyword to update.
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
- description: |-
|
||||
The text to be filtered
|
||||
|
||||
Sample: fnord
|
||||
in: formData
|
||||
maxLength: 40
|
||||
minLength: 1
|
||||
name: keyword
|
||||
required: true
|
||||
type: string
|
||||
- description: |-
|
||||
Should the filter consider word boundaries?
|
||||
|
||||
Sample: true
|
||||
in: formData
|
||||
name: whole_word
|
||||
type: boolean
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Updated filter keyword.
|
||||
schema:
|
||||
$ref: '#/definitions/filterKeyword'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"409":
|
||||
description: conflict (duplicate keyword)
|
||||
"422":
|
||||
description: unprocessable content
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- write:filters
|
||||
summary: Update a single filter keyword with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/filters/statuses/{id}:
|
||||
get:
|
||||
operationId: filterStatusGet
|
||||
parameters:
|
||||
- description: ID of the filter status
|
||||
in: path
|
||||
name: id
|
||||
required: true
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: Requested filter status.
|
||||
schema:
|
||||
$ref: '#/definitions/filterStatus'
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- read:filters
|
||||
summary: Get a single filter status with the given ID.
|
||||
tags:
|
||||
- filters
|
||||
/api/v2/instance:
|
||||
get:
|
||||
operationId: instanceGetV2
|
||||
|
|
|
@ -31,6 +31,7 @@
|
|||
"github.com/superseriousbusiness/gotosocial/internal/api/client/favourites"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/featuredtags"
|
||||
filtersV1 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v1"
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/followrequests"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/lists"
|
||||
|
@ -67,6 +68,7 @@ type Client struct {
|
|||
favourites *favourites.Module // api/v1/favourites
|
||||
featuredTags *featuredtags.Module // api/v1/featured_tags
|
||||
filtersV1 *filtersV1.Module // api/v1/filters
|
||||
filtersV2 *filtersV2.Module // api/v2/filters
|
||||
followRequests *followrequests.Module // api/v1/follow_requests
|
||||
instance *instance.Module // api/v1/instance
|
||||
lists *lists.Module // api/v1/lists
|
||||
|
@ -111,6 +113,7 @@ func (c *Client) Route(r *router.Router, m ...gin.HandlerFunc) {
|
|||
c.favourites.Route(h)
|
||||
c.featuredTags.Route(h)
|
||||
c.filtersV1.Route(h)
|
||||
c.filtersV2.Route(h)
|
||||
c.followRequests.Route(h)
|
||||
c.instance.Route(h)
|
||||
c.lists.Route(h)
|
||||
|
@ -143,6 +146,7 @@ func NewClient(state *state.State, p *processing.Processor) *Client {
|
|||
favourites: favourites.New(p),
|
||||
featuredTags: featuredtags.New(p),
|
||||
filtersV1: filtersV1.New(p),
|
||||
filtersV2: filtersV2.New(p),
|
||||
followRequests: followrequests.New(p),
|
||||
instance: instance.New(p),
|
||||
lists: lists.New(p),
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the list
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
|
|
|
@ -66,6 +66,9 @@ func (suite *FiltersTestSuite) deleteFilter(
|
|||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
|
|
|
@ -68,6 +68,9 @@ func (suite *FiltersTestSuite) getFilter(
|
|||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
// The text to be filtered.
|
||||
//
|
||||
// Sample: fnord
|
||||
// minLength: 1
|
||||
// maxLength: 40
|
||||
// type: string
|
||||
// -
|
||||
|
@ -120,6 +121,8 @@
|
|||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate keyword)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
|
|
|
@ -94,6 +94,9 @@ func (suite *FiltersTestSuite) postFilter(
|
|||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
|
@ -226,14 +229,3 @@ func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
|||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// FUTURE: this should be removed once we support server-side filters.
|
||||
func (suite *FiltersTestSuite) TestPostFilterIrreversibleNotSupported() {
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
irreversible := true
|
||||
_, err := suite.postFilter(&phrase, &context, &irreversible, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
// The text to be filtered.
|
||||
//
|
||||
// Sample: fnord
|
||||
// minLength: 1
|
||||
// maxLength: 40
|
||||
// type: string
|
||||
// -
|
||||
|
@ -126,6 +127,8 @@
|
|||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate keyword)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
|
|
|
@ -97,6 +97,9 @@ func (suite *FiltersTestSuite) putFilter(
|
|||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
|
@ -238,16 +241,6 @@ func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
|||
}
|
||||
}
|
||||
|
||||
// FUTURE: this should be removed once we support server-side filters.
|
||||
func (suite *FiltersTestSuite) TestPutFilterIrreversibleNotSupported() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
irreversible := true
|
||||
_, err := suite.putFilter(id, nil, nil, &irreversible, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
phrase := "GNU/Linux"
|
||||
|
|
|
@ -64,6 +64,9 @@ func (suite *FiltersTestSuite) getFilters(
|
|||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
|
|
|
@ -31,6 +31,10 @@ func validateNormalizeCreateUpdateFilter(form *model.FilterCreateUpdateRequestV1
|
|||
if err := validate.FilterKeyword(form.Phrase); err != nil {
|
||||
return err
|
||||
}
|
||||
// For filter v1 forwards compatibility, the phrase is used as the title of a v2 filter, so it must pass that as well.
|
||||
if err := validate.FilterTitle(form.Phrase); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate.FilterContexts(form.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
80
internal/api/client/filters/v2/filter.go
Normal file
80
internal/api/client/filters/v2/filter.go
Normal file
|
@ -0,0 +1,80 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
)
|
||||
|
||||
const (
|
||||
// BasePath is the base path for serving the filters API, minus the 'api' prefix
|
||||
BasePath = "/v2/filters"
|
||||
// BasePathWithID is the base path with the ID key in it, for operations on an existing filter.
|
||||
BasePathWithID = BasePath + "/:" + apiutil.IDKey
|
||||
// FilterKeywordsPathWithID is the path for operations on an existing filter's keywords.
|
||||
FilterKeywordsPathWithID = BasePathWithID + "/keywords"
|
||||
// FilterStatusesPathWithID is the path for operations on an existing filter's statuses.
|
||||
FilterStatusesPathWithID = BasePathWithID + "/statuses"
|
||||
|
||||
// KeywordPath is the base path for operations on filter keywords that don't require a filter ID.
|
||||
KeywordPath = BasePath + "/keywords"
|
||||
// KeywordPathWithKeywordID is the path for operations on an existing filter keyword.
|
||||
KeywordPathWithKeywordID = KeywordPath + "/:" + apiutil.IDKey
|
||||
|
||||
// StatusPath is the base path for operations on filter statuses that don't require a filter ID.
|
||||
StatusPath = BasePath + "/statuses"
|
||||
// StatusPathWithStatusID is the path for operations on an existing filter status.
|
||||
StatusPathWithStatusID = StatusPath + "/:" + apiutil.IDKey
|
||||
)
|
||||
|
||||
// Module implements APIs for client-side aka "v1" filtering.
|
||||
type Module struct {
|
||||
processor *processing.Processor
|
||||
}
|
||||
|
||||
func New(processor *processing.Processor) *Module {
|
||||
return &Module{
|
||||
processor: processor,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Module) Route(attachHandler func(method string, path string, f ...gin.HandlerFunc) gin.IRoutes) {
|
||||
attachHandler(http.MethodGet, BasePath, m.FiltersGETHandler)
|
||||
|
||||
attachHandler(http.MethodPost, BasePath, m.FilterPOSTHandler)
|
||||
attachHandler(http.MethodGet, BasePathWithID, m.FilterGETHandler)
|
||||
attachHandler(http.MethodPut, BasePathWithID, m.FilterPUTHandler)
|
||||
attachHandler(http.MethodDelete, BasePathWithID, m.FilterDELETEHandler)
|
||||
|
||||
attachHandler(http.MethodGet, FilterKeywordsPathWithID, m.FilterKeywordsGETHandler)
|
||||
attachHandler(http.MethodPost, FilterKeywordsPathWithID, m.FilterKeywordPOSTHandler)
|
||||
|
||||
attachHandler(http.MethodGet, KeywordPathWithKeywordID, m.FilterKeywordGETHandler)
|
||||
attachHandler(http.MethodPut, KeywordPathWithKeywordID, m.FilterKeywordPUTHandler)
|
||||
attachHandler(http.MethodDelete, KeywordPathWithKeywordID, m.FilterKeywordDELETEHandler)
|
||||
|
||||
attachHandler(http.MethodGet, FilterStatusesPathWithID, m.FilterStatusesGETHandler)
|
||||
attachHandler(http.MethodPost, FilterStatusesPathWithID, m.FilterStatusPOSTHandler)
|
||||
|
||||
attachHandler(http.MethodGet, StatusPathWithStatusID, m.FilterStatusGETHandler)
|
||||
attachHandler(http.MethodDelete, StatusPathWithStatusID, m.FilterStatusDELETEHandler)
|
||||
}
|
118
internal/api/client/filters/v2/filter_test.go
Normal file
118
internal/api/client/filters/v2/filter_test.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/storage"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
type FiltersTestSuite struct {
|
||||
suite.Suite
|
||||
db db.DB
|
||||
storage *storage.Driver
|
||||
mediaManager *media.Manager
|
||||
federator *federation.Federator
|
||||
processor *processing.Processor
|
||||
emailSender email.Sender
|
||||
sentEmails map[string]string
|
||||
state state.State
|
||||
|
||||
// standard suite models
|
||||
testTokens map[string]*gtsmodel.Token
|
||||
testClients map[string]*gtsmodel.Client
|
||||
testApplications map[string]*gtsmodel.Application
|
||||
testUsers map[string]*gtsmodel.User
|
||||
testAccounts map[string]*gtsmodel.Account
|
||||
testStatuses map[string]*gtsmodel.Status
|
||||
testFilters map[string]*gtsmodel.Filter
|
||||
testFilterKeywords map[string]*gtsmodel.FilterKeyword
|
||||
testFilterStatuses map[string]*gtsmodel.FilterStatus
|
||||
|
||||
// module being tested
|
||||
filtersModule *filtersV2.Module
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) SetupSuite() {
|
||||
suite.testTokens = testrig.NewTestTokens()
|
||||
suite.testClients = testrig.NewTestClients()
|
||||
suite.testApplications = testrig.NewTestApplications()
|
||||
suite.testUsers = testrig.NewTestUsers()
|
||||
suite.testAccounts = testrig.NewTestAccounts()
|
||||
suite.testStatuses = testrig.NewTestStatuses()
|
||||
suite.testFilters = testrig.NewTestFilters()
|
||||
suite.testFilterKeywords = testrig.NewTestFilterKeywords()
|
||||
suite.testFilterStatuses = testrig.NewTestFilterStatuses()
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) SetupTest() {
|
||||
suite.state.Caches.Init()
|
||||
testrig.StartNoopWorkers(&suite.state)
|
||||
|
||||
testrig.InitTestConfig()
|
||||
config.Config(func(cfg *config.Configuration) {
|
||||
cfg.WebAssetBaseDir = "../../../../../web/assets/"
|
||||
cfg.WebTemplateBaseDir = "../../../../../web/templates/"
|
||||
})
|
||||
testrig.InitTestLog()
|
||||
|
||||
suite.db = testrig.NewTestDB(&suite.state)
|
||||
suite.state.DB = suite.db
|
||||
suite.storage = testrig.NewInMemoryStorage()
|
||||
suite.state.Storage = suite.storage
|
||||
|
||||
testrig.StartTimelines(
|
||||
&suite.state,
|
||||
visibility.NewFilter(&suite.state),
|
||||
typeutils.NewConverter(&suite.state),
|
||||
)
|
||||
|
||||
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
|
||||
suite.federator = testrig.NewTestFederator(&suite.state, testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../../testrig/media")), suite.mediaManager)
|
||||
suite.sentEmails = make(map[string]string)
|
||||
suite.emailSender = testrig.NewEmailSender("../../../../../web/template/", suite.sentEmails)
|
||||
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
|
||||
suite.filtersModule = filtersV2.New(suite.processor)
|
||||
|
||||
testrig.StandardDBSetup(suite.db, nil)
|
||||
testrig.StandardStorageSetup(suite.storage, "../../../../../testrig/media")
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TearDownTest() {
|
||||
testrig.StandardDBTeardown(suite.db)
|
||||
testrig.StandardStorageTeardown(suite.storage)
|
||||
testrig.StopWorkers(&suite.state)
|
||||
}
|
||||
|
||||
func TestFiltersTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(FiltersTestSuite))
|
||||
}
|
90
internal/api/client/filters/v2/filterdelete.go
Normal file
90
internal/api/client/filters/v2/filterdelete.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterDELETEHandler swagger:operation DELETE /api/v2/filters/{id} filterV2Delete
|
||||
//
|
||||
// Delete a single filter with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// description: filter deleted
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
errWithCode = m.processor.FiltersV2().Delete(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiutil.EmptyJSONObject)
|
||||
}
|
115
internal/api/client/filters/v2/filterdelete_test.go
Normal file
115
internal/api/client/filters/v2/filterdelete_test.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) deleteFilter(
|
||||
filterID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) error {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodDelete, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterDELETEHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
resp := &struct{}{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteFilter() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilter() {
|
||||
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
err := suite.deleteFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
93
internal/api/client/filters/v2/filterget.go
Normal file
93
internal/api/client/filters/v2/filterget.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterGETHandler swagger:operation GET /api/v2/filters/{id} filterV2Get
|
||||
//
|
||||
// Get a single filter with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: Requested filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV2"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().Get(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
133
internal/api/client/filters/v2/filterget_test.go
Normal file
133
internal/api/client/filters/v2/filterget_test.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilter(
|
||||
filterID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV2{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilter() {
|
||||
expectedFilter := suite.testFilters["local_account_1_filter_1"]
|
||||
|
||||
filter, err := suite.getFilter(expectedFilter.ID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.NotEmpty(filter)
|
||||
suite.Equal(expectedFilter.Action, typeutils.APIFilterActionToFilterAction(filter.FilterAction))
|
||||
suite.Equal(expectedFilter.ID, filter.ID)
|
||||
suite.Equal(expectedFilter.Title, filter.Title)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilter() {
|
||||
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||
|
||||
_, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
_, err := suite.getFilter(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Test that an empty filter with no keywords or statuses serializes the keywords and statuses arrays as empty arrays,
|
||||
// not as null values or entirely omitted fields.
|
||||
func (suite *FiltersTestSuite) TestGetEmptyFilter() {
|
||||
id := suite.testFilters["local_account_1_filter_4"].ID
|
||||
|
||||
_, err := suite.getFilter(id, http.StatusOK, `{"id":"01HZ55WWWP82WYP2A1BKWK8Y9Q","title":"empty filter with no keywords or statuses","context":["home","public"],"expires_at":null,"filter_action":"warn","keywords":[],"statuses":[]}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
54
internal/api/client/filters/v2/filterkeyworddelete.go
Normal file
54
internal/api/client/filters/v2/filterkeyworddelete.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
func (m *Module) FilterKeywordDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
errWithCode = m.processor.FiltersV2().KeywordDelete(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiutil.EmptyJSONObject)
|
||||
}
|
115
internal/api/client/filters/v2/filterkeyworddelete_test.go
Normal file
115
internal/api/client/filters/v2/filterkeyworddelete_test.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) deleteFilterKeyword(
|
||||
filterKeywordID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) error {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodDelete, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.KeywordPath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterKeywordDELETEHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
resp := &struct{}{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteFilterKeyword() {
|
||||
id := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
|
||||
err := suite.deleteFilterKeyword(id, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterKeyword() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
|
||||
err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteNonexistentFilterKeyword() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
err := suite.deleteFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
93
internal/api/client/filters/v2/filterkeywordget.go
Normal file
93
internal/api/client/filters/v2/filterkeywordget.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterKeywordGETHandler swagger:operation GET /api/v2/filters/keywords/{id} filterKeywordGet
|
||||
//
|
||||
// Get a single filter keyword with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter keyword
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterKeyword
|
||||
// description: Requested filter keyword.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterKeyword"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterKeywordGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().KeywordGet(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
122
internal/api/client/filters/v2/filterkeywordget_test.go
Normal file
122
internal/api/client/filters/v2/filterkeywordget_test.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilterKeyword(
|
||||
filterKeywordID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterKeyword, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.KeywordPath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterKeywordGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterKeyword{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilterKeyword() {
|
||||
expectedFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"]
|
||||
|
||||
filterKeyword, err := suite.getFilterKeyword(expectedFilterKeyword.ID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.NotEmpty(filterKeyword)
|
||||
suite.Equal(expectedFilterKeyword.ID, filterKeyword.ID)
|
||||
suite.Equal(expectedFilterKeyword.Keyword, filterKeyword.Keyword)
|
||||
suite.Equal(util.PtrValueOr(expectedFilterKeyword.WholeWord, false), filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterKeyword() {
|
||||
id := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
|
||||
_, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetNonexistentFilterKeyword() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
_, err := suite.getFilterKeyword(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
151
internal/api/client/filters/v2/filterkeywordpost.go
Normal file
151
internal/api/client/filters/v2/filterkeywordpost.go
Normal file
|
@ -0,0 +1,151 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
// FilterKeywordPOSTHandler swagger:operation POST /api/v2/filters/{id}/keywords filterKeywordPost
|
||||
//
|
||||
// Add a filter keyword to an existing filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// description: ID of the filter to add the filtered status to.
|
||||
// -
|
||||
// name: keyword
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The text to be filtered
|
||||
//
|
||||
// Sample: fnord
|
||||
// type: string
|
||||
// minLength: 1
|
||||
// maxLength: 40
|
||||
// -
|
||||
// name: whole_word
|
||||
// in: formData
|
||||
// description: |-
|
||||
// Should the filter consider word boundaries?
|
||||
//
|
||||
// Sample: true
|
||||
// type: boolean
|
||||
// default: false
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterKeyword
|
||||
// description: New filter keyword.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterKeyword"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate keyword)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterKeywordPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterKeywordCreateUpdateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateUpdateFilterKeyword(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().KeywordCreate(c.Request.Context(), authed.Account, filterID, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
|
||||
func validateNormalizeCreateUpdateFilterKeyword(form *apimodel.FilterKeywordCreateUpdateRequest) error {
|
||||
if err := validate.FilterKeyword(form.Keyword); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
form.WholeWord = util.Ptr(util.PtrValueOr(form.WholeWord, false))
|
||||
|
||||
return nil
|
||||
}
|
192
internal/api/client/filters/v2/filterkeywordpost_test.go
Normal file
192
internal/api/client/filters/v2/filterkeywordpost_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilterKeyword(
|
||||
filterID string,
|
||||
keyword *string,
|
||||
wholeWord *bool,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterKeyword, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID+"/keywords", nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if keyword != nil {
|
||||
ctx.Request.Form["keyword"] = []string{*keyword}
|
||||
}
|
||||
if wholeWord != nil {
|
||||
ctx.Request.Form["whole_word"] = []string{strconv.FormatBool(*wholeWord)}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterKeywordPOSTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterKeyword{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordFull() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
keyword := "fnords"
|
||||
wholeWord := true
|
||||
filterKeyword, err := suite.postFilterKeyword(filterID, &keyword, &wholeWord, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(keyword, filterKeyword.Keyword)
|
||||
suite.Equal(wholeWord, filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordFullJSON() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
requestJson := `{
|
||||
"keyword": "fnords",
|
||||
"whole_word": true
|
||||
}`
|
||||
filterKeyword, err := suite.postFilterKeyword(filterID, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("fnords", filterKeyword.Keyword)
|
||||
suite.True(filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordMinimal() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
keyword := "fnords"
|
||||
filterKeyword, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(keyword, filterKeyword.Keyword)
|
||||
suite.False(filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordEmptyKeyword() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
keyword := ""
|
||||
_, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter keyword must be provided, and must be no more than 40 chars"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordMissingKeyword() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
_, err := suite.postFilterKeyword(filterID, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter keyword must be provided, and must be no more than 40 chars"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Creating another filter keyword in the same filter with the same keyword should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordKeywordConflict() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
keyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].Keyword
|
||||
_, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusConflict, `{"error":"Conflict: duplicate keyword"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordAnotherAccountsFilter() {
|
||||
filterID := suite.testFilters["local_account_2_filter_1"].ID
|
||||
keyword := "fnords"
|
||||
_, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterKeywordNonexistentFilter() {
|
||||
filterID := "not_even_a_real_ULID"
|
||||
keyword := "fnords"
|
||||
_, err := suite.postFilterKeyword(filterID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
138
internal/api/client/filters/v2/filterkeywordput.go
Normal file
138
internal/api/client/filters/v2/filterkeywordput.go
Normal file
|
@ -0,0 +1,138 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterKeywordPUTHandler swagger:operation PUT /api/v2/filters/keywords{id} filterKeywordPut
|
||||
//
|
||||
// Update a single filter keyword with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// description: ID of the filter keyword to update.
|
||||
// -
|
||||
// name: keyword
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The text to be filtered
|
||||
//
|
||||
// Sample: fnord
|
||||
// type: string
|
||||
// minLength: 1
|
||||
// maxLength: 40
|
||||
// -
|
||||
// name: whole_word
|
||||
// in: formData
|
||||
// description: |-
|
||||
// Should the filter consider word boundaries?
|
||||
//
|
||||
// Sample: true
|
||||
// type: boolean
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterKeyword
|
||||
// description: Updated filter keyword.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterKeyword"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate keyword)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterKeywordPUTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterKeywordCreateUpdateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateUpdateFilterKeyword(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().KeywordUpdate(c.Request.Context(), authed.Account, id, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
192
internal/api/client/filters/v2/filterkeywordput_test.go
Normal file
192
internal/api/client/filters/v2/filterkeywordput_test.go
Normal file
|
@ -0,0 +1,192 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) putFilterKeyword(
|
||||
filterKeywordID string,
|
||||
keyword *string,
|
||||
wholeWord *bool,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterKeyword, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPut, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.KeywordPath+"/"+filterKeywordID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if keyword != nil {
|
||||
ctx.Request.Form["keyword"] = []string{*keyword}
|
||||
}
|
||||
if wholeWord != nil {
|
||||
ctx.Request.Form["whole_word"] = []string{strconv.FormatBool(*wholeWord)}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddParam("id", filterKeywordID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterKeywordPUTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterKeyword{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordFull() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
keyword := "fnords"
|
||||
wholeWord := true
|
||||
filterKeyword, err := suite.putFilterKeyword(filterKeywordID, &keyword, &wholeWord, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(keyword, filterKeyword.Keyword)
|
||||
suite.Equal(wholeWord, filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordFullJSON() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
requestJson := `{
|
||||
"keyword": "fnords",
|
||||
"whole_word": true
|
||||
}`
|
||||
filterKeyword, err := suite.putFilterKeyword(filterKeywordID, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("fnords", filterKeyword.Keyword)
|
||||
suite.True(filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordMinimal() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
keyword := "fnords"
|
||||
filterKeyword, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(keyword, filterKeyword.Keyword)
|
||||
suite.False(filterKeyword.WholeWord)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordEmptyKeyword() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
keyword := ""
|
||||
_, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter keyword must be provided, and must be no more than 40 chars"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordMissingKeyword() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"].ID
|
||||
_, err := suite.putFilterKeyword(filterKeywordID, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter keyword must be provided, and must be no more than 40 chars"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Changing our filter keyword to the same keyword as another filter keyword in the same filter should fail.
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordKeywordConflict() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_1_filter_2_keyword_1"].ID
|
||||
conflictingKeyword := suite.testFilterKeywords["local_account_1_filter_2_keyword_2"].Keyword
|
||||
_, err := suite.putFilterKeyword(filterKeywordID, &conflictingKeyword, nil, nil, http.StatusConflict, `{"error":"Conflict: duplicate keyword"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordAnotherAccountsFilterKeyword() {
|
||||
filterKeywordID := suite.testFilterKeywords["local_account_2_filter_1_keyword_1"].ID
|
||||
keyword := "fnord"
|
||||
_, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterKeywordNonexistentFilterKeyword() {
|
||||
filterKeywordID := "not_even_a_real_ULID"
|
||||
keyword := "fnord"
|
||||
_, err := suite.putFilterKeyword(filterKeywordID, &keyword, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
95
internal/api/client/filters/v2/filterkeywordsget.go
Normal file
95
internal/api/client/filters/v2/filterkeywordsget.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterKeywordsGETHandler swagger:operation GET /api/v2/filters/{id}/keywords filterKeywordsGet
|
||||
//
|
||||
// Get all filter keywords for a given filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterKeywords
|
||||
// description: Requested filter keywords.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/filterKeyword"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterKeywordsGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().KeywordsGetForFilterID(c.Request.Context(), authed.Account, filterID)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
117
internal/api/client/filters/v2/filterkeywordsget_test.go
Normal file
117
internal/api/client/filters/v2/filterkeywordsget_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilterKeywords(
|
||||
filterID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) ([]*apimodel.FilterKeyword, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID+"/keywords", nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterKeywordsGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := make([]*apimodel.FilterKeyword, 0)
|
||||
if err := json.Unmarshal(b, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilterKeywords() {
|
||||
// Collect the sets of filter keyword IDs we expect to see.
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
expectedFilterKeywordIDs := []string{}
|
||||
for _, filterKeyword := range suite.testFilterKeywords {
|
||||
if filterKeyword.FilterID == filterID {
|
||||
expectedFilterKeywordIDs = append(expectedFilterKeywordIDs, filterKeyword.ID)
|
||||
}
|
||||
}
|
||||
suite.NotEmpty(expectedFilterKeywordIDs)
|
||||
|
||||
// Fetch all filter keywords for the test filter.
|
||||
filterKeywords, err := suite.getFilterKeywords(filterID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotEmpty(filterKeywords)
|
||||
|
||||
// Check that we got the right ones.
|
||||
suite.Len(filterKeywords, len(expectedFilterKeywordIDs))
|
||||
actualFilterKeywordIDs := []string{}
|
||||
for _, filterKeyword := range filterKeywords {
|
||||
actualFilterKeywordIDs = append(actualFilterKeywordIDs, filterKeyword.ID)
|
||||
}
|
||||
suite.ElementsMatch(expectedFilterKeywordIDs, actualFilterKeywordIDs)
|
||||
}
|
202
internal/api/client/filters/v2/filterpost.go
Normal file
202
internal/api/client/filters/v2/filterpost.go
Normal file
|
@ -0,0 +1,202 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
// FilterPOSTHandler swagger:operation POST /api/v2/filters filterV2Post
|
||||
//
|
||||
// Create a single filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: title
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The name of the filter.
|
||||
//
|
||||
// Sample: illuminati nonsense
|
||||
// type: string
|
||||
// minLength: 1
|
||||
// maxLength: 200
|
||||
// -
|
||||
// name: context[]
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The contexts in which the filter should be applied.
|
||||
//
|
||||
// Sample: home, public
|
||||
// enum:
|
||||
// - home
|
||||
// - notifications
|
||||
// - public
|
||||
// - thread
|
||||
// - account
|
||||
// type: array
|
||||
// items:
|
||||
// type:
|
||||
// string
|
||||
// collectionFormat: multi
|
||||
// minItems: 1
|
||||
// uniqueItems: true
|
||||
// -
|
||||
// name: expires_in
|
||||
// in: formData
|
||||
// description: |-
|
||||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Sample: 86400
|
||||
// type: number
|
||||
// -
|
||||
// name: filter_action
|
||||
// in: formData
|
||||
// description: |-
|
||||
// The action to be taken when a status matches this filter.
|
||||
//
|
||||
// Sample: warn
|
||||
// type: string
|
||||
// enum:
|
||||
// - warn
|
||||
// - hide
|
||||
// default: warn
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: New filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV2"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate title, keyword, or status)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterCreateRequestV2{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeCreateFilter(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().Create(c.Request.Context(), authed.Account, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
|
||||
func validateNormalizeCreateFilter(form *apimodel.FilterCreateRequestV2) error {
|
||||
if err := validate.FilterTitle(form.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
action := util.PtrValueOr(form.FilterAction, apimodel.FilterActionWarn)
|
||||
if err := validate.FilterAction(action); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := validate.FilterContexts(form.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Apply defaults for missing fields.
|
||||
form.FilterAction = util.Ptr(action)
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
221
internal/api/client/filters/v2/filterpost_test.go
Normal file
221
internal/api/client/filters/v2/filterpost_test.go
Normal file
|
@ -0,0 +1,221 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilter(title *string, context *[]string, action *string, expiresIn *int, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if title != nil {
|
||||
ctx.Request.Form["title"] = []string{*title}
|
||||
}
|
||||
if context != nil {
|
||||
ctx.Request.Form["context[]"] = *context
|
||||
}
|
||||
if action != nil {
|
||||
ctx.Request.Form["filter_action"] = []string{*action}
|
||||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
}
|
||||
}
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterPOSTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV2{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterFull() {
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home", "public"}
|
||||
action := "warn"
|
||||
expiresIn := 86400
|
||||
filter, err := suite.postFilter(&title, &context, &action, &expiresIn, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(title, filter.Title)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(apimodel.FilterActionWarn, filter.FilterAction)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
suite.Empty(filter.Keywords)
|
||||
suite.Empty(filter.Statuses)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterFullJSON() {
|
||||
// Use a numeric literal with a fractional part to test the JSON-specific handling for non-integer "expires_in".
|
||||
requestJson := `{
|
||||
"title": "GNU/Linux",
|
||||
"context": ["home", "public"],
|
||||
"filter_action": "warn",
|
||||
"whole_word": true,
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.postFilter(nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("GNU/Linux", filter.Title)
|
||||
suite.ElementsMatch(
|
||||
[]apimodel.FilterContext{
|
||||
apimodel.FilterContextHome,
|
||||
apimodel.FilterContextPublic,
|
||||
},
|
||||
filter.Context,
|
||||
)
|
||||
suite.Equal(apimodel.FilterActionWarn, filter.FilterAction)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
suite.Empty(filter.Keywords)
|
||||
suite.Empty(filter.Statuses)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMinimal() {
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(title, filter.Title)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(apimodel.FilterActionWarn, filter.FilterAction)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
suite.Empty(filter.Keywords)
|
||||
suite.Empty(filter.Statuses)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterEmptyTitle() {
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingTitle() {
|
||||
context := []string{"home"}
|
||||
_, err := suite.postFilter(nil, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterEmptyContext() {
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.postFilter(&title, &context, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterMissingContext() {
|
||||
title := "GNU/Linux"
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Creating another filter with the same title should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterTitleConflict() {
|
||||
title := suite.testFilters["local_account_1_filter_1"].Title
|
||||
_, err := suite.postFilter(&title, nil, nil, nil, nil, http.StatusUnprocessableEntity, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
206
internal/api/client/filters/v2/filterput.go
Normal file
206
internal/api/client/filters/v2/filterput.go
Normal file
|
@ -0,0 +1,206 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
// FilterPUTHandler swagger:operation PUT /api/v2/filters/{id} filterV2Put
|
||||
//
|
||||
// Update a single filter with the given ID.
|
||||
// Note that this is actually closer to a PATCH operation:
|
||||
// only provided fields will be updated, and omitted fields will remain set to previous values.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// description: ID of the filter.
|
||||
// -
|
||||
// name: title
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The name of the filter.
|
||||
//
|
||||
// Sample: illuminati nonsense
|
||||
// type: string
|
||||
// minLength: 1
|
||||
// maxLength: 200
|
||||
// -
|
||||
// name: context[]
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The contexts in which the filter should be applied.
|
||||
//
|
||||
// Sample: home, public
|
||||
// enum:
|
||||
// - home
|
||||
// - notifications
|
||||
// - public
|
||||
// - thread
|
||||
// - account
|
||||
// type: array
|
||||
// items:
|
||||
// type:
|
||||
// string
|
||||
// collectionFormat: multi
|
||||
// minItems: 1
|
||||
// uniqueItems: true
|
||||
// -
|
||||
// name: expires_in
|
||||
// in: formData
|
||||
// description: |-
|
||||
// Number of seconds from now that the filter should expire.
|
||||
//
|
||||
// Sample: 86400
|
||||
// type: number
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filter
|
||||
// description: Updated filter.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterV2"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate title, keyword, or status)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterPUTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterUpdateRequestV2{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateNormalizeUpdateFilter(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().Update(c.Request.Context(), authed.Account, id, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
|
||||
func validateNormalizeUpdateFilter(form *apimodel.FilterUpdateRequestV2) error {
|
||||
if form.Title != nil {
|
||||
if err := validate.FilterTitle(*form.Title); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if form.FilterAction != nil {
|
||||
if err := validate.FilterAction(*form.FilterAction); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if form.Context != nil {
|
||||
if err := validate.FilterContexts(*form.Context); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize filter expiry if necessary.
|
||||
// If we parsed this as JSON, expires_in
|
||||
// may be either a float64 or a string.
|
||||
if ei := form.ExpiresInI; ei != nil {
|
||||
switch e := ei.(type) {
|
||||
case float64:
|
||||
form.ExpiresIn = util.Ptr(int(e))
|
||||
|
||||
case string:
|
||||
expiresIn, err := strconv.Atoi(e)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse expires_in value %s as integer: %w", e, err)
|
||||
}
|
||||
|
||||
form.ExpiresIn = &expiresIn
|
||||
|
||||
default:
|
||||
return fmt.Errorf("could not parse expires_in type %T as integer", ei)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
230
internal/api/client/filters/v2/filterput_test.go
Normal file
230
internal/api/client/filters/v2/filterput_test.go
Normal file
|
@ -0,0 +1,230 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) putFilter(filterID string, title *string, context *[]string, action *string, expiresIn *int, requestJson *string, expectedHTTPStatus int, expectedBody string) (*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPut, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if title != nil {
|
||||
ctx.Request.Form["title"] = []string{*title}
|
||||
}
|
||||
if context != nil {
|
||||
ctx.Request.Form["context[]"] = *context
|
||||
}
|
||||
if action != nil {
|
||||
ctx.Request.Form["filter_action"] = []string{*action}
|
||||
}
|
||||
if expiresIn != nil {
|
||||
ctx.Request.Form["expires_in"] = []string{strconv.Itoa(*expiresIn)}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterPUTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterV2{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterFull() {
|
||||
id := suite.testFilters["local_account_1_filter_2"].ID
|
||||
title := "messy synoptic varblabbles"
|
||||
context := []string{"home", "public"}
|
||||
action := "hide"
|
||||
expiresIn := 86400
|
||||
filter, err := suite.putFilter(id, &title, &context, &action, &expiresIn, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(title, filter.Title)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(apimodel.FilterActionHide, filter.FilterAction)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
suite.Len(filter.Keywords, 3)
|
||||
suite.Len(filter.Statuses, 0)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterFullJSON() {
|
||||
id := suite.testFilters["local_account_1_filter_2"].ID
|
||||
// Use a numeric literal with a fractional part to test the JSON-specific handling for non-integer "expires_in".
|
||||
requestJson := `{
|
||||
"title": "messy synoptic varblabbles",
|
||||
"context": ["home", "public"],
|
||||
"filter_action": "hide",
|
||||
"expires_in": 86400.1
|
||||
}`
|
||||
filter, err := suite.putFilter(id, nil, nil, nil, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal("messy synoptic varblabbles", filter.Title)
|
||||
suite.ElementsMatch(
|
||||
[]apimodel.FilterContext{
|
||||
apimodel.FilterContextHome,
|
||||
apimodel.FilterContextPublic,
|
||||
},
|
||||
filter.Context,
|
||||
)
|
||||
suite.Equal(apimodel.FilterActionHide, filter.FilterAction)
|
||||
if suite.NotNil(filter.ExpiresAt) {
|
||||
suite.NotEmpty(*filter.ExpiresAt)
|
||||
}
|
||||
suite.Len(filter.Keywords, 3)
|
||||
suite.Len(filter.Statuses, 0)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterMinimal() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
filter, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(title, filter.Title)
|
||||
filterContext := make([]string, 0, len(filter.Context))
|
||||
for _, c := range filter.Context {
|
||||
filterContext = append(filterContext, string(c))
|
||||
}
|
||||
suite.ElementsMatch(context, filterContext)
|
||||
suite.Equal(apimodel.FilterActionWarn, filter.FilterAction)
|
||||
suite.Nil(filter.ExpiresAt)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterEmptyTitle() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := ""
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: filter title must be provided, and must be no more than 200 chars"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutFilterEmptyContext() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: at least one filter context is required"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Changing our title to a title used by an existing filter should fail.
|
||||
func (suite *FiltersTestSuite) TestPutFilterTitleConflict() {
|
||||
id := suite.testFilters["local_account_1_filter_1"].ID
|
||||
title := suite.testFilters["local_account_1_filter_2"].Title
|
||||
_, err := suite.putFilter(id, &title, nil, nil, nil, nil, http.StatusConflict, `{"error":"Conflict: you already have a filter with this title"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutAnotherAccountsFilter() {
|
||||
id := suite.testFilters["local_account_2_filter_1"].ID
|
||||
title := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &title, &context, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPutNonexistentFilter() {
|
||||
id := "not_even_a_real_ULID"
|
||||
phrase := "GNU/Linux"
|
||||
context := []string{"home"}
|
||||
_, err := suite.putFilter(id, &phrase, &context, nil, nil, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
81
internal/api/client/filters/v2/filtersget.go
Normal file
81
internal/api/client/filters/v2/filtersget.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FiltersGETHandler swagger:operation GET /api/v2/filters filtersV2Get
|
||||
//
|
||||
// Get all filters for the authenticated account.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filters
|
||||
// description: Requested filters.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/filterV2"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilters, errWithCode := m.processor.FiltersV2().GetAll(c.Request.Context(), authed.Account)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilters)
|
||||
}
|
154
internal/api/client/filters/v2/filtersget_test.go
Normal file
154
internal/api/client/filters/v2/filtersget_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilters(
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) ([]*apimodel.FilterV2, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FiltersGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := make([]*apimodel.FilterV2, 0)
|
||||
if err := json.Unmarshal(b, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilters() {
|
||||
// Set of filter IDs for the test user.
|
||||
expectedFilterIDs := []string{}
|
||||
// Map of filter IDs to filter keyword and status IDs.
|
||||
expectedFilters := map[string]struct {
|
||||
keywordIDs []string
|
||||
statusIDs []string
|
||||
}{}
|
||||
|
||||
// Collect the sets of IDs we expect to see.
|
||||
accountID := suite.testAccounts["local_account_1"].ID
|
||||
for _, filter := range suite.testFilters {
|
||||
if filter.AccountID == accountID {
|
||||
expectedFilterIDs = append(expectedFilterIDs, filter.ID)
|
||||
expectedFilters[filter.ID] = struct {
|
||||
keywordIDs []string
|
||||
statusIDs []string
|
||||
}{}
|
||||
}
|
||||
}
|
||||
for _, filterKeyword := range suite.testFilterKeywords {
|
||||
if filterKeyword.AccountID == accountID {
|
||||
expectedIDsForFilter := expectedFilters[filterKeyword.FilterID]
|
||||
expectedIDsForFilter.keywordIDs = append(expectedIDsForFilter.keywordIDs, filterKeyword.ID)
|
||||
expectedFilters[filterKeyword.FilterID] = expectedIDsForFilter
|
||||
}
|
||||
}
|
||||
for _, filterStatus := range suite.testFilterStatuses {
|
||||
if filterStatus.AccountID == accountID {
|
||||
expectedIDsForFilter := expectedFilters[filterStatus.FilterID]
|
||||
expectedIDsForFilter.statusIDs = append(expectedIDsForFilter.statusIDs, filterStatus.ID)
|
||||
expectedFilters[filterStatus.FilterID] = expectedIDsForFilter
|
||||
}
|
||||
}
|
||||
suite.NotEmpty(expectedFilterIDs)
|
||||
suite.NotEmpty(expectedFilters)
|
||||
|
||||
// Fetch all filters for the logged-in account.
|
||||
filters, err := suite.getFilters(http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotEmpty(filters)
|
||||
|
||||
// Check that we got the right ones.
|
||||
suite.Len(filters, len(expectedFilters))
|
||||
|
||||
actualFilterIDs := []string{}
|
||||
for _, filter := range filters {
|
||||
actualFilterIDs = append(actualFilterIDs, filter.ID)
|
||||
|
||||
expectedIDsForFilter := expectedFilters[filter.ID]
|
||||
|
||||
actualFilterKeywordIDs := []string{}
|
||||
for _, filterKeyword := range filter.Keywords {
|
||||
actualFilterKeywordIDs = append(actualFilterKeywordIDs, filterKeyword.ID)
|
||||
}
|
||||
suite.ElementsMatch(actualFilterKeywordIDs, expectedIDsForFilter.keywordIDs)
|
||||
|
||||
actualFilterStatusIDs := []string{}
|
||||
for _, filterStatus := range filter.Statuses {
|
||||
actualFilterStatusIDs = append(actualFilterStatusIDs, filterStatus.ID)
|
||||
}
|
||||
suite.ElementsMatch(actualFilterStatusIDs, expectedIDsForFilter.statusIDs)
|
||||
}
|
||||
suite.ElementsMatch(expectedFilterIDs, actualFilterIDs)
|
||||
}
|
54
internal/api/client/filters/v2/filterstatusdelete.go
Normal file
54
internal/api/client/filters/v2/filterstatusdelete.go
Normal file
|
@ -0,0 +1,54 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
func (m *Module) FilterStatusDELETEHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
errWithCode = m.processor.FiltersV2().StatusDelete(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiutil.EmptyJSONObject)
|
||||
}
|
112
internal/api/client/filters/v2/filterstatusdelete_test.go
Normal file
112
internal/api/client/filters/v2/filterstatusdelete_test.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) deleteFilterStatus(
|
||||
filterStatusID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) error {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodDelete, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.StatusPath+"/"+filterStatusID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterStatusID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterDELETEHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return errs.Combine()
|
||||
}
|
||||
|
||||
resp := &struct{}{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteFilterStatus() {
|
||||
id := suite.testFilterStatuses["local_account_1_filter_3_status_1"].ID
|
||||
|
||||
err := suite.deleteFilterStatus(id, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteAnotherAccountsFilterStatus() {
|
||||
id := suite.testFilterStatuses["local_account_2_filter_1_status_1"].ID
|
||||
|
||||
err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestDeleteNonexistentFilterStatus() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
err := suite.deleteFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
95
internal/api/client/filters/v2/filterstatusesget.go
Normal file
95
internal/api/client/filters/v2/filterstatusesget.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterStatusesGETHandler swagger:operation GET /api/v2/filters/{id}/statuses filterStatusesGet
|
||||
//
|
||||
// Get all filter statuses for a given filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterStatuses
|
||||
// description: Requested filter statuses.
|
||||
// schema:
|
||||
// type: array
|
||||
// items:
|
||||
// "$ref": "#/definitions/filterStatus"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterStatusesGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().StatusesGetForFilterID(c.Request.Context(), authed.Account, filterID)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
117
internal/api/client/filters/v2/filterstatusesget_test.go
Normal file
117
internal/api/client/filters/v2/filterstatusesget_test.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilterStatuses(
|
||||
filterID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) ([]*apimodel.FilterStatus, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID+"/statuses", nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterStatusesGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := make([]*apimodel.FilterStatus, 0)
|
||||
if err := json.Unmarshal(b, &resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilterStatuses() {
|
||||
// Collect the sets of filter status IDs we expect to see.
|
||||
filterID := suite.testFilters["local_account_1_filter_3"].ID
|
||||
expectedFilterStatusIDs := []string{}
|
||||
for _, filterStatus := range suite.testFilterStatuses {
|
||||
if filterStatus.FilterID == filterID {
|
||||
expectedFilterStatusIDs = append(expectedFilterStatusIDs, filterStatus.ID)
|
||||
}
|
||||
}
|
||||
suite.NotEmpty(expectedFilterStatusIDs)
|
||||
|
||||
// Fetch all filter statuses for the test filter.
|
||||
filterStatuses, err := suite.getFilterStatuses(filterID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
suite.NotEmpty(filterStatuses)
|
||||
|
||||
// Check that we got the right ones.
|
||||
suite.Len(filterStatuses, len(expectedFilterStatusIDs))
|
||||
actualFilterStatusIDs := []string{}
|
||||
for _, filterStatus := range filterStatuses {
|
||||
actualFilterStatusIDs = append(actualFilterStatusIDs, filterStatus.ID)
|
||||
}
|
||||
suite.ElementsMatch(expectedFilterStatusIDs, actualFilterStatusIDs)
|
||||
}
|
93
internal/api/client/filters/v2/filterstatusget.go
Normal file
93
internal/api/client/filters/v2/filterstatusget.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// FilterStatusGETHandler swagger:operation GET /api/v2/filters/statuses/{id} filterStatusGet
|
||||
//
|
||||
// Get a single filter status with the given ID.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// type: string
|
||||
// description: ID of the filter status
|
||||
// in: path
|
||||
// required: true
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - read:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterStatus
|
||||
// description: Requested filter status.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterStatus"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterStatusGETHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
id, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().StatusGet(c.Request.Context(), authed.Account, id)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, apiFilter)
|
||||
}
|
120
internal/api/client/filters/v2/filterstatusget_test.go
Normal file
120
internal/api/client/filters/v2/filterstatusget_test.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) getFilterStatus(
|
||||
filterStatusID string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterStatus, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodGet, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.StatusPath+"/"+filterStatusID, nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
|
||||
ctx.AddParam("id", filterStatusID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterStatusGETHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterStatus{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetFilterStatus() {
|
||||
expectedFilterStatus := suite.testFilterStatuses["local_account_1_filter_3_status_1"]
|
||||
|
||||
filterStatus, err := suite.getFilterStatus(expectedFilterStatus.ID, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.NotEmpty(filterStatus)
|
||||
suite.Equal(expectedFilterStatus.ID, filterStatus.ID)
|
||||
suite.Equal(expectedFilterStatus.StatusID, filterStatus.StatusID)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetAnotherAccountsFilterStatus() {
|
||||
id := suite.testFilterStatuses["local_account_2_filter_1_status_1"].ID
|
||||
|
||||
_, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestGetNonexistentFilterStatus() {
|
||||
id := "not_even_a_real_ULID"
|
||||
|
||||
_, err := suite.getFilterStatus(id, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
133
internal/api/client/filters/v2/filterstatuspost.go
Normal file
133
internal/api/client/filters/v2/filterstatuspost.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||
)
|
||||
|
||||
// FilterStatusPOSTHandler swagger:operation POST /api/v2/filters/{id}/statuses filterStatusPost
|
||||
//
|
||||
// Add a filter status to an existing filter.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - filters
|
||||
//
|
||||
// consumes:
|
||||
// - application/json
|
||||
// - application/xml
|
||||
// - application/x-www-form-urlencoded
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: id
|
||||
// in: path
|
||||
// type: string
|
||||
// required: true
|
||||
// description: ID of the filter to add the filtered status to.
|
||||
// -
|
||||
// name: status_id
|
||||
// in: formData
|
||||
// required: true
|
||||
// description: |-
|
||||
// The ID of the status to filter.
|
||||
//
|
||||
// Sample: 01HXA2NE0K8T1C70K90E74GYD0
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - write:filters
|
||||
//
|
||||
// responses:
|
||||
// '200':
|
||||
// name: filterStatus
|
||||
// description: New filter status.
|
||||
// schema:
|
||||
// "$ref": "#/definitions/filterStatus"
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '409':
|
||||
// description: conflict (duplicate status)
|
||||
// '422':
|
||||
// description: unprocessable content
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) FilterStatusPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if authed.Account.IsMoving() {
|
||||
apiutil.ForbiddenAfterMove(c)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
filterID, errWithCode := apiutil.ParseID(c.Param(apiutil.IDKey))
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.FilterStatusCreateRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateCreateFilterStatus(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnprocessableEntity(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiFilter, errWithCode := m.processor.FiltersV2().StatusCreate(c.Request.Context(), authed.Account, filterID, form)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
apiutil.JSON(c, http.StatusOK, apiFilter)
|
||||
}
|
||||
|
||||
func validateCreateFilterStatus(form *apimodel.FilterStatusCreateRequest) error {
|
||||
return validate.ULID(form.StatusID, "status_id")
|
||||
}
|
180
internal/api/client/filters/v2/filterstatuspost_test.go
Normal file
180
internal/api/client/filters/v2/filterstatuspost_test.go
Normal file
|
@ -0,0 +1,180 @@
|
|||
// 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 v2_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
filtersV2 "github.com/superseriousbusiness/gotosocial/internal/api/client/filters/v2"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||
)
|
||||
|
||||
func (suite *FiltersTestSuite) postFilterStatus(
|
||||
filterID string,
|
||||
statusID *string,
|
||||
requestJson *string,
|
||||
expectedHTTPStatus int,
|
||||
expectedBody string,
|
||||
) (*apimodel.FilterStatus, error) {
|
||||
// instantiate recorder + test context
|
||||
recorder := httptest.NewRecorder()
|
||||
ctx, _ := testrig.CreateGinTestContext(recorder, nil)
|
||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||
|
||||
// create the request
|
||||
ctx.Request = httptest.NewRequest(http.MethodPost, config.GetProtocol()+"://"+config.GetHost()+"/api/"+filtersV2.BasePath+"/"+filterID+"/statuses", nil)
|
||||
ctx.Request.Header.Set("accept", "application/json")
|
||||
if requestJson != nil {
|
||||
ctx.Request.Header.Set("content-type", "application/json")
|
||||
ctx.Request.Body = io.NopCloser(strings.NewReader(*requestJson))
|
||||
} else {
|
||||
ctx.Request.Form = make(url.Values)
|
||||
if statusID != nil {
|
||||
ctx.Request.Form["status_id"] = []string{*statusID}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.AddParam("id", filterID)
|
||||
|
||||
// trigger the handler
|
||||
suite.filtersModule.FilterStatusPOSTHandler(ctx)
|
||||
|
||||
// read the response
|
||||
result := recorder.Result()
|
||||
defer result.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
errs := gtserror.NewMultiError(2)
|
||||
|
||||
// check code + body
|
||||
if resultCode := recorder.Code; expectedHTTPStatus != resultCode {
|
||||
errs.Appendf("expected %d got %d", expectedHTTPStatus, resultCode)
|
||||
if expectedBody == "" {
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
}
|
||||
|
||||
// if we got an expected body, return early
|
||||
if expectedBody != "" {
|
||||
if string(b) != expectedBody {
|
||||
errs.Appendf("expected %s got %s", expectedBody, string(b))
|
||||
}
|
||||
return nil, errs.Combine()
|
||||
}
|
||||
|
||||
resp := &apimodel.FilterStatus{}
|
||||
if err := json.Unmarshal(b, resp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatus() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
statusID := suite.testStatuses["admin_account_status_1"].ID
|
||||
filterStatus, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(statusID, filterStatus.StatusID)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusJSON() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
requestJson := `{
|
||||
"status_id": "01F8MH75CBF9JFX4ZAD54N0W0R"
|
||||
}`
|
||||
filterStatus, err := suite.postFilterStatus(filterID, nil, &requestJson, http.StatusOK, "")
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
|
||||
suite.Equal(suite.testStatuses["admin_account_status_1"].ID, filterStatus.StatusID)
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusEmptyStatusID() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
statusID := ""
|
||||
_, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: status_id must be provided"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusInvalidStatusID() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
statusID := "112401162517176488" // ma'am, that's clearly a Mastodon ID, this is a Wendy's
|
||||
_, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: status_id didn't match the expected ULID format for an ID (26 characters from the set 0123456789ABCDEFGHJKMNPQRSTVWXYZ)"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusMissingStatusID() {
|
||||
filterID := suite.testFilters["local_account_1_filter_1"].ID
|
||||
_, err := suite.postFilterStatus(filterID, nil, nil, http.StatusUnprocessableEntity, `{"error":"Unprocessable Entity: status_id must be provided"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// Creating another filter status in the same filter with the same status ID should fail.
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusStatusIDConflict() {
|
||||
filterID := suite.testFilters["local_account_1_filter_3"].ID
|
||||
statusID := suite.testFilterStatuses["local_account_1_filter_3_status_1"].StatusID
|
||||
_, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusConflict, `{"error":"Conflict: duplicate status"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusAnotherAccountsFilter() {
|
||||
filterID := suite.testFilters["local_account_2_filter_1"].ID
|
||||
statusID := suite.testStatuses["admin_account_status_1"].ID
|
||||
_, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *FiltersTestSuite) TestPostFilterStatusNonexistentFilter() {
|
||||
filterID := "not_even_a_real_ULID"
|
||||
statusID := suite.testStatuses["admin_account_status_1"].ID
|
||||
_, err := suite.postFilterStatus(filterID, &statusID, nil, http.StatusNotFound, `{"error":"Not Found"}`)
|
||||
if err != nil {
|
||||
suite.FailNow(err.Error())
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ type FilterKeyword struct {
|
|||
//
|
||||
// Example: fnord
|
||||
Keyword string `json:"keyword"`
|
||||
// Should the filter consider word boundaries?
|
||||
// Should the filter keyword consider word boundaries?
|
||||
//
|
||||
// Example: true
|
||||
WholeWord bool `json:"whole_word"`
|
||||
|
@ -104,3 +104,88 @@ type FilterStatus struct {
|
|||
// The status ID to be filtered.
|
||||
StatusID string `json:"phrase"`
|
||||
}
|
||||
|
||||
// FilterCreateRequestV2 captures params for creating a v2 filter.
|
||||
//
|
||||
// swagger:ignore
|
||||
type FilterCreateRequestV2 struct {
|
||||
// The name of the filter.
|
||||
//
|
||||
// Required: true
|
||||
// Example: fnord
|
||||
Title string `form:"title" json:"title" xml:"title"`
|
||||
// The contexts in which the filter should be applied.
|
||||
//
|
||||
// Required: true
|
||||
// Minimum length: 1
|
||||
// Unique: true
|
||||
// Enum: home,notifications,public,thread,account
|
||||
// Example: ["home", "public"]
|
||||
Context []FilterContext `form:"context[]" json:"context" xml:"context"`
|
||||
// The action to be taken when a status matches this filter. If omitted, defaults to warn.
|
||||
// Enum:
|
||||
// - warn
|
||||
// - hide
|
||||
// Example: warn
|
||||
FilterAction *FilterAction `form:"filter_action" json:"filter_action" xml:"filter_action"`
|
||||
|
||||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
ExpiresIn *int `json:"-" form:"expires_in" xml:"expires_in"`
|
||||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
}
|
||||
|
||||
// FilterKeywordCreateUpdateRequest captures params for creating or updating a filter keyword.
|
||||
//
|
||||
// swagger:ignore
|
||||
type FilterKeywordCreateUpdateRequest struct {
|
||||
// The text to be filtered.
|
||||
//
|
||||
// Example: fnord
|
||||
// Maximum length: 40
|
||||
Keyword string `form:"keyword" json:"keyword" xml:"keyword"`
|
||||
// Should the filter keyword consider word boundaries?
|
||||
//
|
||||
// Example: true
|
||||
WholeWord *bool `form:"whole_word" json:"whole_word" xml:"whole_word"`
|
||||
}
|
||||
|
||||
// FilterStatusCreateRequest captures params for creating a filter status.
|
||||
//
|
||||
// swagger:ignore
|
||||
type FilterStatusCreateRequest struct {
|
||||
// The status ID to be filtered.
|
||||
StatusID string `form:"status_id" json:"status_id" xml:"status_id"`
|
||||
}
|
||||
|
||||
// FilterUpdateRequestV2 captures params for creating a v2 filter.
|
||||
//
|
||||
// swagger:ignore
|
||||
type FilterUpdateRequestV2 struct {
|
||||
// The name of the filter.
|
||||
//
|
||||
// Example: illuminati nonsense
|
||||
Title *string `form:"title" json:"title" xml:"title"`
|
||||
// The contexts in which the filter should be applied.
|
||||
//
|
||||
// Minimum length: 1
|
||||
// Unique: true
|
||||
// Enum: home,notifications,public,thread,account
|
||||
// Example: ["home", "public"]
|
||||
Context *[]FilterContext `form:"context[]" json:"context" xml:"context"`
|
||||
// The action to be taken when a status matches this filter.
|
||||
// Enum:
|
||||
// - warn
|
||||
// - hide
|
||||
// Example: warn
|
||||
FilterAction *FilterAction `form:"filter_action" json:"filter_action" xml:"filter_action"`
|
||||
|
||||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
ExpiresIn *int `json:"-" form:"expires_in" xml:"expires_in"`
|
||||
// Number of seconds from now that the filter should expire. If omitted, filter never expires.
|
||||
//
|
||||
// Example: 86400
|
||||
ExpiresInI interface{} `json:"expires_in"`
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
|
@ -197,10 +198,14 @@ func (f *filterDB) UpdateFilter(
|
|||
ctx context.Context,
|
||||
filter *gtsmodel.Filter,
|
||||
filterColumns []string,
|
||||
filterKeywordColumns []string,
|
||||
filterKeywordColumns [][]string,
|
||||
deleteFilterKeywordIDs []string,
|
||||
deleteFilterStatusIDs []string,
|
||||
) error {
|
||||
if len(filter.Keywords) != len(filterKeywordColumns) {
|
||||
return errors.New("number of filter keywords must match number of lists of filter keyword columns")
|
||||
}
|
||||
|
||||
updatedAt := time.Now()
|
||||
filter.UpdatedAt = updatedAt
|
||||
for _, filterKeyword := range filter.Keywords {
|
||||
|
@ -214,8 +219,10 @@ func (f *filterDB) UpdateFilter(
|
|||
if len(filterColumns) > 0 {
|
||||
filterColumns = append(filterColumns, "updated_at")
|
||||
}
|
||||
if len(filterKeywordColumns) > 0 {
|
||||
filterKeywordColumns = append(filterKeywordColumns, "updated_at")
|
||||
for i := range filterKeywordColumns {
|
||||
if len(filterKeywordColumns[i]) > 0 {
|
||||
filterKeywordColumns[i] = append(filterKeywordColumns[i], "updated_at")
|
||||
}
|
||||
}
|
||||
|
||||
// Update database.
|
||||
|
@ -229,11 +236,11 @@ func (f *filterDB) UpdateFilter(
|
|||
return err
|
||||
}
|
||||
|
||||
if len(filter.Keywords) > 0 {
|
||||
for i, filterKeyword := range filter.Keywords {
|
||||
if _, err := NewUpsert(tx).
|
||||
Model(&filter.Keywords).
|
||||
Model(filterKeyword).
|
||||
Constraint("id").
|
||||
Column(filterKeywordColumns...).
|
||||
Column(filterKeywordColumns[i]...).
|
||||
Exec(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -127,7 +127,7 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
|
|||
}
|
||||
check.Statuses = append(check.Statuses, newStatus)
|
||||
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, nil, nil, nil); err != nil {
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{nil, nil}, nil, nil); err != nil {
|
||||
t.Fatalf("error updating filter: %v", err)
|
||||
}
|
||||
// Now fetch newly updated filter.
|
||||
|
@ -175,7 +175,7 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
|
|||
check.Keywords = []*gtsmodel.FilterKeyword{filterKeyword}
|
||||
check.Statuses = nil
|
||||
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, nil, []string{newKeyword.ID}, nil); err != nil {
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{{"whole_word"}}, []string{newKeyword.ID}, nil); err != nil {
|
||||
t.Fatalf("error updating filter: %v", err)
|
||||
}
|
||||
check, err = suite.db.GetFilterByID(ctx, filter.ID)
|
||||
|
@ -222,7 +222,7 @@ func (suite *FilterTestSuite) TestFilterCRUD() {
|
|||
StatusID: newStatus.StatusID,
|
||||
}
|
||||
check.Statuses = []*gtsmodel.FilterStatus{redundantStatus}
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, nil, nil, nil); err != nil {
|
||||
if err := suite.db.UpdateFilter(ctx, check, nil, [][]string{nil}, nil, nil); err != nil {
|
||||
t.Fatalf("error updating filter: %v", err)
|
||||
}
|
||||
check, err = suite.db.GetFilterByID(ctx, filter.ID)
|
||||
|
|
|
@ -42,11 +42,13 @@ type Filter interface {
|
|||
// and deletes indicated filter keywords and statuses by ID.
|
||||
// It uses a transaction to ensure no partial updates.
|
||||
// The column lists are optional; if not specified, all columns will be updated.
|
||||
// The filter keyword columns list is *per keyword*.
|
||||
// To update all keyword columns, provide a list where every element is an empty list.
|
||||
UpdateFilter(
|
||||
ctx context.Context,
|
||||
filter *gtsmodel.Filter,
|
||||
filterColumns []string,
|
||||
filterKeywordColumns []string,
|
||||
filterKeywordColumns [][]string,
|
||||
deleteFilterKeywordIDs []string,
|
||||
deleteFilterStatusIDs []string,
|
||||
) error
|
||||
|
|
|
@ -81,6 +81,8 @@ type FilterStatus struct {
|
|||
type FilterAction string
|
||||
|
||||
const (
|
||||
// FilterActionNone filters should not exist, except internally, for partially constructed or invalid filters.
|
||||
FilterActionNone FilterAction = ""
|
||||
// FilterActionWarn means that the status should be shown behind a warning.
|
||||
FilterActionWarn FilterAction = "warn"
|
||||
// FilterActionHide means that the status should be removed from timeline results entirely.
|
||||
|
|
|
@ -59,8 +59,8 @@ func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*a
|
|||
}
|
||||
|
||||
apiFilters := make([]*apimodel.FilterV1, 0, len(filters))
|
||||
for _, list := range filters {
|
||||
apiFilter, errWithCode := p.apiFilter(ctx, list)
|
||||
for _, filter := range filters {
|
||||
apiFilter, errWithCode := p.apiFilter(ctx, filter)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
|
|
@ -149,9 +149,11 @@ func (p *Processor) Update(
|
|||
"context_thread",
|
||||
"context_account",
|
||||
}
|
||||
filterKeywordColumns := []string{
|
||||
"keyword",
|
||||
"whole_word",
|
||||
filterKeywordColumns := [][]string{
|
||||
{
|
||||
"keyword",
|
||||
"whole_word",
|
||||
},
|
||||
}
|
||||
if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, filterKeywordColumns, nil, nil); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
|
|
38
internal/processing/filters/v2/convert.go
Normal file
38
internal/processing/filters/v2/convert.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// apiFilter is a shortcut to return the API v2 filter version of the given
|
||||
// filter, or return an appropriate error if conversion fails.
|
||||
func (p *Processor) apiFilter(ctx context.Context, filterKeyword *gtsmodel.Filter) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||
apiFilter, err := p.converter.FilterToAPIFilterV2(ctx, filterKeyword)
|
||||
if err != nil {
|
||||
return nil, gtserror.NewErrorInternalError(fmt.Errorf("error converting filter to API v2 filter: %w", err))
|
||||
}
|
||||
|
||||
return apiFilter, nil
|
||||
}
|
75
internal/processing/filters/v2/create.go
Normal file
75
internal/processing/filters/v2/create.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Create a new filter for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form *apimodel.FilterCreateRequestV2) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||
filter := >smodel.Filter{
|
||||
ID: id.NewULID(),
|
||||
AccountID: account.ID,
|
||||
Title: form.Title,
|
||||
Action: typeutils.APIFilterActionToFilterAction(*form.FilterAction),
|
||||
}
|
||||
if form.ExpiresIn != nil {
|
||||
filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
|
||||
}
|
||||
for _, context := range form.Context {
|
||||
switch context {
|
||||
case apimodel.FilterContextHome:
|
||||
filter.ContextHome = util.Ptr(true)
|
||||
case apimodel.FilterContextNotifications:
|
||||
filter.ContextNotifications = util.Ptr(true)
|
||||
case apimodel.FilterContextPublic:
|
||||
filter.ContextPublic = util.Ptr(true)
|
||||
case apimodel.FilterContextThread:
|
||||
filter.ContextThread = util.Ptr(true)
|
||||
case apimodel.FilterContextAccount:
|
||||
filter.ContextAccount = util.Ptr(true)
|
||||
default:
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(
|
||||
fmt.Errorf("unsupported filter context '%s'", context),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.state.DB.PutFilter(ctx, filter); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("duplicate title, keyword, or status")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.apiFilter(ctx, filter)
|
||||
}
|
53
internal/processing/filters/v2/delete.go
Normal file
53
internal/processing/filters/v2/delete.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Delete an existing filter and all its attached keywords and statuses for the given account.
|
||||
func (p *Processor) Delete(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
filterID string,
|
||||
) gtserror.WithCode {
|
||||
// Get the filter for this keyword.
|
||||
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// Check that the account owns it.
|
||||
if filter.AccountID != account.ID {
|
||||
return gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete the entire filter.
|
||||
if err := p.state.DB.DeleteFilterByID(ctx, filter.ID); err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
35
internal/processing/filters/v2/filters.go
Normal file
35
internal/processing/filters/v2/filters.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
state *state.State
|
||||
converter *typeutils.Converter
|
||||
}
|
||||
|
||||
func New(state *state.State, converter *typeutils.Converter) Processor {
|
||||
return Processor{
|
||||
state: state,
|
||||
converter: converter,
|
||||
}
|
||||
}
|
81
internal/processing/filters/v2/get.go
Normal file
81
internal/processing/filters/v2/get.go
Normal file
|
@ -0,0 +1,81 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// Get looks up a filter by ID and returns it with keywords and statuses.
|
||||
func (p *Processor) Get(ctx context.Context, account *gtsmodel.Account, filterID string) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
return p.apiFilter(ctx, filter)
|
||||
}
|
||||
|
||||
// GetAll looks up all filters for the current account and returns them with keywords and statuses.
|
||||
func (p *Processor) GetAll(ctx context.Context, account *gtsmodel.Account) ([]*apimodel.FilterV2, gtserror.WithCode) {
|
||||
filters, err := p.state.DB.GetFiltersForAccountID(
|
||||
ctx,
|
||||
account.ID,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
apiFilters := make([]*apimodel.FilterV2, 0, len(filters))
|
||||
for _, filter := range filters {
|
||||
apiFilter, errWithCode := p.apiFilter(ctx, filter)
|
||||
if errWithCode != nil {
|
||||
return nil, errWithCode
|
||||
}
|
||||
|
||||
apiFilters = append(apiFilters, apiFilter)
|
||||
}
|
||||
|
||||
// Sort them by ID so that they're in a stable order.
|
||||
// Clients may opt to sort them lexically in a locale-aware manner.
|
||||
slices.SortFunc(apiFilters, func(lhs *apimodel.FilterV2, rhs *apimodel.FilterV2) int {
|
||||
return strings.Compare(lhs.ID, rhs.ID)
|
||||
})
|
||||
|
||||
return apiFilters, nil
|
||||
}
|
67
internal/processing/filters/v2/keywordcreate.go
Normal file
67
internal/processing/filters/v2/keywordcreate.go
Normal file
|
@ -0,0 +1,67 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
// KeywordCreate adds a filter keyword to an existing filter for the given account, using the provided parameters.
|
||||
// These params should have already been normalized and validated by the time they reach this function.
|
||||
func (p *Processor) KeywordCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterKeywordCreateUpdateRequest) (*apimodel.FilterKeyword, gtserror.WithCode) {
|
||||
// Check that the filter is owned by the given account.
|
||||
filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
filterKeyword := >smodel.FilterKeyword{
|
||||
ID: id.NewULID(),
|
||||
AccountID: account.ID,
|
||||
FilterID: filter.ID,
|
||||
Keyword: form.Keyword,
|
||||
WholeWord: form.WholeWord,
|
||||
}
|
||||
|
||||
if err := p.state.DB.PutFilterKeyword(ctx, filterKeyword); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("duplicate keyword")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
|
||||
}
|
53
internal/processing/filters/v2/keyworddelete.go
Normal file
53
internal/processing/filters/v2/keyworddelete.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// KeywordDelete deletes an existing filter keyword from a filter.
|
||||
func (p *Processor) KeywordDelete(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
filterID string,
|
||||
) gtserror.WithCode {
|
||||
// Get the filter keyword.
|
||||
filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterID)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// Check that the account owns it.
|
||||
if filterKeyword.AccountID != account.ID {
|
||||
return gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete the filter keyword.
|
||||
if err := p.state.DB.DeleteFilterKeywordByID(ctx, filterKeyword.ID); err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
internal/processing/filters/v2/keywordget.go
Normal file
89
internal/processing/filters/v2/keywordget.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// KeywordGet looks up a filter keyword by ID.
|
||||
func (p *Processor) KeywordGet(ctx context.Context, account *gtsmodel.Account, filterKeywordID string) (*apimodel.FilterKeyword, gtserror.WithCode) {
|
||||
filterKeyword, err := p.state.DB.GetFilterKeywordByID(ctx, filterKeywordID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filterKeyword.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
|
||||
}
|
||||
|
||||
// KeywordsGetForFilterID looks up all filter keywords for the given filter.
|
||||
func (p *Processor) KeywordsGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterKeyword, gtserror.WithCode) {
|
||||
// Check that the filter is owned by the given account.
|
||||
filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(nil)
|
||||
}
|
||||
|
||||
filterKeywords, err := p.state.DB.GetFilterKeywordsForFilterID(
|
||||
ctx,
|
||||
filter.ID,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
apiFilterKeywords := make([]*apimodel.FilterKeyword, 0, len(filterKeywords))
|
||||
for _, filterKeyword := range filterKeywords {
|
||||
apiFilterKeywords = append(apiFilterKeywords, p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
|
||||
}
|
||||
|
||||
// Sort them by ID so that they're in a stable order.
|
||||
// Clients may opt to sort them lexically in a locale-aware manner.
|
||||
slices.SortFunc(apiFilterKeywords, func(lhs *apimodel.FilterKeyword, rhs *apimodel.FilterKeyword) int {
|
||||
return strings.Compare(lhs.ID, rhs.ID)
|
||||
})
|
||||
|
||||
return apiFilterKeywords, nil
|
||||
}
|
66
internal/processing/filters/v2/keywordupdate.go
Normal file
66
internal/processing/filters/v2/keywordupdate.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// KeywordUpdate updates an existing filter keyword for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) KeywordUpdate(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
filterKeywordID string,
|
||||
form *apimodel.FilterKeywordCreateUpdateRequest,
|
||||
) (*apimodel.FilterKeyword, gtserror.WithCode) {
|
||||
// Get the filter keyword by ID.
|
||||
filterKeyword, err := p.state.DB.GetFilterKeywordByID(gtscontext.SetBarebones(ctx), filterKeywordID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filterKeyword.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter keyword %s doesn't belong to account %s", filterKeyword.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
filterKeyword.Keyword = form.Keyword
|
||||
filterKeyword.WholeWord = form.WholeWord
|
||||
|
||||
if err := p.state.DB.UpdateFilterKeyword(ctx, filterKeyword, "keyword", "whole_word"); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("duplicate keyword")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.converter.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword), nil
|
||||
}
|
66
internal/processing/filters/v2/statuscreate.go
Normal file
66
internal/processing/filters/v2/statuscreate.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/id"
|
||||
)
|
||||
|
||||
// StatusCreate adds a filter status to an existing filter for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) StatusCreate(ctx context.Context, account *gtsmodel.Account, filterID string, form *apimodel.FilterStatusCreateRequest) (*apimodel.FilterStatus, gtserror.WithCode) {
|
||||
// Check that the filter is owned by the given account.
|
||||
filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
filterStatus := >smodel.FilterStatus{
|
||||
ID: id.NewULID(),
|
||||
AccountID: account.ID,
|
||||
FilterID: filter.ID,
|
||||
StatusID: form.StatusID,
|
||||
}
|
||||
|
||||
if err := p.state.DB.PutFilterStatus(ctx, filterStatus); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("duplicate status")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
|
||||
}
|
53
internal/processing/filters/v2/statusdelete.go
Normal file
53
internal/processing/filters/v2/statusdelete.go
Normal file
|
@ -0,0 +1,53 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// StatusDelete deletes an existing filter status from a filter.
|
||||
func (p *Processor) StatusDelete(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
filterID string,
|
||||
) gtserror.WithCode {
|
||||
// Get the filter status.
|
||||
filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterID)
|
||||
if err != nil {
|
||||
return gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
|
||||
// Check that the account owns it.
|
||||
if filterStatus.AccountID != account.ID {
|
||||
return gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
// Delete the filter status.
|
||||
if err := p.state.DB.DeleteFilterStatusByID(ctx, filterStatus.ID); err != nil {
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
89
internal/processing/filters/v2/statusget.go
Normal file
89
internal/processing/filters/v2/statusget.go
Normal file
|
@ -0,0 +1,89 @@
|
|||
// GoToSocial
|
||||
// Copyright (C) GoToSocial Authors admin@gotosocial.org
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
)
|
||||
|
||||
// StatusGet looks up a filter status by ID.
|
||||
func (p *Processor) StatusGet(ctx context.Context, account *gtsmodel.Account, filterStatusID string) (*apimodel.FilterStatus, gtserror.WithCode) {
|
||||
filterStatus, err := p.state.DB.GetFilterStatusByID(ctx, filterStatusID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filterStatus.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter status %s doesn't belong to account %s", filterStatus.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
return p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus), nil
|
||||
}
|
||||
|
||||
// StatusesGetForFilterID looks up all filter statuses for the given filter.
|
||||
func (p *Processor) StatusesGetForFilterID(ctx context.Context, account *gtsmodel.Account, filterID string) ([]*apimodel.FilterStatus, gtserror.WithCode) {
|
||||
// Check that the filter is owned by the given account.
|
||||
filter, err := p.state.DB.GetFilterByID(gtscontext.SetBarebones(ctx), filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(nil)
|
||||
}
|
||||
|
||||
filterStatuses, err := p.state.DB.GetFilterStatusesForFilterID(
|
||||
ctx,
|
||||
filter.ID,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
apiFilterStatuses := make([]*apimodel.FilterStatus, 0, len(filterStatuses))
|
||||
for _, filterStatus := range filterStatuses {
|
||||
apiFilterStatuses = append(apiFilterStatuses, p.converter.FilterStatusToAPIFilterStatus(ctx, filterStatus))
|
||||
}
|
||||
|
||||
// Sort them by ID so that they're in a stable order.
|
||||
// Clients may opt to sort them by status ID instead.
|
||||
slices.SortFunc(apiFilterStatuses, func(lhs *apimodel.FilterStatus, rhs *apimodel.FilterStatus) int {
|
||||
return strings.Compare(lhs.ID, rhs.ID)
|
||||
})
|
||||
|
||||
return apiFilterStatuses, nil
|
||||
}
|
125
internal/processing/filters/v2/update.go
Normal file
125
internal/processing/filters/v2/update.go
Normal file
|
@ -0,0 +1,125 @@
|
|||
// 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 v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||
)
|
||||
|
||||
// Update an existing filter for the given account, using the provided parameters.
|
||||
// These params should have already been validated by the time they reach this function.
|
||||
func (p *Processor) Update(
|
||||
ctx context.Context,
|
||||
account *gtsmodel.Account,
|
||||
filterID string,
|
||||
form *apimodel.FilterUpdateRequestV2,
|
||||
) (*apimodel.FilterV2, gtserror.WithCode) {
|
||||
// Get the filter by ID, with existing keywords and statuses.
|
||||
filter, err := p.state.DB.GetFilterByID(ctx, filterID)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.ErrNoEntries) {
|
||||
return nil, gtserror.NewErrorNotFound(err)
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
if filter.AccountID != account.ID {
|
||||
return nil, gtserror.NewErrorNotFound(
|
||||
fmt.Errorf("filter %s doesn't belong to account %s", filter.ID, account.ID),
|
||||
)
|
||||
}
|
||||
|
||||
// Filter columns that we're going to update.
|
||||
filterColumns := []string{}
|
||||
|
||||
// Apply filter changes.
|
||||
if form.Title != nil {
|
||||
filterColumns = append(filterColumns, "title")
|
||||
filter.Title = *form.Title
|
||||
}
|
||||
if form.FilterAction != nil {
|
||||
filterColumns = append(filterColumns, "action")
|
||||
filter.Action = typeutils.APIFilterActionToFilterAction(*form.FilterAction)
|
||||
}
|
||||
// TODO: (Vyr) is it possible to unset a filter expiration with this API?
|
||||
if form.ExpiresIn != nil {
|
||||
filterColumns = append(filterColumns, "expires_at")
|
||||
filter.ExpiresAt = time.Now().Add(time.Second * time.Duration(*form.ExpiresIn))
|
||||
}
|
||||
if form.Context != nil {
|
||||
filterColumns = append(filterColumns,
|
||||
"context_home",
|
||||
"context_notifications",
|
||||
"context_public",
|
||||
"context_thread",
|
||||
"context_account",
|
||||
)
|
||||
filter.ContextHome = util.Ptr(false)
|
||||
filter.ContextNotifications = util.Ptr(false)
|
||||
filter.ContextPublic = util.Ptr(false)
|
||||
filter.ContextThread = util.Ptr(false)
|
||||
filter.ContextAccount = util.Ptr(false)
|
||||
for _, context := range *form.Context {
|
||||
switch context {
|
||||
case apimodel.FilterContextHome:
|
||||
filter.ContextHome = util.Ptr(true)
|
||||
case apimodel.FilterContextNotifications:
|
||||
filter.ContextNotifications = util.Ptr(true)
|
||||
case apimodel.FilterContextPublic:
|
||||
filter.ContextPublic = util.Ptr(true)
|
||||
case apimodel.FilterContextThread:
|
||||
filter.ContextThread = util.Ptr(true)
|
||||
case apimodel.FilterContextAccount:
|
||||
filter.ContextAccount = util.Ptr(true)
|
||||
default:
|
||||
return nil, gtserror.NewErrorUnprocessableEntity(
|
||||
fmt.Errorf("unsupported filter context '%s'", context),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Temporarily detach keywords and statuses from filter, since we're not updating them below.
|
||||
filterKeywords := filter.Keywords
|
||||
filterStatuses := filter.Statuses
|
||||
filter.Keywords = nil
|
||||
filter.Statuses = nil
|
||||
|
||||
if err := p.state.DB.UpdateFilter(ctx, filter, filterColumns, nil, nil, nil); err != nil {
|
||||
if errors.Is(err, db.ErrAlreadyExists) {
|
||||
err = errors.New("you already have a filter with this title")
|
||||
return nil, gtserror.NewErrorConflict(err, err.Error())
|
||||
}
|
||||
return nil, gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
// Re-attach keywords and statuses before returning.
|
||||
filter.Keywords = filterKeywords
|
||||
filter.Statuses = filterStatuses
|
||||
|
||||
return p.apiFilter(ctx, filter)
|
||||
}
|
|
@ -30,6 +30,7 @@
|
|||
"github.com/superseriousbusiness/gotosocial/internal/processing/common"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/fedi"
|
||||
filtersv1 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v1"
|
||||
filtersv2 "github.com/superseriousbusiness/gotosocial/internal/processing/filters/v2"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/list"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/markers"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/processing/media"
|
||||
|
@ -73,6 +74,7 @@ type Processor struct {
|
|||
admin admin.Processor
|
||||
fedi fedi.Processor
|
||||
filtersv1 filtersv1.Processor
|
||||
filtersv2 filtersv2.Processor
|
||||
list list.Processor
|
||||
markers markers.Processor
|
||||
media media.Processor
|
||||
|
@ -102,6 +104,10 @@ func (p *Processor) FiltersV1() *filtersv1.Processor {
|
|||
return &p.filtersv1
|
||||
}
|
||||
|
||||
func (p *Processor) FiltersV2() *filtersv2.Processor {
|
||||
return &p.filtersv2
|
||||
}
|
||||
|
||||
func (p *Processor) List() *list.Processor {
|
||||
return &p.list
|
||||
}
|
||||
|
@ -184,6 +190,7 @@ func NewProcessor(
|
|||
processor.admin = admin.New(state, cleaner, converter, mediaManager, federator.TransportController(), emailSender)
|
||||
processor.fedi = fedi.New(state, &common, converter, federator, filter)
|
||||
processor.filtersv1 = filtersv1.New(state, converter)
|
||||
processor.filtersv2 = filtersv2.New(state, converter)
|
||||
processor.list = list.New(state, converter)
|
||||
processor.markers = markers.New(state, converter)
|
||||
processor.polls = polls.New(&common, state, converter)
|
||||
|
|
|
@ -47,3 +47,13 @@ func APIMarkerNameToMarkerName(m apimodel.MarkerName) gtsmodel.MarkerName {
|
|||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func APIFilterActionToFilterAction(m apimodel.FilterAction) gtsmodel.FilterAction {
|
||||
switch m {
|
||||
case apimodel.FilterActionWarn:
|
||||
return gtsmodel.FilterActionWarn
|
||||
case apimodel.FilterActionHide:
|
||||
return gtsmodel.FilterActionHide
|
||||
}
|
||||
return gtsmodel.FilterActionNone
|
||||
}
|
||||
|
|
|
@ -1852,19 +1852,12 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor
|
|||
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),
|
||||
})
|
||||
apiFilterKeywords = append(apiFilterKeywords, *c.FilterKeywordToAPIFilterKeyword(ctx, filterKeyword))
|
||||
}
|
||||
|
||||
apiFilterStatuses := make([]apimodel.FilterStatus, 0, len(filter.Keywords))
|
||||
for _, filterStatus := range filter.Statuses {
|
||||
apiFilterStatuses = append(apiFilterStatuses, apimodel.FilterStatus{
|
||||
ID: filterStatus.ID,
|
||||
StatusID: filterStatus.StatusID,
|
||||
})
|
||||
apiFilterStatuses = append(apiFilterStatuses, *c.FilterStatusToAPIFilterStatus(ctx, filterStatus))
|
||||
}
|
||||
|
||||
return &apimodel.FilterV2{
|
||||
|
@ -1915,6 +1908,23 @@ func filterActionToAPIFilterAction(m gtsmodel.FilterAction) apimodel.FilterActio
|
|||
return apimodel.FilterActionNone
|
||||
}
|
||||
|
||||
// FilterKeywordToAPIFilterKeyword converts a GTS model filter status into an API filter status.
|
||||
func (c *Converter) FilterKeywordToAPIFilterKeyword(ctx context.Context, filterKeyword *gtsmodel.FilterKeyword) *apimodel.FilterKeyword {
|
||||
return &apimodel.FilterKeyword{
|
||||
ID: filterKeyword.ID,
|
||||
Keyword: filterKeyword.Keyword,
|
||||
WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false),
|
||||
}
|
||||
}
|
||||
|
||||
// FilterStatusToAPIFilterStatus converts a GTS model filter status into an API filter status.
|
||||
func (c *Converter) FilterStatusToAPIFilterStatus(ctx context.Context, filterStatus *gtsmodel.FilterStatus) *apimodel.FilterStatus {
|
||||
return &apimodel.FilterStatus{
|
||||
ID: filterStatus.ID,
|
||||
StatusID: filterStatus.StatusID,
|
||||
}
|
||||
}
|
||||
|
||||
// convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied.
|
||||
func (c *Converter) convertEmojisToAPIEmojis(ctx context.Context, emojis []*gtsmodel.Emoji, emojiIDs []string) ([]apimodel.Emoji, error) {
|
||||
var errs gtserror.MultiError
|
||||
|
|
|
@ -45,6 +45,7 @@
|
|||
maximumProfileFields = 6
|
||||
maximumListTitleLength = 200
|
||||
maximumFilterKeywordLength = 40
|
||||
maximumFilterTitleLength = 200
|
||||
)
|
||||
|
||||
// Password returns a helpful error if the given password
|
||||
|
@ -242,9 +243,16 @@ func SiteTerms(t string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// ULID returns true if the passed string is a valid ULID.
|
||||
func ULID(i string) bool {
|
||||
return regexes.ULID.MatchString(i)
|
||||
// ULID returns an error if the passed string is not a valid ULID.
|
||||
// The name param is used to form error messages.
|
||||
func ULID(i string, name string) error {
|
||||
if i == "" {
|
||||
return fmt.Errorf("%s must be provided", name)
|
||||
}
|
||||
if !regexes.ULID.MatchString(i) {
|
||||
return fmt.Errorf("%s didn't match the expected ULID format for an ID (26 characters from the set 0123456789ABCDEFGHJKMNPQRSTVWXYZ)", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProfileFields validates the length of provided fields slice,
|
||||
|
@ -308,7 +316,7 @@ func MarkerName(name string) error {
|
|||
return fmt.Errorf("marker timeline name '%s' was not recognized, valid options are '%s', '%s'", name, apimodel.MarkerNameHome, apimodel.MarkerNameNotifications)
|
||||
}
|
||||
|
||||
// FilterKeyword validates the title of a new or updated List.
|
||||
// FilterKeyword validates a filter keyword.
|
||||
func FilterKeyword(keyword string) error {
|
||||
if keyword == "" {
|
||||
return fmt.Errorf("filter keyword must be provided, and must be no more than %d chars", maximumFilterKeywordLength)
|
||||
|
@ -321,6 +329,19 @@ func FilterKeyword(keyword string) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// FilterTitle validates the title of a new or updated filter.
|
||||
func FilterTitle(title string) error {
|
||||
if title == "" {
|
||||
return fmt.Errorf("filter title must be provided, and must be no more than %d chars", maximumFilterTitleLength)
|
||||
}
|
||||
|
||||
if length := len([]rune(title)); length > maximumFilterTitleLength {
|
||||
return fmt.Errorf("filter title length must be no more than %d chars, provided title was %d chars", maximumFilterTitleLength, length)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FilterContexts validates the context of a new or updated filter.
|
||||
func FilterContexts(contexts []apimodel.FilterContext) error {
|
||||
if len(contexts) == 0 {
|
||||
|
@ -349,6 +370,20 @@ func FilterContexts(contexts []apimodel.FilterContext) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func FilterAction(action apimodel.FilterAction) error {
|
||||
switch action {
|
||||
case apimodel.FilterActionWarn,
|
||||
apimodel.FilterActionHide:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(
|
||||
"filter action '%s' was not recognized, valid options are '%s', '%s'",
|
||||
action,
|
||||
apimodel.FilterActionWarn,
|
||||
apimodel.FilterActionHide,
|
||||
)
|
||||
}
|
||||
|
||||
// CreateAccount checks through all the prerequisites for
|
||||
// creating a new account, according to the provided form.
|
||||
// If the account isn't eligible, an error will be returned.
|
||||
|
|
|
@ -3288,6 +3288,26 @@ func NewTestFilters() map[string]*gtsmodel.Filter {
|
|||
ContextHome: util.Ptr(true),
|
||||
ContextPublic: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_filter_3": {
|
||||
ID: "01HWXQDXE4QX4R9EGMG729Y76C",
|
||||
CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
Title: "puppies",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
ContextPublic: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_filter_4": {
|
||||
ID: "01HZ55WWWP82WYP2A1BKWK8Y9Q",
|
||||
CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
Title: "empty filter with no keywords or statuses",
|
||||
Action: gtsmodel.FilterActionWarn,
|
||||
ContextHome: util.Ptr(true),
|
||||
ContextPublic: util.Ptr(true),
|
||||
},
|
||||
"local_account_2_filter_1": {
|
||||
ID: "01HNGFYJBED9FS0VWRVMY4TKXH",
|
||||
CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
|
@ -3330,6 +3350,15 @@ func NewTestFilterKeywords() map[string]*gtsmodel.FilterKeyword {
|
|||
Keyword: "bar",
|
||||
WholeWord: util.Ptr(true),
|
||||
},
|
||||
"local_account_1_filter_2_keyword_3": {
|
||||
ID: "01HXATJTGYT4BTG2YASE5M7GSD",
|
||||
CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
UpdatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
FilterID: "01HN277FSPQAWXZXK92QPPYF79",
|
||||
Keyword: "quux",
|
||||
WholeWord: util.Ptr(true),
|
||||
},
|
||||
"local_account_2_filter_1_keyword_1": {
|
||||
ID: "01HNGG51HV2JT67XQ5MQ7RA1WE",
|
||||
CreatedAt: TimeMustParse("2024-01-25T12:20:03+02:00"),
|
||||
|
@ -3343,8 +3372,24 @@ func NewTestFilterKeywords() map[string]*gtsmodel.FilterKeyword {
|
|||
}
|
||||
|
||||
func NewTestFilterStatuses() map[string]*gtsmodel.FilterStatus {
|
||||
// FUTURE: (filters v2) test filter statuses
|
||||
return map[string]*gtsmodel.FilterStatus{}
|
||||
return map[string]*gtsmodel.FilterStatus{
|
||||
"local_account_1_filter_3_status_1": {
|
||||
ID: "01HWXQDY8EE182AWQKS45JV50W",
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
AccountID: "01F8MH1H7YV1Z7D2C8K2730QBF",
|
||||
FilterID: "01HWXQDXE4QX4R9EGMG729Y76C",
|
||||
StatusID: "01F8MHAAY43M6RJ473VQFCVH37",
|
||||
},
|
||||
"local_account_2_filter_1_status_1": {
|
||||
ID: "01HX9WXVEH05E78ABR81FZFFFY",
|
||||
CreatedAt: time.Time{},
|
||||
UpdatedAt: time.Time{},
|
||||
AccountID: "01F8MH1VYJAE00TVVGMM5JNJ8X",
|
||||
FilterID: "01HNGFYJBED9FS0VWRVMY4TKXH",
|
||||
StatusID: "01FVW7JHQFSFK166WWKR8CBA6M",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// GetSignatureForActivity prepares a mock HTTP request as if it were going to deliver activity to destination signed for privkey and pubKeyID, signs the request and returns the header values.
|
||||
|
|
Loading…
Reference in a new issue