mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-30 23:42:25 -05:00 
			
		
		
		
	feature: filters v2 server-side warning/hiding (#2793)
* Remove dead code * Filter statuses when converting to frontend representation * status.filtered is an array * Make matching case-insensitive * Remove TODOs that don't need to be done now * Add missing filter check for notification * lint: rename ErrHideStatus * APIFilterActionToFilterAction not used yet * swaggerino docseroni * Address review comments * Add apimodel.FilterActionNone --------- Co-authored-by: tobi <31960611+tsmethurst@users.noreply.github.com> Co-authored-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
		
					parent
					
						
							
								a0d066844f
							
						
					
				
			
			
				commit
				
					
						45f4afe60e
					
				
			
		
					 24 changed files with 855 additions and 130 deletions
				
			
		|  | @ -1,5 +1,9 @@ | ||||||
| basePath: / | basePath: / | ||||||
| definitions: | definitions: | ||||||
|  |     FilterAction: | ||||||
|  |         title: FilterAction is the action to apply to statuses matching a filter. | ||||||
|  |         type: string | ||||||
|  |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|     InstanceConfigurationEmojis: |     InstanceConfigurationEmojis: | ||||||
|         properties: |         properties: | ||||||
|             emoji_size_limit: |             emoji_size_limit: | ||||||
|  | @ -1037,6 +1041,60 @@ definitions: | ||||||
|         type: string |         type: string | ||||||
|         x-go-name: FilterContext |         x-go-name: FilterContext | ||||||
|         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|  |     filterKeyword: | ||||||
|  |         properties: | ||||||
|  |             id: | ||||||
|  |                 description: The ID of the filter keyword entry in the database. | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: ID | ||||||
|  |             keyword: | ||||||
|  |                 description: The text to be filtered. | ||||||
|  |                 example: fnord | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: Keyword | ||||||
|  |             whole_word: | ||||||
|  |                 description: Should the filter consider word boundaries? | ||||||
|  |                 example: true | ||||||
|  |                 type: boolean | ||||||
|  |                 x-go-name: WholeWord | ||||||
|  |         title: FilterKeyword represents text to filter within a v2 filter. | ||||||
|  |         type: object | ||||||
|  |         x-go-name: FilterKeyword | ||||||
|  |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|  |     filterResult: | ||||||
|  |         properties: | ||||||
|  |             filter: | ||||||
|  |                 $ref: '#/definitions/filterV2' | ||||||
|  |             keyword_matches: | ||||||
|  |                 description: The keywords within the filter that were matched. | ||||||
|  |                 items: | ||||||
|  |                     type: string | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: KeywordMatches | ||||||
|  |             status_matches: | ||||||
|  |                 description: The status IDs within the filter that were matched. | ||||||
|  |                 items: | ||||||
|  |                     type: string | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: StatusMatches | ||||||
|  |         title: FilterResult is returned along with a filtered status to explain why it was filtered. | ||||||
|  |         type: object | ||||||
|  |         x-go-name: FilterResult | ||||||
|  |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|  |     filterStatus: | ||||||
|  |         properties: | ||||||
|  |             id: | ||||||
|  |                 description: The ID of the filter status entry in the database. | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: ID | ||||||
|  |             phrase: | ||||||
|  |                 description: The status ID to be filtered. | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: StatusID | ||||||
|  |         title: FilterStatus represents a single status to filter within a v2 filter. | ||||||
|  |         type: object | ||||||
|  |         x-go-name: FilterStatus | ||||||
|  |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|     filterV1: |     filterV1: | ||||||
|         description: |- |         description: |- | ||||||
|             Note that v1 filters are mapped to v2 filters and v2 filter keywords internally. |             Note that v1 filters are mapped to v2 filters and v2 filter keywords internally. | ||||||
|  | @ -1086,6 +1144,52 @@ definitions: | ||||||
|         type: object |         type: object | ||||||
|         x-go-name: FilterV1 |         x-go-name: FilterV1 | ||||||
|         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|  |     filterV2: | ||||||
|  |         description: v2 filters have names and can include multiple phrases and status IDs to filter. | ||||||
|  |         properties: | ||||||
|  |             context: | ||||||
|  |                 description: The contexts in which the filter should be applied. | ||||||
|  |                 example: | ||||||
|  |                     - home | ||||||
|  |                     - public | ||||||
|  |                 items: | ||||||
|  |                     $ref: '#/definitions/filterContext' | ||||||
|  |                 minItems: 1 | ||||||
|  |                 type: array | ||||||
|  |                 uniqueItems: true | ||||||
|  |                 x-go-name: Context | ||||||
|  |             expires_at: | ||||||
|  |                 description: When the filter should no longer be applied. Null if the filter does not expire. | ||||||
|  |                 example: "2024-02-01T02:57:49Z" | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: ExpiresAt | ||||||
|  |             filter_action: | ||||||
|  |                 $ref: '#/definitions/FilterAction' | ||||||
|  |             id: | ||||||
|  |                 description: The ID of the filter in the database. | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: ID | ||||||
|  |             keywords: | ||||||
|  |                 description: The keywords grouped under this filter. | ||||||
|  |                 items: | ||||||
|  |                     $ref: '#/definitions/filterKeyword' | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: Keywords | ||||||
|  |             statuses: | ||||||
|  |                 description: The statuses grouped under this filter. | ||||||
|  |                 items: | ||||||
|  |                     $ref: '#/definitions/filterStatus' | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: Statuses | ||||||
|  |             title: | ||||||
|  |                 description: The name of the filter. | ||||||
|  |                 example: Linux Words | ||||||
|  |                 type: string | ||||||
|  |                 x-go-name: Title | ||||||
|  |         title: FilterV2 represents a user-defined filter for determining which statuses should not be shown to the user. | ||||||
|  |         type: object | ||||||
|  |         x-go-name: FilterV2 | ||||||
|  |         x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model | ||||||
|     headerFilter: |     headerFilter: | ||||||
|         properties: |         properties: | ||||||
|             created_at: |             created_at: | ||||||
|  | @ -2118,6 +2222,12 @@ definitions: | ||||||
|                 format: int64 |                 format: int64 | ||||||
|                 type: integer |                 type: integer | ||||||
|                 x-go-name: FavouritesCount |                 x-go-name: FavouritesCount | ||||||
|  |             filtered: | ||||||
|  |                 description: A list of filters that matched this status and why they matched, if there are any such filters. | ||||||
|  |                 items: | ||||||
|  |                     $ref: '#/definitions/filterResult' | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: Filtered | ||||||
|             id: |             id: | ||||||
|                 description: ID of the status. |                 description: ID of the status. | ||||||
|                 example: 01FBVD42CQ3ZEEVMW180SBX03B |                 example: 01FBVD42CQ3ZEEVMW180SBX03B | ||||||
|  | @ -2321,6 +2431,12 @@ definitions: | ||||||
|                 format: int64 |                 format: int64 | ||||||
|                 type: integer |                 type: integer | ||||||
|                 x-go-name: FavouritesCount |                 x-go-name: FavouritesCount | ||||||
|  |             filtered: | ||||||
|  |                 description: A list of filters that matched this status and why they matched, if there are any such filters. | ||||||
|  |                 items: | ||||||
|  |                     $ref: '#/definitions/filterResult' | ||||||
|  |                 type: array | ||||||
|  |                 x-go-name: Filtered | ||||||
|             id: |             id: | ||||||
|                 description: ID of the status. |                 description: ID of the status. | ||||||
|                 example: 01FBVD42CQ3ZEEVMW180SBX03B |                 example: 01FBVD42CQ3ZEEVMW180SBX03B | ||||||
|  |  | ||||||
							
								
								
									
										34
									
								
								internal/api/model/filterresult.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								internal/api/model/filterresult.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,34 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | // FilterResult is returned along with a filtered status to explain why it was filtered. | ||||||
|  | // | ||||||
|  | // swagger:model filterResult | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - filters | ||||||
|  | type FilterResult struct { | ||||||
|  | 	// The filter that was matched. | ||||||
|  | 	Filter FilterV2 `json:"filter"` | ||||||
|  | 	// The keywords within the filter that were matched. | ||||||
|  | 	KeywordMatches []string `json:"keyword_matches"` | ||||||
|  | 	// The status IDs within the filter that were matched. | ||||||
|  | 	StatusMatches []string `json:"status_matches"` | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								internal/api/model/filterv2.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								internal/api/model/filterv2.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,106 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | package model | ||||||
|  | 
 | ||||||
|  | // FilterV2 represents a user-defined filter for determining which statuses should not be shown to the user. | ||||||
|  | // v2 filters have names and can include multiple phrases and status IDs to filter. | ||||||
|  | // | ||||||
|  | // swagger:model filterV2 | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - filters | ||||||
|  | type FilterV2 struct { | ||||||
|  | 	// The ID of the filter in the database. | ||||||
|  | 	ID string `json:"id"` | ||||||
|  | 	// The name of the filter. | ||||||
|  | 	// | ||||||
|  | 	// Example: Linux Words | ||||||
|  | 	Title string `json:"title"` | ||||||
|  | 	// The contexts in which the filter should be applied. | ||||||
|  | 	// | ||||||
|  | 	// Minimum items: 1 | ||||||
|  | 	// Unique: true | ||||||
|  | 	// Enum: | ||||||
|  | 	//	- home | ||||||
|  | 	//	- notifications | ||||||
|  | 	//	- public | ||||||
|  | 	//	- thread | ||||||
|  | 	//	- account | ||||||
|  | 	// Example: ["home", "public"] | ||||||
|  | 	Context []FilterContext `json:"context"` | ||||||
|  | 	// When the filter should no longer be applied. Null if the filter does not expire. | ||||||
|  | 	// | ||||||
|  | 	// Example: 2024-02-01T02:57:49Z | ||||||
|  | 	ExpiresAt *string `json:"expires_at"` | ||||||
|  | 	// The action to be taken when a status matches this filter. | ||||||
|  | 	// Enum: | ||||||
|  | 	//	- warn | ||||||
|  | 	//	- hide | ||||||
|  | 	FilterAction FilterAction `json:"filter_action"` | ||||||
|  | 	// The keywords grouped under this filter. | ||||||
|  | 	Keywords []FilterKeyword `json:"keywords"` | ||||||
|  | 	// The statuses grouped under this filter. | ||||||
|  | 	Statuses []FilterStatus `json:"statuses"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FilterAction is the action to apply to statuses matching a filter. | ||||||
|  | type FilterAction string | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// FilterActionNone filters should not exist, except internally, for partially constructed or invalid filters. | ||||||
|  | 	FilterActionNone FilterAction = "" | ||||||
|  | 	// FilterActionWarn filters will include this status in API results with a warning. | ||||||
|  | 	FilterActionWarn FilterAction = "warn" | ||||||
|  | 	// FilterActionHide filters will remove this status from API results. | ||||||
|  | 	FilterActionHide FilterAction = "hide" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // FilterKeyword represents text to filter within a v2 filter. | ||||||
|  | // | ||||||
|  | // swagger:model filterKeyword | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - filters | ||||||
|  | type FilterKeyword struct { | ||||||
|  | 	// The ID of the filter keyword entry in the database. | ||||||
|  | 	ID string `json:"id"` | ||||||
|  | 	// The text to be filtered. | ||||||
|  | 	// | ||||||
|  | 	// Example: fnord | ||||||
|  | 	Keyword string `json:"keyword"` | ||||||
|  | 	// Should the filter consider word boundaries? | ||||||
|  | 	// | ||||||
|  | 	// Example: true | ||||||
|  | 	WholeWord bool `json:"whole_word"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FilterStatus represents a single status to filter within a v2 filter. | ||||||
|  | // | ||||||
|  | // swagger:model filterStatus | ||||||
|  | // | ||||||
|  | // --- | ||||||
|  | // tags: | ||||||
|  | // - filters | ||||||
|  | type FilterStatus struct { | ||||||
|  | 	// The ID of the filter status entry in the database. | ||||||
|  | 	ID string `json:"id"` | ||||||
|  | 	// The status ID to be filtered. | ||||||
|  | 	StatusID string `json:"phrase"` | ||||||
|  | } | ||||||
|  | @ -100,6 +100,8 @@ type Status struct { | ||||||
| 	// so the user may redraft from the source text without the client having to reverse-engineer | 	// so the user may redraft from the source text without the client having to reverse-engineer | ||||||
| 	// the original text from the HTML content. | 	// the original text from the HTML content. | ||||||
| 	Text string `json:"text,omitempty"` | 	Text string `json:"text,omitempty"` | ||||||
|  | 	// A list of filters that matched this status and why they matched, if there are any such filters. | ||||||
|  | 	Filtered []FilterResult `json:"filtered,omitempty"` | ||||||
| 
 | 
 | ||||||
| 	// Additional fields not exposed via JSON | 	// Additional fields not exposed via JSON | ||||||
| 	// (used only internally for templating etc). | 	// (used only internally for templating etc). | ||||||
|  |  | ||||||
							
								
								
									
										45
									
								
								internal/filter/status/status.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								internal/filter/status/status.go
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | ||||||
|  | // GoToSocial | ||||||
|  | // Copyright (C) GoToSocial Authors admin@gotosocial.org | ||||||
|  | // SPDX-License-Identifier: AGPL-3.0-or-later | ||||||
|  | // | ||||||
|  | // This program is free software: you can redistribute it and/or modify | ||||||
|  | // it under the terms of the GNU Affero General Public License as published by | ||||||
|  | // the Free Software Foundation, either version 3 of the License, or | ||||||
|  | // (at your option) any later version. | ||||||
|  | // | ||||||
|  | // This program is distributed in the hope that it will be useful, | ||||||
|  | // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||||
|  | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||||
|  | // GNU Affero General Public License for more details. | ||||||
|  | // | ||||||
|  | // You should have received a copy of the GNU Affero General Public License | ||||||
|  | // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||||
|  | 
 | ||||||
|  | // Package status represents status filters managed by the user through the API. | ||||||
|  | package status | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | // ErrHideStatus indicates that a status has been filtered and should not be returned at all. | ||||||
|  | var ErrHideStatus = errors.New("hide status") | ||||||
|  | 
 | ||||||
|  | // FilterContext determines the filters that apply to a given status or list of statuses. | ||||||
|  | type FilterContext string | ||||||
|  | 
 | ||||||
|  | const ( | ||||||
|  | 	// FilterContextNone means no filters should be applied. | ||||||
|  | 	// There are no filters with this context; it's for internal use only. | ||||||
|  | 	FilterContextNone FilterContext = "" | ||||||
|  | 	// FilterContextHome means this status is being filtered as part of a home or list timeline. | ||||||
|  | 	FilterContextHome FilterContext = "home" | ||||||
|  | 	// FilterContextNotifications means this status is being filtered as part of the notifications timeline. | ||||||
|  | 	FilterContextNotifications FilterContext = "notifications" | ||||||
|  | 	// FilterContextPublic means this status is being filtered as part of a public or tag timeline. | ||||||
|  | 	FilterContextPublic FilterContext = "public" | ||||||
|  | 	// FilterContextThread means this status is being filtered as part of a thread's context. | ||||||
|  | 	FilterContextThread FilterContext = "thread" | ||||||
|  | 	// FilterContextAccount means this status is being filtered as part of an account's statuses. | ||||||
|  | 	FilterContextAccount FilterContext = "account" | ||||||
|  | ) | ||||||
|  | @ -23,6 +23,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -74,7 +75,7 @@ func (p *Processor) BookmarksGet(ctx context.Context, requestingAccount *gtsmode | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		// Convert the status. | 		// Convert the status. | ||||||
| 		item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount) | 		item, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf(ctx, "error converting bookmarked status to api: %s", err) | 			log.Errorf(ctx, "error converting bookmarked status to api: %s", err) | ||||||
| 			continue | 			continue | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -96,9 +97,15 @@ func (p *Processor) StatusesGet( | ||||||
| 		return nil, gtserror.NewErrorInternalError(err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	for _, s := range filtered { | 	for _, s := range filtered { | ||||||
| 		// Convert filtered statuses to API statuses. | 		// Convert filtered statuses to API statuses. | ||||||
| 		item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount) | 		item, err := p.converter.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextAccount, filters) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf(ctx, "error convering to api status: %v", err) | 			log.Errorf(ctx, "error convering to api status: %v", err) | ||||||
| 			continue | 			continue | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" | 	"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -184,7 +185,7 @@ func (p *Processor) GetAPIStatus( | ||||||
| 	apiStatus *apimodel.Status, | 	apiStatus *apimodel.Status, | ||||||
| 	errWithCode gtserror.WithCode, | 	errWithCode gtserror.WithCode, | ||||||
| ) { | ) { | ||||||
| 	apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester) | 	apiStatus, err := p.converter.StatusToAPIStatus(ctx, target, requester, statusfilter.FilterContextNone, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error converting status: %w", err) | 		err = gtserror.Newf("error converting status: %w", err) | ||||||
| 		return nil, gtserror.NewErrorInternalError(err) | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | @ -192,87 +193,6 @@ func (p *Processor) GetAPIStatus( | ||||||
| 	return apiStatus, nil | 	return apiStatus, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // GetVisibleAPIStatuses converts an array of gtsmodel.Status (inputted by next function) into |  | ||||||
| // API model statuses, checking first for visibility. Please note that all errors will be |  | ||||||
| // logged at ERROR level, but will not be returned. Callers are likely to run into show-stopping |  | ||||||
| // errors in the lead-up to this function, whereas calling this should not be a show-stopper. |  | ||||||
| func (p *Processor) GetVisibleAPIStatuses( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	requester *gtsmodel.Account, |  | ||||||
| 	next func(int) *gtsmodel.Status, |  | ||||||
| 	length int, |  | ||||||
| ) []*apimodel.Status { |  | ||||||
| 	return p.getVisibleAPIStatuses(ctx, 3, requester, next, length) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // GetVisibleAPIStatusesPaged is functionally equivalent to GetVisibleAPIStatuses(), |  | ||||||
| // except the statuses are returned as a converted slice of statuses as interface{}. |  | ||||||
| func (p *Processor) GetVisibleAPIStatusesPaged( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	requester *gtsmodel.Account, |  | ||||||
| 	next func(int) *gtsmodel.Status, |  | ||||||
| 	length int, |  | ||||||
| ) []interface{} { |  | ||||||
| 	statuses := p.getVisibleAPIStatuses(ctx, 3, requester, next, length) |  | ||||||
| 	if len(statuses) == 0 { |  | ||||||
| 		return nil |  | ||||||
| 	} |  | ||||||
| 	items := make([]interface{}, len(statuses)) |  | ||||||
| 	for i, status := range statuses { |  | ||||||
| 		items[i] = status |  | ||||||
| 	} |  | ||||||
| 	return items |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| func (p *Processor) getVisibleAPIStatuses( |  | ||||||
| 	ctx context.Context, |  | ||||||
| 	calldepth int, // used to skip wrapping func above these's names |  | ||||||
| 	requester *gtsmodel.Account, |  | ||||||
| 	next func(int) *gtsmodel.Status, |  | ||||||
| 	length int, |  | ||||||
| ) []*apimodel.Status { |  | ||||||
| 	// Start new log entry with |  | ||||||
| 	// the above calling func's name. |  | ||||||
| 	l := log. |  | ||||||
| 		WithContext(ctx). |  | ||||||
| 		WithField("caller", log.Caller(calldepth+1)) |  | ||||||
| 
 |  | ||||||
| 	// Preallocate slice according to expected length. |  | ||||||
| 	statuses := make([]*apimodel.Status, 0, length) |  | ||||||
| 
 |  | ||||||
| 	for i := 0; i < length; i++ { |  | ||||||
| 		// Get next status. |  | ||||||
| 		status := next(i) |  | ||||||
| 		if status == nil { |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Check whether this status is visible to requesting account. |  | ||||||
| 		visible, err := p.filter.StatusVisible(ctx, requester, status) |  | ||||||
| 		if err != nil { |  | ||||||
| 			l.Errorf("error checking status visibility: %v", err) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		if !visible { |  | ||||||
| 			// Not visible to requester. |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Convert the status to an API model representation. |  | ||||||
| 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requester) |  | ||||||
| 		if err != nil { |  | ||||||
| 			l.Errorf("error converting status: %v", err) |  | ||||||
| 			continue |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Append API model to return slice. |  | ||||||
| 		statuses = append(statuses, apiStatus) |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	return statuses |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| // InvalidateTimelinedStatus is a shortcut function for invalidating the cached | // InvalidateTimelinedStatus is a shortcut function for invalidating the cached | ||||||
| // representation one status in the home timeline and all list timelines of the | // representation one status in the home timeline and all list timelines of the | ||||||
| // given accountID. It should only be called in cases where a status update | // given accountID. It should only be called in cases where a status update | ||||||
|  |  | ||||||
|  | @ -21,6 +21,7 @@ import ( | ||||||
| 	"context" | 	"context" | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -113,7 +114,7 @@ func (p *Processor) packageStatuses( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount) | 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err) | 			log.Debugf(ctx, "skipping status %s because it couldn't be converted to its api representation: %s", status.ID, err) | ||||||
| 			continue | 			continue | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import ( | ||||||
| 	"strings" | 	"strings" | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
|  | @ -280,7 +281,15 @@ func TopoSort(apiStatuses []*apimodel.Status, targetAccountID string) { | ||||||
| 
 | 
 | ||||||
| // ContextGet returns the context (previous and following posts) from the given status ID. | // ContextGet returns the context (previous and following posts) from the given status ID. | ||||||
| func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { | func (p *Processor) ContextGet(ctx context.Context, requestingAccount *gtsmodel.Account, targetStatusID string) (*apimodel.Context, gtserror.WithCode) { | ||||||
| 	return p.contextGet(ctx, requestingAccount, targetStatusID, p.converter.StatusToAPIStatus) | 	filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 	convert := func(ctx context.Context, status *gtsmodel.Status, requestingAccount *gtsmodel.Account) (*apimodel.Status, error) { | ||||||
|  | 		return p.converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextThread, filters) | ||||||
|  | 	} | ||||||
|  | 	return p.contextGet(ctx, requestingAccount, targetStatusID, convert) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // WebContextGet is like ContextGet, but is explicitly | // WebContextGet is like ContextGet, but is explicitly | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 	"testing" | 	"testing" | ||||||
| 
 | 
 | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/stream" | 	"github.com/superseriousbusiness/gotosocial/internal/stream" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | 	"github.com/superseriousbusiness/gotosocial/internal/typeutils" | ||||||
| ) | ) | ||||||
|  | @ -39,7 +40,7 @@ func (suite *StatusUpdateTestSuite) TestStreamNotification() { | ||||||
| 	suite.NoError(errWithCode) | 	suite.NoError(errWithCode) | ||||||
| 
 | 
 | ||||||
| 	editedStatus := suite.testStatuses["remote_account_1_status_1"] | 	editedStatus := suite.testStatuses["remote_account_1_status_1"] | ||||||
| 	apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(context.Background(), editedStatus, account) | 	apiStatus, err := typeutils.NewConverter(&suite.state).StatusToAPIStatus(context.Background(), editedStatus, account, statusfilter.FilterContextNotifications, nil) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	suite.streamProcessor.StatusUpdate(context.Background(), account, apiStatus, stream.TimelineHome) | 	suite.streamProcessor.StatusUpdate(context.Background(), account, apiStatus, stream.TimelineHome) | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | 	"github.com/superseriousbusiness/gotosocial/internal/oauth" | ||||||
|  | @ -54,7 +55,7 @@ func (p *Processor) FavedTimelineGet(ctx context.Context, authed *oauth.Auth, ma | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account) | 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, authed.Account, statusfilter.FilterContextNone, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf(ctx, "error convering to api status: %v", err) | 			log.Errorf(ctx, "error convering to api status: %v", err) | ||||||
| 			continue | 			continue | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | @ -98,7 +99,13 @@ func HomeTimelineStatusPrepare(state *state.State, converter *typeutils.Converte | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return converter.StatusToAPIStatus(ctx, status, requestingAccount) | 		filters, err := state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | 	"github.com/superseriousbusiness/gotosocial/internal/filter/visibility" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | @ -110,7 +111,13 @@ func ListTimelineStatusPrepare(state *state.State, converter *typeutils.Converte | ||||||
| 			return nil, err | 			return nil, err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return converter.StatusToAPIStatus(ctx, status, requestingAccount) | 		filters, err := state.DB.GetFiltersForAccountID(ctx, requestingAccount.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAccount.ID, err) | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return converter.StatusToAPIStatus(ctx, status, requestingAccount, statusfilter.FilterContextHome, filters) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -43,6 +43,12 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma | ||||||
| 		return util.EmptyPageableResponse(), nil | 		return util.EmptyPageableResponse(), nil | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	filters, err := p.state.DB.GetFiltersForAccountID(ctx, authed.Account.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", authed.Account.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	var ( | 	var ( | ||||||
| 		items          = make([]interface{}, 0, count) | 		items          = make([]interface{}, 0, count) | ||||||
| 		nextMaxIDValue string | 		nextMaxIDValue string | ||||||
|  | @ -70,7 +76,7 @@ func (p *Processor) NotificationsGet(ctx context.Context, authed *oauth.Auth, ma | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		item, err := p.converter.NotificationToAPINotification(ctx, n) | 		item, err := p.converter.NotificationToAPINotification(ctx, n, filters) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err) | 			log.Debugf(ctx, "skipping notification %s because it couldn't be converted to its api representation: %s", n.ID, err) | ||||||
| 			continue | 			continue | ||||||
|  | @ -104,7 +110,13 @@ func (p *Processor) NotificationGet(ctx context.Context, account *gtsmodel.Accou | ||||||
| 		return nil, gtserror.NewErrorNotFound(err) | 		return nil, gtserror.NewErrorNotFound(err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif) | 	filters, err := p.state.DB.GetFiltersForAccountID(ctx, account.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", account.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	apiNotif, err := p.converter.NotificationToAPINotification(ctx, notif, filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if errors.Is(err, db.ErrNoEntries) { | 		if errors.Is(err, db.ErrNoEntries) { | ||||||
| 			return nil, gtserror.NewErrorNotFound(err) | 			return nil, gtserror.NewErrorNotFound(err) | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -46,6 +47,16 @@ func (p *Processor) PublicTimelineGet( | ||||||
| 		items          = make([]any, 0, limit) | 		items          = make([]any, 0, limit) | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | 	var filters []*gtsmodel.Filter | ||||||
|  | 	if requester != nil { | ||||||
|  | 		var err error | ||||||
|  | 		filters, err = p.state.DB.GetFiltersForAccountID(ctx, requester.ID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requester.ID, err) | ||||||
|  | 			return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	// Try a few times to select appropriate public | 	// Try a few times to select appropriate public | ||||||
| 	// statuses from the db, paging up or down to | 	// statuses from the db, paging up or down to | ||||||
| 	// reattempt if nothing suitable is found. | 	// reattempt if nothing suitable is found. | ||||||
|  | @ -87,7 +98,10 @@ outer: | ||||||
| 				continue inner | 				continue inner | ||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requester) | 			apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requester, statusfilter.FilterContextPublic, filters) | ||||||
|  | 			if errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				log.Errorf(ctx, "error converting to api status: %v", err) | 				log.Errorf(ctx, "error converting to api status: %v", err) | ||||||
| 				continue inner | 				continue inner | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
|  | @ -111,6 +112,12 @@ func (p *Processor) packageTagResponse( | ||||||
| 		prevMinIDValue = statuses[0].ID | 		prevMinIDValue = statuses[0].ID | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
|  | 	filters, err := p.state.DB.GetFiltersForAccountID(ctx, requestingAcct.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		err = gtserror.Newf("couldn't retrieve filters for account %s: %w", requestingAcct.ID, err) | ||||||
|  | 		return nil, gtserror.NewErrorInternalError(err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	for _, s := range statuses { | 	for _, s := range statuses { | ||||||
| 		timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s) | 		timelineable, err := p.filter.StatusTagTimelineable(ctx, requestingAcct, s) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -122,7 +129,10 @@ func (p *Processor) packageTagResponse( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct) | 		apiStatus, err := p.converter.StatusToAPIStatus(ctx, s, requestingAcct, statusfilter.FilterContextPublic, filters) | ||||||
|  | 		if errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			log.Errorf(ctx, "error converting to api status: %v", err) | 			log.Errorf(ctx, "error converting to api status: %v", err) | ||||||
| 			continue | 			continue | ||||||
|  |  | ||||||
|  | @ -28,6 +28,7 @@ import ( | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/ap" | 	"github.com/superseriousbusiness/gotosocial/internal/ap" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/id" | 	"github.com/superseriousbusiness/gotosocial/internal/id" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/messages" | 	"github.com/superseriousbusiness/gotosocial/internal/messages" | ||||||
|  | @ -154,6 +155,8 @@ func (suite *FromClientAPITestSuite) statusJSON( | ||||||
| 		ctx, | 		ctx, | ||||||
| 		status, | 		status, | ||||||
| 		requestingAccount, | 		requestingAccount, | ||||||
|  | 		statusfilter.FilterContextNone, | ||||||
|  | 		nil, | ||||||
| 	) | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		suite.FailNow(err.Error()) | 		suite.FailNow(err.Error()) | ||||||
|  | @ -258,7 +261,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() { | ||||||
| 		suite.FailNow("timed out waiting for new status notification") | 		suite.FailNow("timed out waiting for new status notification") | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif) | 	apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		suite.FailNow(err.Error()) | 		suite.FailNow(err.Error()) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -467,7 +467,12 @@ func (s *Surface) Notify( | ||||||
| 	unlock() | 	unlock() | ||||||
| 
 | 
 | ||||||
| 	// Stream notification to the user. | 	// Stream notification to the user. | ||||||
| 	apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif) | 	filters, err := s.State.DB.GetFiltersForAccountID(ctx, targetAccount.ID) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return gtserror.Newf("couldn't retrieve filters for account %s: %w", targetAccount.ID, err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return gtserror.Newf("error converting notification to api representation: %w", err) | 		return gtserror.Newf("error converting notification to api representation: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | @ -22,6 +22,7 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 
 | 
 | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | 	"github.com/superseriousbusiness/gotosocial/internal/gtscontext" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
|  | @ -111,6 +112,11 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		filters, err := s.State.DB.GetFiltersForAccountID(ctx, follow.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return gtserror.Newf("couldn't retrieve filters for account %s: %w", follow.AccountID, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// Add status to any relevant lists | 		// Add status to any relevant lists | ||||||
| 		// for this follow, if applicable. | 		// for this follow, if applicable. | ||||||
| 		s.listTimelineStatusForFollow( | 		s.listTimelineStatusForFollow( | ||||||
|  | @ -118,6 +124,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||||
| 			status, | 			status, | ||||||
| 			follow, | 			follow, | ||||||
| 			&errs, | 			&errs, | ||||||
|  | 			filters, | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 		// Add status to home timeline for owner | 		// Add status to home timeline for owner | ||||||
|  | @ -129,6 +136,7 @@ func (s *Surface) timelineAndNotifyStatusForFollowers( | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineHome, | 			stream.TimelineHome, | ||||||
|  | 			filters, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errs.Appendf("error home timelining status: %w", err) | 			errs.Appendf("error home timelining status: %w", err) | ||||||
|  | @ -180,6 +188,7 @@ func (s *Surface) listTimelineStatusForFollow( | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follow *gtsmodel.Follow, | 	follow *gtsmodel.Follow, | ||||||
| 	errs *gtserror.MultiError, | 	errs *gtserror.MultiError, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) { | ) { | ||||||
| 	// To put this status in appropriate list timelines, | 	// To put this status in appropriate list timelines, | ||||||
| 	// we need to get each listEntry that pertains to | 	// we need to get each listEntry that pertains to | ||||||
|  | @ -222,6 +231,7 @@ func (s *Surface) listTimelineStatusForFollow( | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list | 			stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list | ||||||
|  | 			filters, | ||||||
| 		); err != nil { | 		); err != nil { | ||||||
| 			errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err) | 			errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err) | ||||||
| 			// implicit continue | 			// implicit continue | ||||||
|  | @ -332,6 +342,7 @@ func (s *Surface) timelineStatus( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	streamType string, | 	streamType string, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) (bool, error) { | ) (bool, error) { | ||||||
| 	// Ingest status into given timeline using provided function. | 	// Ingest status into given timeline using provided function. | ||||||
| 	if inserted, err := ingest(ctx, timelineID, status); err != nil { | 	if inserted, err := ingest(ctx, timelineID, status); err != nil { | ||||||
|  | @ -343,7 +354,12 @@ func (s *Surface) timelineStatus( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	// The status was inserted so stream it to the user. | 	// The status was inserted so stream it to the user. | ||||||
| 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, | ||||||
|  | 		status, | ||||||
|  | 		account, | ||||||
|  | 		statusfilter.FilterContextHome, | ||||||
|  | 		filters, | ||||||
|  | 	) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||||
| 		return true, err | 		return true, err | ||||||
|  | @ -457,6 +473,11 @@ func (s *Surface) timelineStatusUpdateForFollowers( | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		filters, err := s.State.DB.GetFiltersForAccountID(ctx, follow.AccountID) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return gtserror.Newf("couldn't retrieve filters for account %s: %w", follow.AccountID, err) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		// Add status to any relevant lists | 		// Add status to any relevant lists | ||||||
| 		// for this follow, if applicable. | 		// for this follow, if applicable. | ||||||
| 		s.listTimelineStatusUpdateForFollow( | 		s.listTimelineStatusUpdateForFollow( | ||||||
|  | @ -464,6 +485,7 @@ func (s *Surface) timelineStatusUpdateForFollowers( | ||||||
| 			status, | 			status, | ||||||
| 			follow, | 			follow, | ||||||
| 			&errs, | 			&errs, | ||||||
|  | 			filters, | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 		// Add status to home timeline for owner | 		// Add status to home timeline for owner | ||||||
|  | @ -473,6 +495,7 @@ func (s *Surface) timelineStatusUpdateForFollowers( | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineHome, | 			stream.TimelineHome, | ||||||
|  | 			filters, | ||||||
| 		) | 		) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			errs.Appendf("error home timelining status: %w", err) | 			errs.Appendf("error home timelining status: %w", err) | ||||||
|  | @ -490,6 +513,7 @@ func (s *Surface) listTimelineStatusUpdateForFollow( | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	follow *gtsmodel.Follow, | 	follow *gtsmodel.Follow, | ||||||
| 	errs *gtserror.MultiError, | 	errs *gtserror.MultiError, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) { | ) { | ||||||
| 	// To put this status in appropriate list timelines, | 	// To put this status in appropriate list timelines, | ||||||
| 	// we need to get each listEntry that pertains to | 	// we need to get each listEntry that pertains to | ||||||
|  | @ -530,6 +554,7 @@ func (s *Surface) listTimelineStatusUpdateForFollow( | ||||||
| 			follow.Account, | 			follow.Account, | ||||||
| 			status, | 			status, | ||||||
| 			stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list | 			stream.TimelineList+":"+listEntry.ListID, // key streamType to this specific list | ||||||
|  | 			filters, | ||||||
| 		); err != nil { | 		); err != nil { | ||||||
| 			errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err) | 			errs.Appendf("error adding status to timeline for list %s: %w", listEntry.ListID, err) | ||||||
| 			// implicit continue | 			// implicit continue | ||||||
|  | @ -544,8 +569,13 @@ func (s *Surface) timelineStreamStatusUpdate( | ||||||
| 	account *gtsmodel.Account, | 	account *gtsmodel.Account, | ||||||
| 	status *gtsmodel.Status, | 	status *gtsmodel.Status, | ||||||
| 	streamType string, | 	streamType string, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) error { | ) error { | ||||||
| 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account) | 	apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account, statusfilter.FilterContextHome, filters) | ||||||
|  | 	if errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
|  | 		// Don't put this status in the stream. | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | 		err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err) | ||||||
| 		return err | 		return err | ||||||
|  |  | ||||||
|  | @ -24,6 +24,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"codeberg.org/gruf/go-kv" | 	"codeberg.org/gruf/go-kv" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| ) | ) | ||||||
|  | @ -121,6 +122,12 @@ func (t *timeline) prepareXBetweenIDs(ctx context.Context, amount int, behindID | ||||||
| 	for e, entry := range toPrepare { | 	for e, entry := range toPrepare { | ||||||
| 		prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID) | 		prepared, err := t.prepareFunction(ctx, t.timelineID, entry.itemID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | 			if errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
|  | 				// This item has been filtered out by the requesting user's filters. | ||||||
|  | 				// Remove it and skip past it. | ||||||
|  | 				t.items.data.Remove(e) | ||||||
|  | 				continue | ||||||
|  | 			} | ||||||
| 			if errors.Is(err, db.ErrNoEntries) { | 			if errors.Is(err, db.ErrNoEntries) { | ||||||
| 				// ErrNoEntries means something has been deleted, | 				// ErrNoEntries means something has been deleted, | ||||||
| 				// so we'll likely not be able to ever prepare this. | 				// so we'll likely not be able to ever prepare this. | ||||||
|  |  | ||||||
|  | @ -483,6 +483,9 @@ type TypeUtilsTestSuite struct { | ||||||
| 	testReports        map[string]*gtsmodel.Report | 	testReports        map[string]*gtsmodel.Report | ||||||
| 	testMentions       map[string]*gtsmodel.Mention | 	testMentions       map[string]*gtsmodel.Mention | ||||||
| 	testPollVotes      map[string]*gtsmodel.PollVote | 	testPollVotes      map[string]*gtsmodel.PollVote | ||||||
|  | 	testFilters        map[string]*gtsmodel.Filter | ||||||
|  | 	testFilterKeywords map[string]*gtsmodel.FilterKeyword | ||||||
|  | 	testFilterStatues  map[string]*gtsmodel.FilterStatus | ||||||
| 
 | 
 | ||||||
| 	typeconverter *typeutils.Converter | 	typeconverter *typeutils.Converter | ||||||
| } | } | ||||||
|  | @ -506,6 +509,9 @@ func (suite *TypeUtilsTestSuite) SetupTest() { | ||||||
| 	suite.testReports = testrig.NewTestReports() | 	suite.testReports = testrig.NewTestReports() | ||||||
| 	suite.testMentions = testrig.NewTestMentions() | 	suite.testMentions = testrig.NewTestMentions() | ||||||
| 	suite.testPollVotes = testrig.NewTestPollVotes() | 	suite.testPollVotes = testrig.NewTestPollVotes() | ||||||
|  | 	suite.testFilters = testrig.NewTestFilters() | ||||||
|  | 	suite.testFilterKeywords = testrig.NewTestFilterKeywords() | ||||||
|  | 	suite.testFilterStatues = testrig.NewTestFilterStatuses() | ||||||
| 	suite.typeconverter = typeutils.NewConverter(&suite.state) | 	suite.typeconverter = typeutils.NewConverter(&suite.state) | ||||||
| 
 | 
 | ||||||
| 	testrig.StandardDBSetup(suite.db, nil) | 	testrig.StandardDBSetup(suite.db, nil) | ||||||
|  |  | ||||||
|  | @ -22,17 +22,21 @@ import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"math" | 	"math" | ||||||
|  | 	"regexp" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| 	"strings" | 	"strings" | ||||||
|  | 	"time" | ||||||
| 
 | 
 | ||||||
| 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | 	apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | 	"github.com/superseriousbusiness/gotosocial/internal/gtserror" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/language" | 	"github.com/superseriousbusiness/gotosocial/internal/language" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/log" | 	"github.com/superseriousbusiness/gotosocial/internal/log" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/media" | 	"github.com/superseriousbusiness/gotosocial/internal/media" | ||||||
|  | 	"github.com/superseriousbusiness/gotosocial/internal/text" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/uris" | 	"github.com/superseriousbusiness/gotosocial/internal/uris" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/util" | 	"github.com/superseriousbusiness/gotosocial/internal/util" | ||||||
| ) | ) | ||||||
|  | @ -684,12 +688,19 @@ func (c *Converter) TagToAPITag(ctx context.Context, t *gtsmodel.Tag, stubHistor | ||||||
| // (frontend) representation for serialization on the API. | // (frontend) representation for serialization on the API. | ||||||
| // | // | ||||||
| // Requesting account can be nil. | // Requesting account can be nil. | ||||||
|  | // | ||||||
|  | // Filter context can be the empty string if these statuses are not being filtered. | ||||||
|  | // | ||||||
|  | // If there is a matching "hide" filter, the returned status will be nil with a ErrHideStatus error; | ||||||
|  | // callers need to handle that case by excluding it from results. | ||||||
| func (c *Converter) StatusToAPIStatus( | func (c *Converter) StatusToAPIStatus( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	s *gtsmodel.Status, | 	s *gtsmodel.Status, | ||||||
| 	requestingAccount *gtsmodel.Account, | 	requestingAccount *gtsmodel.Account, | ||||||
|  | 	filterContext statusfilter.FilterContext, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) (*apimodel.Status, error) { | ) (*apimodel.Status, error) { | ||||||
| 	apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount) | 	apiStatus, err := c.statusToFrontend(ctx, s, requestingAccount, filterContext, filters) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -704,6 +715,142 @@ func (c *Converter) StatusToAPIStatus( | ||||||
| 	return apiStatus, nil | 	return apiStatus, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // statusToAPIFilterResults applies filters to a status and returns an API filter result object. | ||||||
|  | // The result may be nil if no filters matched. | ||||||
|  | // If the status should not be returned at all, it returns the ErrHideStatus error. | ||||||
|  | func (c *Converter) statusToAPIFilterResults( | ||||||
|  | 	ctx context.Context, | ||||||
|  | 	s *gtsmodel.Status, | ||||||
|  | 	requestingAccount *gtsmodel.Account, | ||||||
|  | 	filterContext statusfilter.FilterContext, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
|  | ) ([]apimodel.FilterResult, error) { | ||||||
|  | 	if filterContext == "" || len(filters) == 0 || s.AccountID == requestingAccount.ID { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	filterResults := make([]apimodel.FilterResult, 0, len(filters)) | ||||||
|  | 
 | ||||||
|  | 	now := time.Now() | ||||||
|  | 	for _, filter := range filters { | ||||||
|  | 		if !filterAppliesInContext(filter, filterContext) { | ||||||
|  | 			// Filter doesn't apply to this context. | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 		if !filter.ExpiresAt.IsZero() && filter.ExpiresAt.Before(now) { | ||||||
|  | 			// Filter is expired. | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// List all matching keywords. | ||||||
|  | 		keywordMatches := make([]string, 0, len(filter.Keywords)) | ||||||
|  | 		fields := filterableTextFields(s) | ||||||
|  | 		for _, filterKeyword := range filter.Keywords { | ||||||
|  | 			wholeWord := util.PtrValueOr(filterKeyword.WholeWord, false) | ||||||
|  | 			wordBreak := `` | ||||||
|  | 			if wholeWord { | ||||||
|  | 				wordBreak = `\b` | ||||||
|  | 			} | ||||||
|  | 			re, err := regexp.Compile(`(?i)` + wordBreak + regexp.QuoteMeta(filterKeyword.Keyword) + wordBreak) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 			var isMatch bool | ||||||
|  | 			for _, field := range fields { | ||||||
|  | 				if re.MatchString(field) { | ||||||
|  | 					isMatch = true | ||||||
|  | 					break | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 			if isMatch { | ||||||
|  | 				keywordMatches = append(keywordMatches, filterKeyword.Keyword) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// A status has only one ID. Not clear why this is a list in the Mastodon API. | ||||||
|  | 		statusMatches := make([]string, 0, 1) | ||||||
|  | 		for _, filterStatus := range filter.Statuses { | ||||||
|  | 			if s.ID == filterStatus.StatusID { | ||||||
|  | 				statusMatches = append(statusMatches, filterStatus.StatusID) | ||||||
|  | 				break | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(keywordMatches) > 0 || len(statusMatches) > 0 { | ||||||
|  | 			switch filter.Action { | ||||||
|  | 			case gtsmodel.FilterActionWarn: | ||||||
|  | 				// Record what matched. | ||||||
|  | 				apiFilter, err := c.FilterToAPIFilterV2(ctx, filter) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return nil, err | ||||||
|  | 				} | ||||||
|  | 				filterResults = append(filterResults, apimodel.FilterResult{ | ||||||
|  | 					Filter:         *apiFilter, | ||||||
|  | 					KeywordMatches: keywordMatches, | ||||||
|  | 					StatusMatches:  statusMatches, | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 			case gtsmodel.FilterActionHide: | ||||||
|  | 				// Don't show this status. Immediate return. | ||||||
|  | 				return nil, statusfilter.ErrHideStatus | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return filterResults, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // filterableTextFields returns all text from a status that we might want to filter on: | ||||||
|  | // - content | ||||||
|  | // - content warning | ||||||
|  | // - media descriptions | ||||||
|  | // - poll options | ||||||
|  | func filterableTextFields(s *gtsmodel.Status) []string { | ||||||
|  | 	fieldCount := 2 + len(s.Attachments) | ||||||
|  | 	if s.Poll != nil { | ||||||
|  | 		fieldCount += len(s.Poll.Options) | ||||||
|  | 	} | ||||||
|  | 	fields := make([]string, 0, fieldCount) | ||||||
|  | 
 | ||||||
|  | 	if s.Content != "" { | ||||||
|  | 		fields = append(fields, text.SanitizeToPlaintext(s.Content)) | ||||||
|  | 	} | ||||||
|  | 	if s.ContentWarning != "" { | ||||||
|  | 		fields = append(fields, s.ContentWarning) | ||||||
|  | 	} | ||||||
|  | 	for _, attachment := range s.Attachments { | ||||||
|  | 		if attachment.Description != "" { | ||||||
|  | 			fields = append(fields, attachment.Description) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	if s.Poll != nil { | ||||||
|  | 		for _, option := range s.Poll.Options { | ||||||
|  | 			if option != "" { | ||||||
|  | 				fields = append(fields, option) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return fields | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // filterAppliesInContext returns whether a given filter applies in a given context. | ||||||
|  | func filterAppliesInContext(filter *gtsmodel.Filter, filterContext statusfilter.FilterContext) bool { | ||||||
|  | 	switch filterContext { | ||||||
|  | 	case statusfilter.FilterContextHome: | ||||||
|  | 		return util.PtrValueOr(filter.ContextHome, false) | ||||||
|  | 	case statusfilter.FilterContextNotifications: | ||||||
|  | 		return util.PtrValueOr(filter.ContextNotifications, false) | ||||||
|  | 	case statusfilter.FilterContextPublic: | ||||||
|  | 		return util.PtrValueOr(filter.ContextPublic, false) | ||||||
|  | 	case statusfilter.FilterContextThread: | ||||||
|  | 		return util.PtrValueOr(filter.ContextThread, false) | ||||||
|  | 	case statusfilter.FilterContextAccount: | ||||||
|  | 		return util.PtrValueOr(filter.ContextAccount, false) | ||||||
|  | 	} | ||||||
|  | 	return false | ||||||
|  | } | ||||||
|  | 
 | ||||||
| // StatusToWebStatus converts a gts model status into an | // StatusToWebStatus converts a gts model status into an | ||||||
| // api representation suitable for serving into a web template. | // api representation suitable for serving into a web template. | ||||||
| // | // | ||||||
|  | @ -713,7 +860,7 @@ func (c *Converter) StatusToWebStatus( | ||||||
| 	s *gtsmodel.Status, | 	s *gtsmodel.Status, | ||||||
| 	requestingAccount *gtsmodel.Account, | 	requestingAccount *gtsmodel.Account, | ||||||
| ) (*apimodel.Status, error) { | ) (*apimodel.Status, error) { | ||||||
| 	webStatus, err := c.statusToFrontend(ctx, s, requestingAccount) | 	webStatus, err := c.statusToFrontend(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  | @ -815,6 +962,8 @@ func (c *Converter) statusToFrontend( | ||||||
| 	ctx context.Context, | 	ctx context.Context, | ||||||
| 	s *gtsmodel.Status, | 	s *gtsmodel.Status, | ||||||
| 	requestingAccount *gtsmodel.Account, | 	requestingAccount *gtsmodel.Account, | ||||||
|  | 	filterContext statusfilter.FilterContext, | ||||||
|  | 	filters []*gtsmodel.Filter, | ||||||
| ) (*apimodel.Status, error) { | ) (*apimodel.Status, error) { | ||||||
| 	// Try to populate status struct pointer fields. | 	// Try to populate status struct pointer fields. | ||||||
| 	// We can continue in many cases of partial failure, | 	// We can continue in many cases of partial failure, | ||||||
|  | @ -913,7 +1062,11 @@ func (c *Converter) statusToFrontend( | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if s.BoostOf != nil { | 	if s.BoostOf != nil { | ||||||
| 		reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount) | 		reblog, err := c.StatusToAPIStatus(ctx, s.BoostOf, requestingAccount, filterContext, filters) | ||||||
|  | 		if errors.Is(err, statusfilter.ErrHideStatus) { | ||||||
|  | 			// If we'd hide the original status, hide the boost. | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, gtserror.Newf("error converting boosted status: %w", err) | 			return nil, gtserror.Newf("error converting boosted status: %w", err) | ||||||
| 		} | 		} | ||||||
|  | @ -977,6 +1130,13 @@ func (c *Converter) statusToFrontend( | ||||||
| 		s.URL = s.URI | 		s.URL = s.URI | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	// Apply filters. | ||||||
|  | 	filterResults, err := c.statusToAPIFilterResults(ctx, s, requestingAccount, filterContext, filters) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("error applying filters: %w", err) | ||||||
|  | 	} | ||||||
|  | 	apiStatus.Filtered = filterResults | ||||||
|  | 
 | ||||||
| 	return apiStatus, nil | 	return apiStatus, nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | @ -1252,7 +1412,7 @@ func (c *Converter) RelationshipToAPIRelationship(ctx context.Context, r *gtsmod | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // NotificationToAPINotification converts a gts notification into a api notification | // NotificationToAPINotification converts a gts notification into a api notification | ||||||
| func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification) (*apimodel.Notification, error) { | func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmodel.Notification, filters []*gtsmodel.Filter) (*apimodel.Notification, error) { | ||||||
| 	if n.TargetAccount == nil { | 	if n.TargetAccount == nil { | ||||||
| 		tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID) | 		tAccount, err := c.state.DB.GetAccountByID(ctx, n.TargetAccountID) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
|  | @ -1293,7 +1453,7 @@ func (c *Converter) NotificationToAPINotification(ctx context.Context, n *gtsmod | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		var err error | 		var err error | ||||||
| 		apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount) | 		apiStatus, err = c.StatusToAPIStatus(ctx, n.Status, n.TargetAccount, statusfilter.FilterContextNotifications, filters) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err) | 			return nil, fmt.Errorf("NotificationToapi: error converting status to api: %s", err) | ||||||
| 		} | 		} | ||||||
|  | @ -1446,7 +1606,7 @@ func (c *Converter) ReportToAdminAPIReport(ctx context.Context, r *gtsmodel.Repo | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	for _, s := range r.Statuses { | 	for _, s := range r.Statuses { | ||||||
| 		status, err := c.StatusToAPIStatus(ctx, s, requestingAccount) | 		status, err := c.StatusToAPIStatus(ctx, s, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err) | 			return nil, fmt.Errorf("ReportToAdminAPIReport: error converting status with id %s to api status: %w", s.ID, err) | ||||||
| 		} | 		} | ||||||
|  | @ -1687,6 +1847,55 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor | ||||||
| 	} | 	} | ||||||
| 	filter := filterKeyword.Filter | 	filter := filterKeyword.Filter | ||||||
| 
 | 
 | ||||||
|  | 	return &apimodel.FilterV1{ | ||||||
|  | 		// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID. | ||||||
|  | 		ID:           filterKeyword.ID, | ||||||
|  | 		Phrase:       filterKeyword.Keyword, | ||||||
|  | 		Context:      filterToAPIFilterContexts(filter), | ||||||
|  | 		WholeWord:    util.PtrValueOr(filterKeyword.WholeWord, false), | ||||||
|  | 		ExpiresAt:    filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt), | ||||||
|  | 		Irreversible: filter.Action == gtsmodel.FilterActionHide, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // FilterToAPIFilterV2 converts one GTS model filter into an API v2 filter. | ||||||
|  | func (c *Converter) FilterToAPIFilterV2(ctx context.Context, filter *gtsmodel.Filter) (*apimodel.FilterV2, error) { | ||||||
|  | 	apiFilterKeywords := make([]apimodel.FilterKeyword, 0, len(filter.Keywords)) | ||||||
|  | 	for _, filterKeyword := range filter.Keywords { | ||||||
|  | 		apiFilterKeywords = append(apiFilterKeywords, apimodel.FilterKeyword{ | ||||||
|  | 			ID:        filterKeyword.ID, | ||||||
|  | 			Keyword:   filterKeyword.Keyword, | ||||||
|  | 			WholeWord: util.PtrValueOr(filterKeyword.WholeWord, false), | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	apiFilterStatuses := make([]apimodel.FilterStatus, 0, len(filter.Keywords)) | ||||||
|  | 	for _, filterStatus := range filter.Statuses { | ||||||
|  | 		apiFilterStatuses = append(apiFilterStatuses, apimodel.FilterStatus{ | ||||||
|  | 			ID:       filterStatus.ID, | ||||||
|  | 			StatusID: filterStatus.StatusID, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return &apimodel.FilterV2{ | ||||||
|  | 		ID:           filter.ID, | ||||||
|  | 		Title:        filter.Title, | ||||||
|  | 		Context:      filterToAPIFilterContexts(filter), | ||||||
|  | 		ExpiresAt:    filterExpiresAtToAPIFilterExpiresAt(filter.ExpiresAt), | ||||||
|  | 		FilterAction: filterActionToAPIFilterAction(filter.Action), | ||||||
|  | 		Keywords:     apiFilterKeywords, | ||||||
|  | 		Statuses:     apiFilterStatuses, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func filterExpiresAtToAPIFilterExpiresAt(expiresAt time.Time) *string { | ||||||
|  | 	if expiresAt.IsZero() { | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	return util.Ptr(util.FormatISO8601(expiresAt)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func filterToAPIFilterContexts(filter *gtsmodel.Filter) []apimodel.FilterContext { | ||||||
| 	apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues) | 	apiContexts := make([]apimodel.FilterContext, 0, apimodel.FilterContextNumValues) | ||||||
| 	if util.PtrValueOr(filter.ContextHome, false) { | 	if util.PtrValueOr(filter.ContextHome, false) { | ||||||
| 		apiContexts = append(apiContexts, apimodel.FilterContextHome) | 		apiContexts = append(apiContexts, apimodel.FilterContextHome) | ||||||
|  | @ -1703,21 +1912,17 @@ func (c *Converter) FilterKeywordToAPIFilterV1(ctx context.Context, filterKeywor | ||||||
| 	if util.PtrValueOr(filter.ContextAccount, false) { | 	if util.PtrValueOr(filter.ContextAccount, false) { | ||||||
| 		apiContexts = append(apiContexts, apimodel.FilterContextAccount) | 		apiContexts = append(apiContexts, apimodel.FilterContextAccount) | ||||||
| 	} | 	} | ||||||
| 
 | 	return apiContexts | ||||||
| 	var expiresAt *string |  | ||||||
| 	if !filter.ExpiresAt.IsZero() { |  | ||||||
| 		expiresAt = util.Ptr(util.FormatISO8601(filter.ExpiresAt)) |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 	return &apimodel.FilterV1{ | func filterActionToAPIFilterAction(m gtsmodel.FilterAction) apimodel.FilterAction { | ||||||
| 		// v1 filters have a single keyword each, so we use the filter keyword ID as the v1 filter ID. | 	switch m { | ||||||
| 		ID:           filterKeyword.ID, | 	case gtsmodel.FilterActionWarn: | ||||||
| 		Phrase:       filterKeyword.Keyword, | 		return apimodel.FilterActionWarn | ||||||
| 		Context:      apiContexts, | 	case gtsmodel.FilterActionHide: | ||||||
| 		WholeWord:    util.PtrValueOr(filterKeyword.WholeWord, false), | 		return apimodel.FilterActionHide | ||||||
| 		ExpiresAt:    expiresAt, | 	} | ||||||
| 		Irreversible: filter.Action == gtsmodel.FilterActionHide, | 	return apimodel.FilterActionNone | ||||||
| 	}, nil |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| // convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied. | // convertEmojisToAPIEmojis will convert a slice of GTS model emojis to frontend API model emojis, falling back to IDs if no GTS models supplied. | ||||||
|  |  | ||||||
|  | @ -25,6 +25,7 @@ import ( | ||||||
| 	"github.com/stretchr/testify/suite" | 	"github.com/stretchr/testify/suite" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/config" | 	"github.com/superseriousbusiness/gotosocial/internal/config" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/db" | 	"github.com/superseriousbusiness/gotosocial/internal/db" | ||||||
|  | 	statusfilter "github.com/superseriousbusiness/gotosocial/internal/filter/status" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | 	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" | ||||||
| 	"github.com/superseriousbusiness/gotosocial/testrig" | 	"github.com/superseriousbusiness/gotosocial/testrig" | ||||||
| ) | ) | ||||||
|  | @ -427,7 +428,7 @@ func (suite *InternalToFrontendTestSuite) TestLocalInstanceAccountToFrontendBloc | ||||||
| func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { | func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { | ||||||
| 	testStatus := suite.testStatuses["admin_account_status_1"] | 	testStatus := suite.testStatuses["admin_account_status_1"] | ||||||
| 	requestingAccount := suite.testAccounts["local_account_1"] | 	requestingAccount := suite.testAccounts["local_account_1"] | ||||||
| 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount) | 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	b, err := json.MarshalIndent(apiStatus, "", "  ") | 	b, err := json.MarshalIndent(apiStatus, "", "  ") | ||||||
|  | @ -537,11 +538,186 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontend() { | ||||||
| }`, string(b)) | }`, string(b)) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | // Test that a status which is filtered with a warn filter by the requesting user has `filtered` set correctly. | ||||||
|  | func (suite *InternalToFrontendTestSuite) TestWarnFilteredStatusToFrontend() { | ||||||
|  | 	testStatus := suite.testStatuses["admin_account_status_1"] | ||||||
|  | 	testStatus.Content += " fnord" | ||||||
|  | 	testStatus.Text += " fnord" | ||||||
|  | 	requestingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	expectedMatchingFilter := suite.testFilters["local_account_1_filter_1"] | ||||||
|  | 	expectedMatchingFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"] | ||||||
|  | 	expectedMatchingFilterKeyword.Filter = expectedMatchingFilter | ||||||
|  | 	expectedMatchingFilter.Keywords = []*gtsmodel.FilterKeyword{expectedMatchingFilterKeyword} | ||||||
|  | 	requestingAccountFilters := []*gtsmodel.Filter{expectedMatchingFilter} | ||||||
|  | 	apiStatus, err := suite.typeconverter.StatusToAPIStatus( | ||||||
|  | 		context.Background(), | ||||||
|  | 		testStatus, | ||||||
|  | 		requestingAccount, | ||||||
|  | 		statusfilter.FilterContextHome, | ||||||
|  | 		requestingAccountFilters, | ||||||
|  | 	) | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	b, err := json.MarshalIndent(apiStatus, "", "  ") | ||||||
|  | 	suite.NoError(err) | ||||||
|  | 
 | ||||||
|  | 	suite.Equal(`{ | ||||||
|  |   "id": "01F8MH75CBF9JFX4ZAD54N0W0R", | ||||||
|  |   "created_at": "2021-10-20T11:36:45.000Z", | ||||||
|  |   "in_reply_to_id": null, | ||||||
|  |   "in_reply_to_account_id": null, | ||||||
|  |   "sensitive": false, | ||||||
|  |   "spoiler_text": "", | ||||||
|  |   "visibility": "public", | ||||||
|  |   "language": "en", | ||||||
|  |   "uri": "http://localhost:8080/users/admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", | ||||||
|  |   "url": "http://localhost:8080/@admin/statuses/01F8MH75CBF9JFX4ZAD54N0W0R", | ||||||
|  |   "replies_count": 1, | ||||||
|  |   "reblogs_count": 0, | ||||||
|  |   "favourites_count": 1, | ||||||
|  |   "favourited": true, | ||||||
|  |   "reblogged": false, | ||||||
|  |   "muted": false, | ||||||
|  |   "bookmarked": true, | ||||||
|  |   "pinned": false, | ||||||
|  |   "content": "hello world! #welcome ! first post on the instance :rainbow: ! fnord", | ||||||
|  |   "reblog": null, | ||||||
|  |   "application": { | ||||||
|  |     "name": "superseriousbusiness", | ||||||
|  |     "website": "https://superserious.business" | ||||||
|  |   }, | ||||||
|  |   "account": { | ||||||
|  |     "id": "01F8MH17FWEB39HZJ76B6VXSKF", | ||||||
|  |     "username": "admin", | ||||||
|  |     "acct": "admin", | ||||||
|  |     "display_name": "", | ||||||
|  |     "locked": false, | ||||||
|  |     "discoverable": true, | ||||||
|  |     "bot": false, | ||||||
|  |     "created_at": "2022-05-17T13:10:59.000Z", | ||||||
|  |     "note": "", | ||||||
|  |     "url": "http://localhost:8080/@admin", | ||||||
|  |     "avatar": "", | ||||||
|  |     "avatar_static": "", | ||||||
|  |     "header": "http://localhost:8080/assets/default_header.png", | ||||||
|  |     "header_static": "http://localhost:8080/assets/default_header.png", | ||||||
|  |     "followers_count": 1, | ||||||
|  |     "following_count": 1, | ||||||
|  |     "statuses_count": 4, | ||||||
|  |     "last_status_at": "2021-10-20T10:41:37.000Z", | ||||||
|  |     "emojis": [], | ||||||
|  |     "fields": [], | ||||||
|  |     "enable_rss": true, | ||||||
|  |     "role": { | ||||||
|  |       "name": "admin" | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |   "media_attachments": [ | ||||||
|  |     { | ||||||
|  |       "id": "01F8MH6NEM8D7527KZAECTCR76", | ||||||
|  |       "type": "image", | ||||||
|  |       "url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", | ||||||
|  |       "text_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/original/01F8MH6NEM8D7527KZAECTCR76.jpg", | ||||||
|  |       "preview_url": "http://localhost:8080/fileserver/01F8MH17FWEB39HZJ76B6VXSKF/attachment/small/01F8MH6NEM8D7527KZAECTCR76.jpg", | ||||||
|  |       "remote_url": null, | ||||||
|  |       "preview_remote_url": null, | ||||||
|  |       "meta": { | ||||||
|  |         "original": { | ||||||
|  |           "width": 1200, | ||||||
|  |           "height": 630, | ||||||
|  |           "size": "1200x630", | ||||||
|  |           "aspect": 1.9047619 | ||||||
|  |         }, | ||||||
|  |         "small": { | ||||||
|  |           "width": 256, | ||||||
|  |           "height": 134, | ||||||
|  |           "size": "256x134", | ||||||
|  |           "aspect": 1.9104477 | ||||||
|  |         }, | ||||||
|  |         "focus": { | ||||||
|  |           "x": 0, | ||||||
|  |           "y": 0 | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       "description": "Black and white image of some 50's style text saying: Welcome On Board", | ||||||
|  |       "blurhash": "LNJRdVM{00Rj%Mayt7j[4nWBofRj" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "mentions": [], | ||||||
|  |   "tags": [ | ||||||
|  |     { | ||||||
|  |       "name": "welcome", | ||||||
|  |       "url": "http://localhost:8080/tags/welcome" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "emojis": [ | ||||||
|  |     { | ||||||
|  |       "shortcode": "rainbow", | ||||||
|  |       "url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", | ||||||
|  |       "static_url": "http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/static/01F8MH9H8E4VG3KDYJR9EGPXCQ.png", | ||||||
|  |       "visible_in_picker": true, | ||||||
|  |       "category": "reactions" | ||||||
|  |     } | ||||||
|  |   ], | ||||||
|  |   "card": null, | ||||||
|  |   "poll": null, | ||||||
|  |   "text": "hello world! #welcome ! first post on the instance :rainbow: ! fnord", | ||||||
|  |   "filtered": [ | ||||||
|  |     { | ||||||
|  |       "filter": { | ||||||
|  |         "id": "01HN26VM6KZTW1ANNRVSBMA461", | ||||||
|  |         "title": "fnord", | ||||||
|  |         "context": [ | ||||||
|  |           "home", | ||||||
|  |           "public" | ||||||
|  |         ], | ||||||
|  |         "expires_at": null, | ||||||
|  |         "filter_action": "warn", | ||||||
|  |         "keywords": [ | ||||||
|  |           { | ||||||
|  |             "id": "01HN272TAVWAXX72ZX4M8JZ0PS", | ||||||
|  |             "keyword": "fnord", | ||||||
|  |             "whole_word": true | ||||||
|  |           } | ||||||
|  |         ], | ||||||
|  |         "statuses": [] | ||||||
|  |       }, | ||||||
|  |       "keyword_matches": [ | ||||||
|  |         "fnord" | ||||||
|  |       ], | ||||||
|  |       "status_matches": [] | ||||||
|  |     } | ||||||
|  |   ] | ||||||
|  | }`, string(b)) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // Test that a status which is filtered with a hide filter by the requesting user results in the ErrHideStatus error. | ||||||
|  | func (suite *InternalToFrontendTestSuite) TestHideFilteredStatusToFrontend() { | ||||||
|  | 	testStatus := suite.testStatuses["admin_account_status_1"] | ||||||
|  | 	testStatus.Content += " fnord" | ||||||
|  | 	testStatus.Text += " fnord" | ||||||
|  | 	requestingAccount := suite.testAccounts["local_account_1"] | ||||||
|  | 	expectedMatchingFilter := suite.testFilters["local_account_1_filter_1"] | ||||||
|  | 	expectedMatchingFilter.Action = gtsmodel.FilterActionHide | ||||||
|  | 	expectedMatchingFilterKeyword := suite.testFilterKeywords["local_account_1_filter_1_keyword_1"] | ||||||
|  | 	expectedMatchingFilterKeyword.Filter = expectedMatchingFilter | ||||||
|  | 	expectedMatchingFilter.Keywords = []*gtsmodel.FilterKeyword{expectedMatchingFilterKeyword} | ||||||
|  | 	requestingAccountFilters := []*gtsmodel.Filter{expectedMatchingFilter} | ||||||
|  | 	_, err := suite.typeconverter.StatusToAPIStatus( | ||||||
|  | 		context.Background(), | ||||||
|  | 		testStatus, | ||||||
|  | 		requestingAccount, | ||||||
|  | 		statusfilter.FilterContextHome, | ||||||
|  | 		requestingAccountFilters, | ||||||
|  | 	) | ||||||
|  | 	suite.ErrorIs(err, statusfilter.ErrHideStatus) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() { | func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownAttachments() { | ||||||
| 	testStatus := suite.testStatuses["remote_account_2_status_1"] | 	testStatus := suite.testStatuses["remote_account_2_status_1"] | ||||||
| 	requestingAccount := suite.testAccounts["admin_account"] | 	requestingAccount := suite.testAccounts["admin_account"] | ||||||
| 
 | 
 | ||||||
| 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount) | 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	b, err := json.MarshalIndent(apiStatus, "", "  ") | 	b, err := json.MarshalIndent(apiStatus, "", "  ") | ||||||
|  | @ -774,7 +950,7 @@ func (suite *InternalToFrontendTestSuite) TestStatusToFrontendUnknownLanguage() | ||||||
| 	*testStatus = *suite.testStatuses["admin_account_status_1"] | 	*testStatus = *suite.testStatuses["admin_account_status_1"] | ||||||
| 	testStatus.Language = "" | 	testStatus.Language = "" | ||||||
| 	requestingAccount := suite.testAccounts["local_account_1"] | 	requestingAccount := suite.testAccounts["local_account_1"] | ||||||
| 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount) | 	apiStatus, err := suite.typeconverter.StatusToAPIStatus(context.Background(), testStatus, requestingAccount, statusfilter.FilterContextNone, nil) | ||||||
| 	suite.NoError(err) | 	suite.NoError(err) | ||||||
| 
 | 
 | ||||||
| 	b, err := json.MarshalIndent(apiStatus, "", "  ") | 	b, err := json.MarshalIndent(apiStatus, "", "  ") | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue