Merge branch 'main' into filters-v2

This commit is contained in:
tobi 2024-05-02 16:32:04 +02:00
commit 02f2147d51
502 changed files with 22831 additions and 23170 deletions

View file

@ -12,7 +12,7 @@ steps:
# We use golangci-lint for linting.
# See: https://golangci-lint.run/
- name: lint
image: golangci/golangci-lint:v1.55.0
image: golangci/golangci-lint:v1.57.2
volumes:
- name: go-build-cache
path: /root/.cache/go-build
@ -28,7 +28,7 @@ steps:
- pull_request
- name: test
image: golang:1.21-alpine
image: golang:1.22-alpine
volumes:
- name: go-build-cache
path: /root/.cache/go-build
@ -80,7 +80,7 @@ steps:
- yarn --cwd ./web/source build
- name: snapshot
image: superseriousbusiness/gotosocial-drone-build:0.4.0 # https://github.com/superseriousbusiness/gotosocial-drone-build
image: superseriousbusiness/gotosocial-drone-build:0.6.0 # https://github.com/superseriousbusiness/gotosocial-drone-build
volumes:
- name: go-build-cache
path: /root/.cache/go-build
@ -99,7 +99,7 @@ steps:
commands:
# Create a snapshot build with GoReleaser.
- git fetch --tags
- goreleaser release --rm-dist --snapshot
- goreleaser release --clean --snapshot
# Login to Docker, push Docker image snapshots + manifests.
- /go/dockerlogin.sh
@ -121,7 +121,7 @@ steps:
- main
- name: release
image: superseriousbusiness/gotosocial-drone-build:0.4.0 # https://github.com/superseriousbusiness/gotosocial-drone-build
image: superseriousbusiness/gotosocial-drone-build:0.6.0 # https://github.com/superseriousbusiness/gotosocial-drone-build
volumes:
- name: go-build-cache
path: /root/.cache/go-build
@ -136,7 +136,7 @@ steps:
commands:
- git fetch --tags
- /go/dockerlogin.sh
- goreleaser release --rm-dist
- goreleaser release --clean
when:
event:
include:
@ -180,7 +180,7 @@ clone:
steps:
- name: mirror
image: superseriousbusiness/gotosocial-drone-build:0.4.0
image: superseriousbusiness/gotosocial-drone-build:0.6.0
environment:
ORIGIN_REPO: https://github.com/superseriousbusiness/gotosocial
TARGET_REPO: https://codeberg.org/superseriousbusiness/gotosocial
@ -193,6 +193,6 @@ steps:
---
kind: signature
hmac: 4789cebf9156b2c3cb0f097311ea7620e709e4332f130dcae51a938515dc952e
hmac: c07f32c63cbb8180c1a37e46ff1362c1c45586819b52c6de67366ae3504314e4
...

13
.vscode/settings.json vendored
View file

@ -10,5 +10,14 @@
},
"eslint.workingDirectories": ["web/source"],
"eslint.lintTask.enable": true,
"eslint.lintTask.options": "${workspaceFolder}/web/source"
}
"eslint.lintTask.options": "${workspaceFolder}/web/source",
"eslint.validate": [
"javascript",
"javascriptreact",
"typescript",
"typescriptreact"
],
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}

View file

@ -2,11 +2,11 @@
# Dockerfile reference: https://docs.docker.com/engine/reference/builder/
# stage 1: generate up-to-date swagger.yaml to put in the final container
FROM --platform=${BUILDPLATFORM} golang:1.21-alpine AS swagger
FROM --platform=${BUILDPLATFORM} golang:1.22-alpine AS swagger
RUN \
### Installs goswagger for building swagger definitions inside this container
go install "github.com/go-swagger/go-swagger/cmd/swagger@v0.30.5" && \
go install "github.com/go-swagger/go-swagger/cmd/swagger@c46c303aaa02" && \
# Makes swagger executable
chmod +x /go/bin/swagger
@ -28,7 +28,7 @@ RUN yarn --cwd ./web/source install && \
rm -rf ./web/source
# stage 3: build the executor container
FROM --platform=${TARGETPLATFORM} alpine:3.17.2 as executor
FROM --platform=${TARGETPLATFORM} alpine:3.19.1 as executor
# switch to non-root user:group for GtS
USER 1000:1000

View file

@ -128,7 +128,7 @@ Plenty of [config options](./example/config.yaml) for admins to play around with
No external dependencies apart from a database (or just use SQLite!). Simply download the binary + assets (or Docker container), and run.
GoToSocial plays nice with lower-powered machines like Raspberry Pi, old laptops and tiny $5/month VPSes.
GoToSocial plays nice with single-board computers, old laptops and tiny $5/month VPSes.
### Safety + security features
@ -277,10 +277,11 @@ The following open source libraries, frameworks, and tools are used by GoToSocia
- [gruf/go-cache](https://codeberg.org/gruf/go-cache); LRU and TTL caches. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-debug](https://codeberg.org/gruf/go-debug); debug build tag. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-errors](https://codeberg.org/gruf/go-errors); context-like error w/ value wrapping [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-fastcopy](https://codeberg.org/gruf/go-fastcopy); performant pooled I/O copying [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-fastcopy](https://codeberg.org/gruf/go-fastcopy); performant (buffer pooled) I/O copying [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-kv](https://codeberg.org/gruf/go-kv); log field formatting. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-list](https://codeberg.org/gruf/go-list); generic doubly linked list. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-mutexes](https://codeberg.org/gruf/go-mutexes); safemutex & mutex map. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-runners](https://codeberg.org/gruf/go-runners); workerpools and synchronization. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-runners](https://codeberg.org/gruf/go-runners); synchronization utilities. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-sched](https://codeberg.org/gruf/go-sched); task scheduler. [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-store](https://codeberg.org/gruf/go-store); file storage backend (local & s3). [MIT License](https://spdx.org/licenses/MIT.html).
- [gruf/go-structr](https://codeberg.org/gruf/go-structr); struct caching + queueing with automated indexing by field. [MIT License](https://spdx.org/licenses/MIT.html).

View file

@ -39,7 +39,6 @@ func initState(ctx context.Context) (*state.State, error) {
var state state.State
state.Caches.Init()
state.Caches.Start()
state.Workers.Start()
// Set the state DB connection
dbConn, err := bundb.NewBunDBService(ctx, &state)
@ -53,7 +52,6 @@ func initState(ctx context.Context) (*state.State, error) {
func stopState(state *state.State) error {
err := state.DB.Close()
state.Workers.Stop()
state.Caches.Stop()
return err
}

View file

@ -127,8 +127,6 @@ func setupList(ctx context.Context) (*list, error) {
state.Caches.Init()
state.Caches.Start()
state.Workers.Start()
dbService, err := bundb.NewBunDBService(ctx, &state)
if err != nil {
return nil, fmt.Errorf("error creating dbservice: %w", err)
@ -148,7 +146,6 @@ func setupList(ctx context.Context) (*list, error) {
func (l *list) shutdown() error {
l.out.Flush()
err := l.dbService.Close()
l.state.Workers.Stop()
l.state.Caches.Stop()
return err
}

View file

@ -44,7 +44,10 @@ func setupPrune(ctx context.Context) (*prune, error) {
state.Caches.Init()
state.Caches.Start()
state.Workers.Start()
// Scheduler is required for the
// claner, but no other workers
// are needed for this CLI action.
state.Workers.StartScheduler()
dbService, err := bundb.NewBunDBService(ctx, &state)
if err != nil {
@ -85,7 +88,7 @@ func (p *prune) shutdown() error {
errs.Appendf("error stopping database: %w", err)
}
p.state.Workers.Stop()
p.state.Workers.Scheduler.Stop()
p.state.Caches.Stop()
return errs.Combine()

View file

@ -35,6 +35,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/filter/spam"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/metrics"
"github.com/superseriousbusiness/gotosocial/internal/middleware"
tlprocessor "github.com/superseriousbusiness/gotosocial/internal/processing/timeline"
@ -128,25 +129,6 @@ var Start action.GTSAction = func(ctx context.Context) error {
TLSInsecureSkipVerify: config.GetHTTPClientTLSInsecureSkipVerify(),
})
// Initialize delivery worker with http client.
state.Workers.Delivery.Init(client)
// Initialize workers.
state.Workers.Start()
defer state.Workers.Stop()
// Add a task to the scheduler to sweep caches.
// Frequency = 1 * minute
// Threshold = 80% capacity
_ = state.Workers.Scheduler.AddRecurring(
"@cachesweep", // id
time.Time{}, // start
time.Minute, // freq
func(context.Context, time.Time) {
state.Caches.Sweep(60)
},
)
// Build handlers used in later initializations.
mediaManager := media.NewManager(&state)
oauthServer := oauth.New(ctx, dbService)
@ -195,10 +177,27 @@ var Start action.GTSAction = func(ctx context.Context) error {
return fmt.Errorf("error starting list timeline: %s", err)
}
// Create a media cleaner using the given state.
// Start the job scheduler
// (this is required for cleaner).
state.Workers.StartScheduler()
// Add a task to the scheduler to sweep caches.
// Frequency = 1 * minute
// Threshold = 60% capacity
_ = state.Workers.Scheduler.AddRecurring(
"@cachesweep", // id
time.Time{}, // start
time.Minute, // freq
func(context.Context, time.Time) {
state.Caches.Sweep(60)
},
)
// Create background cleaner.
cleaner := cleaner.New(&state)
// Create the processor using all the other services we've created so far.
// Create the processor using all the
// other services we've created so far.
processor := processing.NewProcessor(
cleaner,
typeConverter,
@ -209,13 +208,16 @@ var Start action.GTSAction = func(ctx context.Context) error {
emailSender,
)
// Set state client / federator asynchronous worker enqueue functions
state.Workers.EnqueueClientAPI = processor.Workers().EnqueueClientAPI
state.Workers.EnqueueFediAPI = processor.Workers().EnqueueFediAPI
// Initialize the specialized workers.
state.Workers.Client.Init(messages.ClientMsgIndices())
state.Workers.Federator.Init(messages.FederatorMsgIndices())
state.Workers.Delivery.Init(client)
state.Workers.Client.Process = processor.Workers().ProcessFromClientAPI
state.Workers.Federator.Process = processor.Workers().ProcessFromFediAPI
// Set state client / federator synchronous processing functions.
state.Workers.ProcessFromClientAPI = processor.Workers().ProcessFromClientAPI
state.Workers.ProcessFromFediAPI = processor.Workers().ProcessFromFediAPI
// Initialize workers.
state.Workers.Start()
defer state.Workers.Stop()
// Schedule tasks for all existing poll expiries.
if err := processor.Polls().ScheduleAll(ctx); err != nil {

13
docs/admin/robots.md Normal file
View file

@ -0,0 +1,13 @@
# Robots.txt
GoToSocial serves a `robots.txt` file on the host domain. This file contains rules that attempt to block known AI scrapers, as well as some other indexers. It also includes some rules to ensure things like API endpoints aren't indexed by search engines since there really isn't any point to them.
## AI scrapers
The AI scrapers come from a [community maintained repository][airobots]. It's manually kept in sync for the time being. If you know of any missing robots, please send them a PR!
A number of AI scrapers are known to ignore entries in `robots.txt` even if it explicitly matches their User-Agent. This means the `robots.txt` file is not a foolproof way of ensuring AI scrapers don't grab your content.
If you want to block these things fully, you'll need to block based on the User-Agent header in a reverse proxy until GoToSocial can filter requests by User-Agent header.
[airobots]: https://github.com/ai-robots-txt/ai.robots.txt/

View file

@ -3774,11 +3774,13 @@ paths:
/api/v1/admin/accounts:
get:
description: |-
Returned accounts will be ordered alphabetically (a-z) by domain + username.
The next and previous queries can be parsed from the returned Link header.
Example:
```
<https://example.org/api/v1/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
<https://example.org/api/v1/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
````
operationId: adminAccountsGetV1
parameters:
@ -3847,19 +3849,15 @@ paths:
in: query
name: staff
type: boolean
- description: All results returned will be older than the item with this ID.
- description: max_id in the form `[domain]/@[username]`. All results returned will be later in the alphabet than `[domain]/@[username]`. For example, if max_id = `example.org/@someone` then returned entries might contain `example.org/@someone_else`, `later.example.org/@someone`, etc. Local account IDs in this form use an empty string for the `[domain]` part, for example local account with username `someone` would be `/@someone`.
in: query
name: max_id
type: string
- description: All results returned will be newer than the item with this ID.
in: query
name: since_id
type: string
- description: Returns results immediately newer than the item with this ID.
- description: min_id in the form `[domain]/@[username]`. All results returned will be earlier in the alphabet than `[domain]/@[username]`. For example, if min_id = `example.org/@someone` then returned entries might contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc. Local account IDs in this form use an empty string for the `[domain]` part, for example local account with username `someone` would be `/@someone`.
in: query
name: min_id
type: string
- default: 100
- default: 50
description: Maximum number of results to return.
in: query
maximum: 200
@ -8463,11 +8461,13 @@ paths:
/api/v2/admin/accounts:
get:
description: |-
Returned accounts will be ordered alphabetically (a-z) by domain + username.
The next and previous queries can be parsed from the returned Link header.
Example:
```
<https://example.org/api/v2/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
<https://example.org/api/v2/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
````
operationId: adminAccountsGetV2
parameters:
@ -8513,19 +8513,15 @@ paths:
in: query
name: ip
type: string
- description: All results returned will be older than the item with this ID.
- description: max_id in the form `[domain]/@[username]`. All results returned will be later in the alphabet than `[domain]/@[username]`. For example, if max_id = `example.org/@someone` then returned entries might contain `example.org/@someone_else`, `later.example.org/@someone`, etc. Local account IDs in this form use an empty string for the `[domain]` part, for example local account with username `someone` would be `/@someone`.
in: query
name: max_id
type: string
- description: All results returned will be newer than the item with this ID.
in: query
name: since_id
type: string
- description: Returns results immediately newer than the item with this ID.
- description: min_id in the form `[domain]/@[username]`. All results returned will be earlier in the alphabet than `[domain]/@[username]`. For example, if min_id = `example.org/@someone` then returned entries might contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc. Local account IDs in this form use an empty string for the `[domain]` part, for example local account with username `someone` would be `/@someone`.
in: query
name: min_id
type: string
- default: 100
- default: 50
description: Maximum number of results to return.
in: query
maximum: 200

View file

@ -6,7 +6,7 @@ By default, GoToSocial will use Postgres, but this is easy to change.
## SQLite
SQLite, as the name implies, is the lightest database type that GoToSocial can use. It stores entries in a simple file format, usually in the same directory as the GoToSocial binary itself. SQLite is great for small instances and lower-powered machines like Raspberry Pi, where a dedicated database would be overkill.
SQLite, as the name implies, is the lightest database type that GoToSocial can use. It stores entries in a simple file format, usually in the same directory as the GoToSocial binary itself. SQLite is great for small instances and single-board computers, where a dedicated database would be overkill.
To configure GoToSocial to use SQLite, change `db-type` to `sqlite`. The `address` setting will then be a filename instead of an address, so you will want to change it to `sqlite.db` or something similar.

82
go.mod
View file

@ -1,10 +1,10 @@
module github.com/superseriousbusiness/gotosocial
go 1.21
go 1.22
replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.7-concurrency-workaround
replace modernc.org/sqlite => gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround
toolchain go1.21.3
toolchain go1.22.2
require (
codeberg.org/gruf/go-bytes v1.0.2
@ -16,15 +16,16 @@ require (
codeberg.org/gruf/go-fastcopy v1.1.2
codeberg.org/gruf/go-iotools v0.0.0-20230811115124-5d4223615a7f
codeberg.org/gruf/go-kv v1.6.4
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f
codeberg.org/gruf/go-logger/v2 v2.2.1
codeberg.org/gruf/go-mutexes v1.4.0
codeberg.org/gruf/go-mutexes v1.4.1
codeberg.org/gruf/go-runners v1.6.2
codeberg.org/gruf/go-sched v1.2.3
codeberg.org/gruf/go-store/v2 v2.2.4
codeberg.org/gruf/go-structr v0.6.2
codeberg.org/gruf/go-structr v0.7.0
codeberg.org/superseriousbusiness/exif-terminator v0.7.0
github.com/DmitriyVTitov/size v1.5.0
github.com/KimMachineGun/automemlimit v0.5.0
github.com/KimMachineGun/automemlimit v0.6.0
github.com/abema/go-mp4 v1.2.0
github.com/buckket/go-blurhash v1.1.0
github.com/coreos/go-oidc/v3 v3.10.0
@ -34,15 +35,15 @@ require (
github.com/gin-contrib/sessions v1.0.0
github.com/gin-gonic/gin v1.9.1
github.com/go-playground/form/v4 v4.2.1
github.com/go-swagger/go-swagger v0.30.5
github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02
github.com/google/uuid v1.6.0
github.com/gorilla/feeds v1.1.2
github.com/gorilla/websocket v1.5.1
github.com/h2non/filetype v1.1.3
github.com/jackc/pgx/v5 v5.5.5
github.com/microcosm-cc/bluemonday v1.0.26
github.com/miekg/dns v1.1.58
github.com/minio/minio-go/v7 v7.0.69
github.com/miekg/dns v1.1.59
github.com/minio/minio-go/v7 v7.0.70
github.com/mitchellh/mapstructure v1.5.0
github.com/oklog/ulid v1.3.1
github.com/prometheus/client_golang v1.18.0
@ -52,7 +53,7 @@ require (
github.com/superseriousbusiness/activity v1.6.0-gts.0.20240408131430-247f7f7110f0
github.com/superseriousbusiness/httpsig v1.2.0-SSB
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8
github.com/tdewolff/minify/v2 v2.20.19
github.com/tdewolff/minify/v2 v2.20.20
github.com/technologize/otel-go-contrib v1.1.1
github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80
github.com/ulule/limiter/v3 v3.11.2
@ -62,14 +63,14 @@ require (
github.com/uptrace/bun/extra/bunotel v1.2.1
github.com/wagslane/go-password-validator v0.3.0
github.com/yuin/goldmark v1.7.1
go.opentelemetry.io/otel v1.25.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0
go.opentelemetry.io/otel v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0
go.opentelemetry.io/otel/exporters/prometheus v0.46.0
go.opentelemetry.io/otel/metric v1.25.0
go.opentelemetry.io/otel/sdk v1.25.0
go.opentelemetry.io/otel/metric v1.26.0
go.opentelemetry.io/otel/sdk v1.26.0
go.opentelemetry.io/otel/sdk/metric v1.24.0
go.opentelemetry.io/otel/trace v1.25.0
go.opentelemetry.io/otel/trace v1.26.0
go.uber.org/automaxprocs v1.5.3
golang.org/x/crypto v0.22.0
golang.org/x/image v0.15.0
@ -78,7 +79,7 @@ require (
golang.org/x/text v0.14.0
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.29.5
modernc.org/sqlite v1.29.8
mvdan.cc/xurls/v2 v2.5.0
)
@ -89,7 +90,7 @@ require (
codeberg.org/gruf/go-mangler v1.3.0 // indirect
codeberg.org/gruf/go-maps v1.0.3 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
@ -111,7 +112,7 @@ require (
github.com/dsoprea/go-photoshop-info-format v0.0.0-20200610045659-121dd752914d // indirect
github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
@ -120,17 +121,17 @@ require (
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/analysis v0.21.4 // indirect
github.com/go-openapi/errors v0.20.4 // indirect
github.com/go-openapi/inflect v0.19.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/loads v0.21.2 // indirect
github.com/go-openapi/runtime v0.26.0 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/strfmt v0.21.7 // indirect
github.com/go-openapi/swag v0.22.4 // indirect
github.com/go-openapi/validate v0.22.1 // indirect
github.com/go-openapi/analysis v0.23.0 // indirect
github.com/go-openapi/errors v0.22.0 // indirect
github.com/go-openapi/inflect v0.21.0 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/loads v0.22.0 // indirect
github.com/go-openapi/runtime v0.28.0 // indirect
github.com/go-openapi/spec v0.21.0 // indirect
github.com/go-openapi/strfmt v0.23.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-openapi/validate v0.24.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.19.0 // indirect
@ -141,14 +142,14 @@ require (
github.com/golang/geo v0.0.0-20210211234256-740aa86cb551 // indirect
github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/handlers v1.5.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
@ -167,7 +168,6 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/sha256-simd v1.0.1 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@ -183,11 +183,11 @@ require (
github.com/prometheus/procfs v0.12.0 // indirect
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rogpeppe/go-internal v1.12.0 // indirect
github.com/rs/xid v1.5.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
@ -196,7 +196,7 @@ require (
github.com/subosito/gotenv v1.6.0 // indirect
github.com/superseriousbusiness/go-jpeg-image-structure/v2 v2.0.0-20220321154430-d89a106fdabe // indirect
github.com/superseriousbusiness/go-png-image-structure/v2 v2.0.1-SSB // indirect
github.com/tdewolff/parse/v2 v2.7.12 // indirect
github.com/tdewolff/parse/v2 v2.7.13 // indirect
github.com/tmthrgd/go-hex v0.0.0-20190904060850-447a3041c3bc // indirect
github.com/toqueteos/webbrowser v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
@ -205,18 +205,18 @@ require (
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
go.mongodb.org/mongo-driver v1.14.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 // indirect
go.opentelemetry.io/proto/otlp v1.1.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 // indirect
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/arch v0.7.0 // indirect
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 // indirect
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect
google.golang.org/grpc v1.63.0 // indirect
google.golang.org/grpc v1.63.2 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

261
go.sum
View file

@ -56,6 +56,8 @@ codeberg.org/gruf/go-iotools v0.0.0-20230811115124-5d4223615a7f h1:Kazm/PInN2m1S
codeberg.org/gruf/go-iotools v0.0.0-20230811115124-5d4223615a7f/go.mod h1:B8uq4yHtIcKXhBZT9C/SYisz25lldLHMVpwZPz4ADLQ=
codeberg.org/gruf/go-kv v1.6.4 h1:3NZiW8HVdBM3kpOiLb7XfRiihnzZWMAixdCznguhILk=
codeberg.org/gruf/go-kv v1.6.4/go.mod h1:O/YkSvKiS9XsRolM3rqCd9YJmND7dAXu9z+PrlYO4bc=
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f h1:Ss6Z+vygy+jOGhj96d/GwsYYDd22QmIcH74zM7/nQkw=
codeberg.org/gruf/go-list v0.0.0-20240425093752-494db03d641f/go.mod h1:F9pl4h34iuVN7kucKam9fLwsItTc+9mmaKt7pNXRd/4=
codeberg.org/gruf/go-logger/v2 v2.2.1 h1:RP2u059EQKTBFV3cN8X6xDxNk2RkzqdgXGKflKqB7Oc=
codeberg.org/gruf/go-logger/v2 v2.2.1/go.mod h1:m/vBfG5jNUmYXI8Hg9aVSk7Pn8YgEBITQB/B/CzdRss=
codeberg.org/gruf/go-loosy v0.0.0-20231007123304-bb910d1ab5c4 h1:IXwfoU7f2whT6+JKIKskNl/hBlmWmnF1vZd84Eb3cyA=
@ -64,16 +66,16 @@ codeberg.org/gruf/go-mangler v1.3.0 h1:cf0vuuLJuEhoIukPHj+MUBIQSWxZcfEYt2Eo/r7Rs
codeberg.org/gruf/go-mangler v1.3.0/go.mod h1:jnOA76AQoaO2kTHi0DlTTVaFYfRM+9fzs8f4XO6MsOk=
codeberg.org/gruf/go-maps v1.0.3 h1:VDwhnnaVNUIy5O93CvkcE2IZXnMB1+IJjzfop9V12es=
codeberg.org/gruf/go-maps v1.0.3/go.mod h1:D5LNDxlC9rsDuVQVM6JObaVGAdHB6g2dTdOdkh1aXWA=
codeberg.org/gruf/go-mutexes v1.4.0 h1:53H6bFDRcG6rjk3iOTuGaStT/VTFdU5Uw8Dszy88a8g=
codeberg.org/gruf/go-mutexes v1.4.0/go.mod h1:1j/6/MBeBQUedAtAtysLLnBKogfOZAxdym0E3wlaBD8=
codeberg.org/gruf/go-mutexes v1.4.1 h1:SgsuktbbrkKYmgZ1reA5jMGEZbXJ6GQ54fSlKRN5iug=
codeberg.org/gruf/go-mutexes v1.4.1/go.mod h1:1j/6/MBeBQUedAtAtysLLnBKogfOZAxdym0E3wlaBD8=
codeberg.org/gruf/go-runners v1.6.2 h1:oQef9niahfHu/wch14xNxlRMP8i+ABXH1Cb9PzZ4oYo=
codeberg.org/gruf/go-runners v1.6.2/go.mod h1:Tq5PrZ/m/rBXbLZz0u5if+yP3nG5Sf6S8O/GnyEePeQ=
codeberg.org/gruf/go-sched v1.2.3 h1:H5ViDxxzOBR3uIyGBCf0eH8b1L8wMybOXcdtUUTXZHk=
codeberg.org/gruf/go-sched v1.2.3/go.mod h1:vT9uB6KWFIIwnG9vcPY2a0alYNoqdL1mSzRM8I+PK7A=
codeberg.org/gruf/go-store/v2 v2.2.4 h1:8HO1Jh2gg7boQKA3hsDAIXd9zwieu5uXwDXEcTOD9js=
codeberg.org/gruf/go-store/v2 v2.2.4/go.mod h1:zI4VWe5CpXAktYMtaBMrgA5QmO0sQH53LBRvfn1huys=
codeberg.org/gruf/go-structr v0.6.2 h1:1zs7UkPBsRGRDMHhrfFL7GrwAyPHxFXCchu8ADv/zuM=
codeberg.org/gruf/go-structr v0.6.2/go.mod h1:K1FXkUyO6N/JKt8aWqyQ8rtW7Z9ZmXKWP8mFAQ2OJjE=
codeberg.org/gruf/go-structr v0.7.0 h1:gy0/wD7718HwJDoBMeMumk4+7veLrkumgCEOnCyzS8w=
codeberg.org/gruf/go-structr v0.7.0/go.mod h1:K1FXkUyO6N/JKt8aWqyQ8rtW7Z9ZmXKWP8mFAQ2OJjE=
codeberg.org/superseriousbusiness/exif-terminator v0.7.0 h1:Y6VApSXhKqExG0H2hZ2JelRK4xmWdjDQjn13CpEfzko=
codeberg.org/superseriousbusiness/exif-terminator v0.7.0/go.mod h1:gCWKduudUWFzsnixoMzu0FYVdxHWG+AbXnZ50DqxsUE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
@ -81,16 +83,15 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DmitriyVTitov/size v1.5.0 h1:/PzqxYrOyOUX1BXj6J9OuVRVGe+66VL4D9FlUaW515g=
github.com/DmitriyVTitov/size v1.5.0/go.mod h1:le6rNI4CoLQV1b9gzp1+3d7hMAD/uu2QcJ+aYbNgiU0=
github.com/KimMachineGun/automemlimit v0.5.0 h1:BeOe+BbJc8L5chL3OwzVYjVzyvPALdd5wxVVOWuUZmQ=
github.com/KimMachineGun/automemlimit v0.5.0/go.mod h1:di3GCKiu9Y+1fs92erCbUvKzPkNyViN3mA0vti/ykEQ=
github.com/KimMachineGun/automemlimit v0.6.0 h1:p/BXkH+K40Hax+PuWWPQ478hPjsp9h1CPDhLlA3Z37E=
github.com/KimMachineGun/automemlimit v0.6.0/go.mod h1:T7xYht7B8r6AG/AqFcUdc7fzd2bIdBKmepfP2S1svPY=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7YgDP83g=
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0=
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/abema/go-mp4 v1.2.0 h1:gi4X8xg/m179N/J15Fn5ugywN9vtI6PLk6iLldHGLAk=
github.com/abema/go-mp4 v1.2.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
@ -98,7 +99,6 @@ github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY
github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@ -176,9 +176,8 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -218,46 +217,28 @@ github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-openapi/analysis v0.21.2/go.mod h1:HZwRk4RRisyG8vx2Oe6aqeSQcoxRp47Xkp3+K6q+LdY=
github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc=
github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo=
github.com/go-openapi/errors v0.19.8/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.19.9/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M=
github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M=
github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk=
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/loads v0.21.1/go.mod h1:/DtAMXXneXFjbQMGEtbamCZb+4x7eGwkvZCvBmwUG+g=
github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro=
github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw=
github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc=
github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ=
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
github.com/go-openapi/spec v0.20.6/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8=
github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/strfmt v0.21.0/go.mod h1:ZRQ409bWMj+SOgXofQAGTIo2Ebu72Gs+WaRADcS5iNg=
github.com/go-openapi/strfmt v0.21.1/go.mod h1:I/XVKeLc5+MM5oPNN7P6urMOpuLXEcNrCX/rPGuWb0k=
github.com/go-openapi/strfmt v0.21.3/go.mod h1:k+RzNO0Da+k3FrrynSNN8F7n/peCmQQqbbXjtDfvmGg=
github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k=
github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU=
github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU=
github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg=
github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU=
github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo=
github.com/go-openapi/errors v0.22.0 h1:c4xY/OLxUBSTiepAg3j/MHuAv5mJhnf53LLMWFB+u/w=
github.com/go-openapi/errors v0.22.0/go.mod h1:J3DmZScxCDufmIMsdOuDHxJbdOGC0xtUynjIx092vXE=
github.com/go-openapi/inflect v0.21.0 h1:FoBjBTQEcbg2cJUWX6uwL9OyIW8eqc9k4KhN4lfbeYk=
github.com/go-openapi/inflect v0.21.0/go.mod h1:INezMuUu7SJQc2AyR3WO0DqqYUJSj8Kb4hBd7WtjlAw=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/loads v0.22.0 h1:ECPGd4jX1U6NApCGG1We+uEozOAvXvJSF4nnwHZ8Aco=
github.com/go-openapi/loads v0.22.0/go.mod h1:yLsaTCS92mnSAZX5WWoxszLj0u+Ojl+Zs5Stn1oF+rs=
github.com/go-openapi/runtime v0.28.0 h1:gpPPmWSNGo214l6n8hzdXYhPuJcGtziTOgUpvsFWGIQ=
github.com/go-openapi/runtime v0.28.0/go.mod h1:QN7OzcS+XuYmkQLw05akXk0jRH/eZ3kb18+1KwW9gyc=
github.com/go-openapi/spec v0.21.0 h1:LTVzPc3p/RzRnkQqLRndbAzjY0d0BCL72A6j3CdL9ZY=
github.com/go-openapi/spec v0.21.0/go.mod h1:78u6VdPw81XU44qEWGhtr982gJ5BWg2c0I5XwVMotYk=
github.com/go-openapi/strfmt v0.23.0 h1:nlUS6BCqcnAk0pyhi9Y+kdDVZdZMHfEKQiS4HaMgO/c=
github.com/go-openapi/strfmt v0.23.0/go.mod h1:NrtIpfKtWIygRkKVsxh7XQMDQW5HKQl6S5ik2elW+K4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58=
github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@ -270,39 +251,12 @@ github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91
github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4=
github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-swagger/go-swagger v0.30.5 h1:SQ2+xSonWjjoEMOV5tcOnZJVlfyUfCBhGQGArS1b9+U=
github.com/go-swagger/go-swagger v0.30.5/go.mod h1:cWUhSyCNqV7J1wkkxfr5QmbcnCewetCdvEXqgPvbc/Q=
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013 h1:l9rI6sNaZgNC0LnF3MiE+qTmyBA/tZAg1rtyrGbUMK0=
github.com/go-swagger/scan-repo-boundary v0.0.0-20180623220736-973b3573c013/go.mod h1:b65mBPzqzZWxOZGxSWrqs4GInLIn+u99Q9q7p+GKni0=
github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02 h1:J6YiT/eg3gAfKMdVCkWXe6khsO+nxa8W4URZ4AUqzbA=
github.com/go-swagger/go-swagger v0.30.6-0.20240418033037-c46c303aaa02/go.mod h1:i1/E+d8iPNReSE7y04FaVu5OPKB3il5cn+T1Egogg3I=
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 h1:PSPmmucxGiFBtbQcttHTUc4LQ3P09AW+ldO2qspyKdY=
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/gobuffalo/attrs v0.0.0-20190224210810-a9411de4debd/go.mod h1:4duuawTqi2wkkpB4ePgWMaai6/Kc6WEz83bhFwpHzj0=
github.com/gobuffalo/depgen v0.0.0-20190329151759-d478694a28d3/go.mod h1:3STtPUQYuzV0gBVOY3vy6CfMm/ljR4pABfrTeHNLHUY=
github.com/gobuffalo/depgen v0.1.0/go.mod h1:+ifsuy7fhi15RWncXQQKjWS9JPkdah5sZvtHc2RXGlg=
github.com/gobuffalo/envy v1.6.15/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
github.com/gobuffalo/flect v0.1.0/go.mod h1:d2ehjJqGOH/Kjqcoz+F7jHTBbmDb38yXA598Hb50EGs=
github.com/gobuffalo/flect v0.1.1/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/flect v0.1.3/go.mod h1:8JCgGVbRjJhVgD6399mQr4fx5rRfGKVzFjbj6RE/9UI=
github.com/gobuffalo/genny v0.0.0-20190329151137-27723ad26ef9/go.mod h1:rWs4Z12d1Zbf19rlsn0nurr75KqhYp52EAGGxTbBhNk=
github.com/gobuffalo/genny v0.0.0-20190403191548-3ca520ef0d9e/go.mod h1:80lIj3kVJWwOrXWWMRzzdhW3DsrdjILVil/SFKBzF28=
github.com/gobuffalo/genny v0.1.0/go.mod h1:XidbUqzak3lHdS//TPu2OgiFB+51Ur5f7CSnXZ/JDvo=
github.com/gobuffalo/genny v0.1.1/go.mod h1:5TExbEyY48pfunL4QSXxlDOmdsD44RRq4mVZ0Ex28Xk=
github.com/gobuffalo/gitgen v0.0.0-20190315122116-cc086187d211/go.mod h1:vEHJk/E9DmhejeLeNt7UVvlSGv3ziL+djtTr3yyzcOw=
github.com/gobuffalo/gogen v0.0.0-20190315121717-8f38393713f5/go.mod h1:V9QVDIxsgKNZs6L2IYiGR8datgMhB577vzTDqypH360=
github.com/gobuffalo/gogen v0.1.0/go.mod h1:8NTelM5qd8RZ15VjQTFkAW6qOMx5wBbW4dSCS3BY8gg=
github.com/gobuffalo/gogen v0.1.1/go.mod h1:y8iBtmHmGc4qa3urIyo1shvOD8JftTtfcKi+71xfDNE=
github.com/gobuffalo/logger v0.0.0-20190315122211-86e12af44bc2/go.mod h1:QdxcLw541hSGtBnhUc4gaNIXRjiDppFGaDqzbrBd3v8=
github.com/gobuffalo/mapi v1.0.1/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/mapi v1.0.2/go.mod h1:4VAGh89y6rVOvm5A8fKFxYG+wIW6LO1FMTG9hnKStFc=
github.com/gobuffalo/packd v0.0.0-20190315124812-a385830c7fc0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packd v0.1.0/go.mod h1:M2Juc+hhDXf/PnmBANFCqx4DM3wRbgDvnVWeG2RIxq4=
github.com/gobuffalo/packr/v2 v2.0.9/go.mod h1:emmyGweYTm6Kdper+iywB6YK5YzuKchGtJQZ0Odn4pQ=
github.com/gobuffalo/packr/v2 v2.2.0/go.mod h1:CaAwI0GPIAv+5wKLtv8Afwl+Cm78K/I/VCm/3ptBN+0=
github.com/gobuffalo/syncx v0.0.0-20190224160051-33c29581e754/go.mod h1:HhnNqWY95UYwwW3uSASeV7vtgYkT2t16hJgV3AEPUpw=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
@ -342,7 +296,6 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@ -352,7 +305,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
@ -386,8 +338,8 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/gorilla/feeds v1.1.2 h1:pxzZ5PD3RJdhFH2FsJJ4x6PqMqbgFk1+Vez4XWBW8Iw=
github.com/gorilla/feeds v1.1.2/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/handlers v1.5.2 h1:cLTUSsNkgcwhgRqvCNmdbRWG0A3N4F+M2nWKdScwyEE=
github.com/gorilla/handlers v1.5.2/go.mod h1:dX+xVpaxdSw+q0Qek8SSsl3dfMk3jNddUkMzo0GtH0w=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
@ -395,8 +347,8 @@ github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8L
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
@ -406,15 +358,15 @@ github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU=
github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4=
github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@ -430,7 +382,6 @@ github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LF
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
@ -440,12 +391,9 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
github.com/karrick/godirwalk v1.8.0/go.mod h1:H5KPZjojv4lE+QYImBI8xVtrBRgYrIVsaRPx4tDPEn4=
github.com/karrick/godirwalk v1.10.3/go.mod h1:RoGL9dQei4vP9ilrpETWE8CLOZ1kiN0LhBygSwrAsHA=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.10.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
@ -453,10 +401,7 @@ github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
@ -468,13 +413,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/markbates/oncer v0.0.0-20181203154359-bf2de49a0be2/go.mod h1:Ld9puTsIW75CHf65OeIOkyKbteujpZVXDpWK6YGZbxE=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@ -485,21 +425,17 @@ github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvls
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.69 h1:l8AnsQFyY1xiwa/DaQskY4NXSLA2yrGsW5iD9nRPVS0=
github.com/minio/minio-go/v7 v7.0.69/go.mod h1:XAvOPJQ5Xlzk5o3o/ArO2NMbhSGkimC+bpW/ngRKDmQ=
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJnn2g=
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/mitchellh/mapstructure v1.3.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
@ -510,12 +446,10 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@ -530,12 +464,9 @@ github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGd
github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0=
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y=
github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -556,12 +487,10 @@ github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b h1:aUNXCGgukb4gtY
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.2.2/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc=
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
@ -572,11 +501,9 @@ github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWR
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
@ -590,16 +517,13 @@ github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNo
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
@ -628,10 +552,10 @@ github.com/superseriousbusiness/httpsig v1.2.0-SSB h1:BinBGKbf2LSuVT5+MuH0XynHN9
github.com/superseriousbusiness/httpsig v1.2.0-SSB/go.mod h1:+rxfATjFaDoDIVaJOTSP0gj6UrbicaYPEptvCLC9F28=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8 h1:nTIhuP157oOFcscuoK1kCme1xTeGIzztSw70lX9NrDQ=
github.com/superseriousbusiness/oauth2/v4 v4.3.2-SSB.0.20230227143000-f4900831d6c8/go.mod h1:uYC/W92oVRJ49Vh1GcvTqpeFqHi+Ovrl2sMllQWRAEo=
github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo=
github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM=
github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ=
github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk=
github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k=
github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI=
github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA=
github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo=
github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739/go.mod h1:XPuWBzvdUzhCuxWO1ojpXsyzsA5bFoS3tO/Q3kFuTG8=
@ -691,11 +615,6 @@ github.com/wagslane/go-password-validator v0.3.0 h1:vfxOPzGHkz5S146HDpavl0cw1DSV
github.com/wagslane/go-password-validator v0.3.0/go.mod h1:TI1XJ6T5fRdRnHqHt14pvy1tNVnrwe7m3/f1f2fDphQ=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.0.2/go.mod h1:1WAq6h33pAW+iRreB34OORO2Nf7qel3VV3fjBj+hCSs=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.2/go.mod h1:8F9zXuvzgwmyT5DUm4GUfZGDdT3W+LCvS6+da4O5kxM=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
@ -704,7 +623,6 @@ github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7JulP+udvsHwJoVG1YGAP6VLg4y9I5dyZdqmA=
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
@ -716,11 +634,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.7-concurrency-workaround h1:N4h6T8jb9BZTor6d4XJYaKYEh3KNAydpuydR2N1hPRc=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.7-concurrency-workaround/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk=
go.mongodb.org/mongo-driver v1.7.3/go.mod h1:NqaYOwnXWr5Pm7AOpO5QFxKJ503nbMse/R79oO62zWg=
go.mongodb.org/mongo-driver v1.7.5/go.mod h1:VXEWRZ6URJIkUq2SCAyapmhH0ZLRBP+FT4xhp5Zvxng=
go.mongodb.org/mongo-driver v1.10.0/go.mod h1:wsihk0Kdgv8Kqu1Anit4sfK+22vSFbUrAVEYRhCXrA8=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround h1:ESobxED9bfE0nOQP/WPv9+tMR8oZoDIWRKlNK2Vs4Ms=
gitlab.com/NyaaaWhatsUpDoc/sqlite v1.29.8-concurrency-workaround/go.mod h1:lQPm27iqa4UNZpmr4Aor0MH0HkCLbt1huYDfWylLZFk=
go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80=
go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -728,26 +643,26 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
go.opentelemetry.io/otel v1.25.0 h1:gldB5FfhRl7OJQbUHt/8s0a7cE8fbsPAtdpRaApKy4k=
go.opentelemetry.io/otel v1.25.0/go.mod h1:Wa2ds5NOXEMkCmUou1WA7ZBfLTHWIsp034OVD7AO+Vg=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0 h1:vOL89uRfOCCNIjkisd0r7SEdJF3ZJFyCNY34fdZs8eU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.25.0/go.mod h1:8GlBGcDk8KKi7n+2S4BT/CPZQYH3erLu0/k64r1MYgo=
go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs=
go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0 h1:Waw9Wfpo/IXzOI8bCB7DIk+0JZcqqsyn1JFnAc+iam8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.26.0/go.mod h1:wnJIG4fOqyynOnnQF/eQb4/16VlX2EJAHhHgqIqWfAo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/exporters/prometheus v0.46.0 h1:I8WIFXR351FoLJYuloU4EgXbtNX2URfU/85pUPheIEQ=
go.opentelemetry.io/otel/exporters/prometheus v0.46.0/go.mod h1:ztwVUHe5DTR/1v7PeuGRnU5Bbd4QKYwApWmuutKsJSs=
go.opentelemetry.io/otel/metric v1.25.0 h1:LUKbS7ArpFL/I2jJHdJcqMGxkRdxpPHE0VU/D4NuEwA=
go.opentelemetry.io/otel/metric v1.25.0/go.mod h1:rkDLUSd2lC5lq2dFNrX9LGAbINP5B7WBkC78RXCpH5s=
go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo=
go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw=
go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30=
go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4=
go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8=
go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs=
go.opentelemetry.io/otel/sdk/metric v1.24.0 h1:yyMQrPzF+k88/DbH7o4FMAs80puqd+9osbiBrJrz/w8=
go.opentelemetry.io/otel/sdk/metric v1.24.0/go.mod h1:I6Y5FjH6rvEnTTAYQz3Mmv2kl6Ek5IIrmwTLqMrrOE0=
go.opentelemetry.io/otel/trace v1.25.0 h1:tqukZGLwQYRIFtSQM2u2+yfMVTgGVeqRLPUYx1Dq6RM=
go.opentelemetry.io/otel/trace v1.25.0/go.mod h1:hCCs70XM/ljO+BeQkyFnbK28SBIJ/Emuha+ccrCRT7I=
go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI=
go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY=
go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA=
go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0=
go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94=
go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A=
go.uber.org/automaxprocs v1.5.3 h1:kWazyxZUrS3Gs4qUpbwo5kEIMGe/DAvi5Z4tl2NW4j8=
go.uber.org/automaxprocs v1.5.3/go.mod h1:eRbA25aqJrxAbsLO0xy5jVwPt7FQnRgjW+efnwa1WM0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@ -757,16 +672,12 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190422162423-af44ce270edf/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
@ -780,8 +691,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3 h1:hNQpMuAJe5CtcUqCXaWga3FHu+kQvCqcsoVaQgSV60o=
golang.org/x/exp v0.0.0-20240112132812-db7319d0e0e3/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -838,8 +749,6 @@ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81R
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
@ -856,27 +765,20 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190412183630-56d357773e84/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -903,8 +805,6 @@ golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@ -924,8 +824,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
@ -940,13 +838,9 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3
golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190329151228-23e29df326fe/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190416151739-9c9e1878f421/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190420181800-aa740d480789/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190531172133-b3315ee88b7d/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
@ -1054,8 +948,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8=
google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM=
google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -1071,7 +965,6 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
@ -1091,9 +984,7 @@ gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View file

@ -161,8 +161,8 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() {
"type": "OrderedCollectionPage",
"id": targetStatus.URI + "/replies?limit=20&only_other_accounts=false",
"partOf": targetStatus.URI + "/replies?only_other_accounts=false",
"next": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&min_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
"prev": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&max_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
"next": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&max_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
"prev": "http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?limit=20&min_id=01FF25D5Q0DH7CHD57CTRS6WK0&only_other_accounts=false",
"orderedItems": []string{"http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0"},
"totalItems": 1,
})

View file

@ -21,8 +21,7 @@ import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"io"
"net/http"
"net/http/httptest"
"net/url"
@ -42,9 +41,6 @@ import (
"github.com/tomnomnom/linkheader"
)
// random reader according to current-time source seed.
var randRd = rand.New(rand.NewSource(time.Now().Unix()))
type FollowTestSuite struct {
AccountStandardTestSuite
}
@ -76,33 +72,33 @@ func (suite *FollowTestSuite) TestFollowSelf() {
defer result.Body.Close()
// check the response
b, err := ioutil.ReadAll(result.Body)
b, err := io.ReadAll(result.Body)
_ = b
assert.NoError(suite.T(), err)
}
func (suite *FollowTestSuite) TestGetFollowersPageBackwardLimit2() {
suite.testGetFollowersPage(2, "backward")
func (suite *FollowTestSuite) TestGetFollowersPageNewestToOldestLimit2() {
suite.testGetFollowersPage(2, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowersPageBackwardLimit4() {
suite.testGetFollowersPage(4, "backward")
func (suite *FollowTestSuite) TestGetFollowersPageNewestToOldestLimit4() {
suite.testGetFollowersPage(4, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowersPageBackwardLimit6() {
suite.testGetFollowersPage(6, "backward")
func (suite *FollowTestSuite) TestGetFollowersPageNewestToOldestLimit6() {
suite.testGetFollowersPage(6, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowersPageForwardLimit2() {
suite.testGetFollowersPage(2, "forward")
func (suite *FollowTestSuite) TestGetFollowersPageOldestToNewestLimit2() {
suite.testGetFollowersPage(2, "oldestToNewest")
}
func (suite *FollowTestSuite) TestGetFollowersPageForwardLimit4() {
suite.testGetFollowersPage(4, "forward")
func (suite *FollowTestSuite) TestGetFollowersPageOldestToNewestLimit4() {
suite.testGetFollowersPage(4, "oldestToNewest")
}
func (suite *FollowTestSuite) TestGetFollowersPageForwardLimit6() {
suite.testGetFollowersPage(6, "forward")
func (suite *FollowTestSuite) TestGetFollowersPageOldestToNewestLimit6() {
suite.testGetFollowersPage(6, "oldestToNewest")
}
func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string) {
@ -117,8 +113,11 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
var i int
for _, targetAccount := range suite.testAccounts {
if targetAccount.ID == requestingAccount.ID {
// Have each account in the testrig follow the account
// that is requesting their followers from the API.
for _, account := range suite.testAccounts {
targetAccount := requestingAccount
if account.ID == targetAccount.ID {
// we cannot be our own target...
continue
}
@ -132,9 +131,9 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
ID: id,
CreatedAt: now,
UpdatedAt: now,
URI: fmt.Sprintf("%s/follow/%s", targetAccount.URI, id),
AccountID: targetAccount.ID,
TargetAccountID: requestingAccount.ID,
URI: fmt.Sprintf("%s/follow/%s", account.URI, id),
AccountID: account.ID,
TargetAccountID: targetAccount.ID,
})
suite.NoError(err)
@ -152,15 +151,17 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
var query string
switch direction {
case "backward":
// Set the starting query to page backward from newest.
case "newestToOldest":
// Set the starting query to page from
// newest (ie., first entry in slice).
acc := expectAccounts[0].(*model.Account)
newest, _ := suite.db.GetFollow(ctx, acc.ID, requestingAccount.ID)
expectAccounts = expectAccounts[1:]
query = fmt.Sprintf("limit=%d&max_id=%s", limit, newest.ID)
case "forward":
// Set the starting query to page forward from the oldest.
case "oldestToNewest":
// Set the starting query to page from
// oldest (ie., last entry in slice).
acc := expectAccounts[len(expectAccounts)-1].(*model.Account)
oldest, _ := suite.db.GetFollow(ctx, acc.ID, requestingAccount.ID)
expectAccounts = expectAccounts[:len(expectAccounts)-1]
@ -208,9 +209,9 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
)
switch direction {
case "backward":
// When paging backwards (DESC) we:
// - iter from end of received accounts
case "newestToOldest":
// When paging newest to oldest (ie., first page to last page):
// - iter from start of received accounts
// - iterate backward through received accounts
// - stop when we reach last index of received accounts
// - compare each received with the first index of expected accounts
@ -221,8 +222,8 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
expect = func(i []interface{}) interface{} { return i[0] }
trunc = func(i []interface{}) []interface{} { return i[1:] }
case "forward":
// When paging forwards (ASC) we:
case "oldestToNewest":
// When paging oldest to newest (ie., last page to first page):
// - iter from end of received accounts
// - iterate backward through received accounts
// - stop when we reach first index of received accounts
@ -230,7 +231,7 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
// - after each compare, drop the last index of expected accounts
start = func(i []*model.Account) int { return len(i) - 1 }
iter = func(i int) int { return i - 1 }
check = func(idx int, i []*model.Account) bool { return idx >= 0 }
check = func(idx int, _ []*model.Account) bool { return idx >= 0 }
expect = func(i []interface{}) interface{} { return i[len(i)-1] }
trunc = func(i []interface{}) []interface{} { return i[:len(i)-1] }
}
@ -256,7 +257,14 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
// Parse response link header values.
values := result.Header.Values("Link")
links := linkheader.ParseMultiple(values)
filteredLinks := links.FilterByRel("next")
var filteredLinks linkheader.Links
if direction == "newestToOldest" {
filteredLinks = links.FilterByRel("next")
} else {
filteredLinks = links.FilterByRel("prev")
}
suite.NotEmpty(filteredLinks, "no next link provided with more remaining accounts on page=%d", p)
// A ref link header was set.
@ -271,28 +279,28 @@ func (suite *FollowTestSuite) testGetFollowersPage(limit int, direction string)
}
}
func (suite *FollowTestSuite) TestGetFollowingPageBackwardLimit2() {
suite.testGetFollowingPage(2, "backward")
func (suite *FollowTestSuite) TestGetFollowingPageNewestToOldestLimit2() {
suite.testGetFollowingPage(2, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowingPageBackwardLimit4() {
suite.testGetFollowingPage(4, "backward")
func (suite *FollowTestSuite) TestGetFollowingPageNewestToOldestLimit4() {
suite.testGetFollowingPage(4, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowingPageBackwardLimit6() {
suite.testGetFollowingPage(6, "backward")
func (suite *FollowTestSuite) TestGetFollowingPageNewestToOldestLimit6() {
suite.testGetFollowingPage(6, "newestToOldest")
}
func (suite *FollowTestSuite) TestGetFollowingPageForwardLimit2() {
suite.testGetFollowingPage(2, "forward")
func (suite *FollowTestSuite) TestGetFollowingPageOldestToNewestLimit2() {
suite.testGetFollowingPage(2, "oldestToNewest")
}
func (suite *FollowTestSuite) TestGetFollowingPageForwardLimit4() {
suite.testGetFollowingPage(4, "forward")
func (suite *FollowTestSuite) TestGetFollowingPageOldestToNewestLimit4() {
suite.testGetFollowingPage(4, "oldestToNewest")
}
func (suite *FollowTestSuite) TestGetFollowingPageForwardLimit6() {
suite.testGetFollowingPage(6, "forward")
func (suite *FollowTestSuite) TestGetFollowingPageOldestToNewestLimit6() {
suite.testGetFollowingPage(6, "oldestToNewest")
}
func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string) {
@ -307,8 +315,11 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
var i int
// Have the account that is requesting their following
// list from the API follow each account in the testrig.
for _, targetAccount := range suite.testAccounts {
if targetAccount.ID == requestingAccount.ID {
account := requestingAccount
if targetAccount.ID == account.ID {
// we cannot be our own target...
continue
}
@ -322,8 +333,8 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
ID: id,
CreatedAt: now,
UpdatedAt: now,
URI: fmt.Sprintf("%s/follow/%s", requestingAccount.URI, id),
AccountID: requestingAccount.ID,
URI: fmt.Sprintf("%s/follow/%s", account.URI, id),
AccountID: account.ID,
TargetAccountID: targetAccount.ID,
})
suite.NoError(err)
@ -342,15 +353,17 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
var query string
switch direction {
case "backward":
// Set the starting query to page backward from newest.
case "newestToOldest":
// Set the starting query to page from
// newest (ie., first entry in slice).
acc := expectAccounts[0].(*model.Account)
newest, _ := suite.db.GetFollow(ctx, requestingAccount.ID, acc.ID)
expectAccounts = expectAccounts[1:]
query = fmt.Sprintf("limit=%d&max_id=%s", limit, newest.ID)
case "forward":
// Set the starting query to page forward from the oldest.
case "oldestToNewest":
// Set the starting query to page from
// oldest (ie., last entry in slice).
acc := expectAccounts[len(expectAccounts)-1].(*model.Account)
oldest, _ := suite.db.GetFollow(ctx, requestingAccount.ID, acc.ID)
expectAccounts = expectAccounts[:len(expectAccounts)-1]
@ -397,9 +410,9 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
)
switch direction {
case "backward":
// When paging backwards (DESC) we:
// - iter from end of received accounts
case "newestToOldest":
// When paging newest to oldest (ie., first page to last page):
// - iter from start of received accounts
// - iterate backward through received accounts
// - stop when we reach last index of received accounts
// - compare each received with the first index of expected accounts
@ -410,8 +423,8 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
expect = func(i []interface{}) interface{} { return i[0] }
trunc = func(i []interface{}) []interface{} { return i[1:] }
case "forward":
// When paging forwards (ASC) we:
case "oldestToNewest":
// When paging oldest to newest (ie., last page to first page):
// - iter from end of received accounts
// - iterate backward through received accounts
// - stop when we reach first index of received accounts
@ -419,7 +432,7 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
// - after each compare, drop the last index of expected accounts
start = func(i []*model.Account) int { return len(i) - 1 }
iter = func(i int) int { return i - 1 }
check = func(idx int, i []*model.Account) bool { return idx >= 0 }
check = func(idx int, _ []*model.Account) bool { return idx >= 0 }
expect = func(i []interface{}) interface{} { return i[len(i)-1] }
trunc = func(i []interface{}) []interface{} { return i[:len(i)-1] }
}
@ -445,7 +458,14 @@ func (suite *FollowTestSuite) testGetFollowingPage(limit int, direction string)
// Parse response link header values.
values := result.Header.Values("Link")
links := linkheader.ParseMultiple(values)
filteredLinks := links.FilterByRel("next")
var filteredLinks linkheader.Links
if direction == "newestToOldest" {
filteredLinks = links.FilterByRel("next")
} else {
filteredLinks = links.FilterByRel("prev")
}
suite.NotEmpty(filteredLinks, "no next link provided with more remaining accounts on page=%d", p)
// A ref link header was set.

View file

@ -19,11 +19,13 @@
//
// View + page through known accounts according to given filters.
//
// Returned accounts will be ordered alphabetically (a-z) by domain + username.
//
// The next and previous queries can be parsed from the returned Link header.
// Example:
//
// ```
// <https://example.org/api/v1/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
// <https://example.org/api/v1/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v1/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
// ````
//
// ---
@ -117,23 +119,30 @@
// name: max_id
// in: query
// type: string
// description: All results returned will be older than the item with this ID.
// -
// name: since_id
// in: query
// type: string
// description: All results returned will be newer than the item with this ID.
// description: >-
// max_id in the form `[domain]/@[username]`.
// All results returned will be later in the alphabet than `[domain]/@[username]`.
// For example, if max_id = `example.org/@someone` then returned entries might
// contain `example.org/@someone_else`, `later.example.org/@someone`, etc.
// Local account IDs in this form use an empty string for the `[domain]` part,
// for example local account with username `someone` would be `/@someone`.
// -
// name: min_id
// in: query
// type: string
// description: Returns results immediately newer than the item with this ID.
// description: >-
// min_id in the form `[domain]/@[username]`.
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
// For example, if min_id = `example.org/@someone` then returned entries might
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
// Local account IDs in this form use an empty string for the `[domain]` part,
// for example local account with username `someone` would be `/@someone`.
// -
// name: limit
// in: query
// type: integer
// description: Maximum number of results to return.
// default: 100
// default: 50
// maximum: 200
// minimum: 1
//
@ -200,7 +209,7 @@ func (m *Module) AccountsGETV1Handler(c *gin.Context) {
return
}
page, errWithCode := paging.ParseIDPage(c, 1, 200, 100)
page, errWithCode := paging.ParseIDPage(c, 1, 200, 50)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return

View file

@ -19,11 +19,13 @@
//
// View + page through known accounts according to given filters.
//
// Returned accounts will be ordered alphabetically (a-z) by domain + username.
//
// The next and previous queries can be parsed from the returned Link header.
// Example:
//
// ```
// <https://example.org/api/v2/admin/accounts?limit=80&max_id=01FC0SKA48HNSVR6YKZCQGS2V8>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=01FC0SKW5JK2Q4EVAV2B462YY0>; rel="prev"
// <https://example.org/api/v2/admin/accounts?limit=80&max_id=example.org%2F%40someone>; rel="next", <https://example.org/api/v2/admin/accounts?limit=80&min_id=example.org%2F%40someone_else>; rel="prev"
// ````
//
// ---
@ -90,23 +92,30 @@
// name: max_id
// in: query
// type: string
// description: All results returned will be older than the item with this ID.
// -
// name: since_id
// in: query
// type: string
// description: All results returned will be newer than the item with this ID.
// description: >-
// max_id in the form `[domain]/@[username]`.
// All results returned will be later in the alphabet than `[domain]/@[username]`.
// For example, if max_id = `example.org/@someone` then returned entries might
// contain `example.org/@someone_else`, `later.example.org/@someone`, etc.
// Local account IDs in this form use an empty string for the `[domain]` part,
// for example local account with username `someone` would be `/@someone`.
// -
// name: min_id
// in: query
// type: string
// description: Returns results immediately newer than the item with this ID.
// description: >-
// min_id in the form `[domain]/@[username]`.
// All results returned will be earlier in the alphabet than `[domain]/@[username]`.
// For example, if min_id = `example.org/@someone` then returned entries might
// contain `example.org/@earlier_account`, `earlier.example.org/@someone`, etc.
// Local account IDs in this form use an empty string for the `[domain]` part,
// for example local account with username `someone` would be `/@someone`.
// -
// name: limit
// in: query
// type: integer
// description: Maximum number of results to return.
// default: 100
// default: 50
// maximum: 200
// minimum: 1
//
@ -173,7 +182,7 @@ func (m *Module) AccountsGETV2Handler(c *gin.Context) {
return
}
page, errWithCode := paging.ParseIDPage(c, 1, 200, 100)
page, errWithCode := paging.ParseIDPage(c, 1, 200, 50)
if errWithCode != nil {
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
return

View file

@ -0,0 +1,546 @@
// 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 admin_test
import (
"bytes"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
)
type AccountsGetTestSuite struct {
AdminStandardTestSuite
}
func (suite *AccountsGetTestSuite) TestAccountsGetFromTop() {
recorder := httptest.NewRecorder()
path := admin.AccountsV2Path
ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json")
suite.adminModule.AccountsGETV2Handler(ctx)
suite.Equal(http.StatusOK, recorder.Code)
b, err := io.ReadAll(recorder.Body)
if err != nil {
suite.FailNow(err.Error())
}
suite.NotNil(b)
dst := new(bytes.Buffer)
err = json.Indent(dst, b, "", " ")
if err != nil {
suite.FailNow(err.Error())
}
link := recorder.Header().Get("Link")
suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=50&max_id=xn--xample-ova.org%2F%40%C3%BCser>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=50&min_id=%2F%401happyturtle>; rel="prev"`, link)
suite.Equal(`[
{
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
"username": "1happyturtle",
"domain": null,
"created_at": "2022-06-04T13:12:00.000Z",
"email": "tortle.dude@example.org",
"ip": null,
"ips": [],
"locale": "en",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": true,
"approved": true,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01F8MH5NBDF2MV7CTC4Q5128HF",
"username": "1happyturtle",
"acct": "1happyturtle",
"display_name": "happy little turtle :3",
"locked": true,
"discoverable": false,
"bot": false,
"created_at": "2022-06-04T13:12:00.000Z",
"note": "<p>i post about things that concern me</p>",
"url": "http://localhost:8080/@1happyturtle",
"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": 8,
"last_status_at": "2021-07-28T08:40:37.000Z",
"emojis": [],
"fields": [
{
"name": "should you follow me?",
"value": "maybe!",
"verified_at": null
},
{
"name": "age",
"value": "120",
"verified_at": null
}
],
"hide_collections": true,
"role": {
"name": "user"
}
},
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
},
{
"id": "01F8MH17FWEB39HZJ76B6VXSKF",
"username": "admin",
"domain": null,
"created_at": "2022-05-17T13:10:59.000Z",
"email": "admin@example.org",
"ip": null,
"ips": [],
"locale": "en",
"invite_request": null,
"role": {
"name": "admin"
},
"confirmed": true,
"approved": true,
"disabled": false,
"silenced": false,
"suspended": false,
"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"
}
},
"created_by_application_id": "01F8MGXQRHYF5QPMTMXP78QC2F"
},
{
"id": "01AY6P665V14JJR0AFVRT7311Y",
"username": "localhost:8080",
"domain": null,
"created_at": "2020-05-17T13:10:59.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01AY6P665V14JJR0AFVRT7311Y",
"username": "localhost:8080",
"acct": "localhost:8080",
"display_name": "",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2020-05-17T13:10:59.000Z",
"note": "",
"url": "http://localhost:8080/@localhost:8080",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": []
}
},
{
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"domain": null,
"created_at": "2022-05-20T11:09:18.000Z",
"email": "zork@example.org",
"ip": null,
"ips": [],
"locale": "en",
"invite_request": "I wanna be on this damned webbed site so bad! Please! Wow",
"role": {
"name": "user"
},
"confirmed": true,
"approved": true,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01F8MH1H7YV1Z7D2C8K2730QBF",
"username": "the_mighty_zork",
"acct": "the_mighty_zork",
"display_name": "original zork (he/they)",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2022-05-20T11:09:18.000Z",
"note": "<p>hey yo this is my profile!</p>",
"url": "http://localhost:8080/@the_mighty_zork",
"avatar": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/original/01F8MH58A357CV5K7R7TJMSH6S.jpg",
"avatar_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/avatar/small/01F8MH58A357CV5K7R7TJMSH6S.jpg",
"header": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/original/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
"header_static": "http://localhost:8080/fileserver/01F8MH1H7YV1Z7D2C8K2730QBF/header/small/01PFPMWK2FF0D9WMHEJHR07C3Q.jpg",
"followers_count": 2,
"following_count": 2,
"statuses_count": 7,
"last_status_at": "2023-12-10T09:24:00.000Z",
"emojis": [],
"fields": [],
"enable_rss": true,
"role": {
"name": "user"
}
},
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
},
{
"id": "01F8MH0BBE4FHXPH513MBVFHB0",
"username": "weed_lord420",
"domain": null,
"created_at": "2022-06-04T13:12:00.000Z",
"email": "weed_lord420@example.org",
"ip": "199.222.111.89",
"ips": [],
"locale": "en",
"invite_request": "hi, please let me in! I'm looking for somewhere neato bombeato to hang out.",
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01F8MH0BBE4FHXPH513MBVFHB0",
"username": "weed_lord420",
"acct": "weed_lord420",
"display_name": "",
"locked": false,
"discoverable": false,
"bot": false,
"created_at": "2022-06-04T13:12:00.000Z",
"note": "",
"url": "http://localhost:8080/@weed_lord420",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": [],
"role": {
"name": "user"
}
},
"created_by_application_id": "01F8MGY43H3N2C8EWPR2FPYEXG"
},
{
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
"username": "Some_User",
"domain": "example.org",
"created_at": "2020-08-10T12:13:28.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01FHMQX3GAABWSM0S2VZEC2SWC",
"username": "Some_User",
"acct": "Some_User@example.org",
"display_name": "some user",
"locked": true,
"discoverable": true,
"bot": false,
"created_at": "2020-08-10T12:13:28.000Z",
"note": "i'm a real son of a gun",
"url": "http://example.org/@Some_User",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 1,
"last_status_at": "2023-11-02T10:44:25.000Z",
"emojis": [],
"fields": []
}
},
{
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
"domain": "fossbros-anonymous.io",
"created_at": "2021-09-26T10:52:36.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01F8MH5ZK5VRH73AKHQM6Y9VNX",
"username": "foss_satan",
"acct": "foss_satan@fossbros-anonymous.io",
"display_name": "big gerald",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2021-09-26T10:52:36.000Z",
"note": "i post about like, i dunno, stuff, or whatever!!!!",
"url": "http://fossbros-anonymous.io/@foss_satan",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 3,
"last_status_at": "2021-09-11T09:40:37.000Z",
"emojis": [],
"fields": []
}
},
{
"id": "062G5WYKY35KKD12EMSM3F8PJ8",
"username": "her_fuckin_maj",
"domain": "thequeenisstillalive.technology",
"created_at": "2020-08-10T12:13:28.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "062G5WYKY35KKD12EMSM3F8PJ8",
"username": "her_fuckin_maj",
"acct": "her_fuckin_maj@thequeenisstillalive.technology",
"display_name": "lizzzieeeeeeeeeeee",
"locked": true,
"discoverable": true,
"bot": false,
"created_at": "2020-08-10T12:13:28.000Z",
"note": "if i die blame charles don't let that fuck become king",
"url": "http://thequeenisstillalive.technology/@her_fuckin_maj",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/original/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
"header_static": "http://localhost:8080/fileserver/062G5WYKY35KKD12EMSM3F8PJ8/header/small/01PFPMWK2FF0D9WMHEJHR07C3R.jpg",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": []
}
},
{
"id": "07GZRBAEMBNKGZ8Z9VSKSXKR98",
"username": "üser",
"domain": "ëxample.org",
"created_at": "2020-08-10T12:13:28.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "07GZRBAEMBNKGZ8Z9VSKSXKR98",
"username": "üser",
"acct": "üser@ëxample.org",
"display_name": "",
"locked": false,
"discoverable": false,
"bot": false,
"created_at": "2020-08-10T12:13:28.000Z",
"note": "",
"url": "https://xn--xample-ova.org/users/@%C3%BCser",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": []
}
}
]`, dst.String())
}
func (suite *AccountsGetTestSuite) TestAccountsMinID() {
recorder := httptest.NewRecorder()
path := admin.AccountsV2Path + "?limit=1&min_id=/@the_mighty_zork"
ctx := suite.newContext(recorder, http.MethodGet, nil, path, "application/json")
ctx.Params = gin.Params{
{
Key: "min_id",
Value: "/@the_mighty_zork",
},
{
Key: "limit",
Value: "1",
},
}
suite.adminModule.AccountsGETV2Handler(ctx)
suite.Equal(http.StatusOK, recorder.Code)
b, err := io.ReadAll(recorder.Body)
if err != nil {
suite.FailNow(err.Error())
}
suite.NotNil(b)
dst := new(bytes.Buffer)
err = json.Indent(dst, b, "", " ")
if err != nil {
suite.FailNow(err.Error())
}
link := recorder.Header().Get("Link")
suite.Equal(`<http://localhost:8080/api/v2/admin/accounts?limit=1&max_id=%2F%40localhost%3A8080>; rel="next", <http://localhost:8080/api/v2/admin/accounts?limit=1&min_id=%2F%40localhost%3A8080>; rel="prev"`, link)
suite.Equal(`[
{
"id": "01AY6P665V14JJR0AFVRT7311Y",
"username": "localhost:8080",
"domain": null,
"created_at": "2020-05-17T13:10:59.000Z",
"email": "",
"ip": null,
"ips": [],
"locale": "",
"invite_request": null,
"role": {
"name": "user"
},
"confirmed": false,
"approved": false,
"disabled": false,
"silenced": false,
"suspended": false,
"account": {
"id": "01AY6P665V14JJR0AFVRT7311Y",
"username": "localhost:8080",
"acct": "localhost:8080",
"display_name": "",
"locked": false,
"discoverable": true,
"bot": false,
"created_at": "2020-05-17T13:10:59.000Z",
"note": "",
"url": "http://localhost:8080/@localhost:8080",
"avatar": "",
"avatar_static": "",
"header": "http://localhost:8080/assets/default_header.png",
"header_static": "http://localhost:8080/assets/default_header.png",
"followers_count": 0,
"following_count": 0,
"statuses_count": 0,
"last_status_at": null,
"emojis": [],
"fields": []
}
}
]`, dst.String())
}
func TestAccountsGetTestSuite(t *testing.T) {
suite.Run(t, &AccountsGetTestSuite{})
}

View file

@ -107,28 +107,28 @@ func (suite *GetTestSuite) TestGet() {
]`, dst.String())
}
func (suite *GetTestSuite) TestGetPageBackwardLimit2() {
suite.testGetPage(2, "backward")
func (suite *GetTestSuite) TestGetPageNewestToOldestLimit2() {
suite.testGetPage(2, "newestToOldest")
}
func (suite *GetTestSuite) TestGetPageBackwardLimit4() {
suite.testGetPage(4, "backward")
func (suite *GetTestSuite) TestGetPageNewestToOldestLimit4() {
suite.testGetPage(4, "newestToOldest")
}
func (suite *GetTestSuite) TestGetPageBackwardLimit6() {
suite.testGetPage(6, "backward")
func (suite *GetTestSuite) TestGetPageNewestToOldestLimit6() {
suite.testGetPage(6, "newestToOldest")
}
func (suite *GetTestSuite) TestGetPageForwardLimit2() {
suite.testGetPage(2, "forward")
func (suite *GetTestSuite) TestGetPageOldestToNewestLimit2() {
suite.testGetPage(2, "oldestToNewest")
}
func (suite *GetTestSuite) TestGetPageForwardLimit4() {
suite.testGetPage(4, "forward")
func (suite *GetTestSuite) TestGetPageOldestToNewestLimit4() {
suite.testGetPage(4, "oldestToNewest")
}
func (suite *GetTestSuite) TestGetPageForwardLimit6() {
suite.testGetPage(6, "forward")
func (suite *GetTestSuite) TestGetPageOldestToNewestLimit6() {
suite.testGetPage(6, "oldestToNewest")
}
func (suite *GetTestSuite) testGetPage(limit int, direction string) {
@ -143,8 +143,11 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
var i int
for _, targetAccount := range suite.testAccounts {
if targetAccount.ID == requestingAccount.ID {
// Have each account in the testrig follow req the
// account requesting their followers from the API.
for _, account := range suite.testAccounts {
targetAccount := requestingAccount
if account.ID == requestingAccount.ID {
// we cannot be our own target...
continue
}
@ -158,9 +161,9 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
ID: id,
CreatedAt: now,
UpdatedAt: now,
URI: fmt.Sprintf("%s/follow/%s", targetAccount.URI, id),
AccountID: targetAccount.ID,
TargetAccountID: requestingAccount.ID,
URI: fmt.Sprintf("%s/follow/%s", account.URI, id),
AccountID: account.ID,
TargetAccountID: targetAccount.ID,
})
suite.NoError(err)
@ -178,15 +181,17 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
var query string
switch direction {
case "backward":
// Set the starting query to page backward from newest.
case "newestToOldest":
// Set the starting query to page from
// newest (ie., first entry in slice).
acc := expectAccounts[0].(*model.Account)
newest, _ := suite.db.GetFollowRequest(ctx, acc.ID, requestingAccount.ID)
expectAccounts = expectAccounts[1:]
query = fmt.Sprintf("limit=%d&max_id=%s", limit, newest.ID)
case "forward":
// Set the starting query to page forward from the oldest.
case "oldestToNewest":
// Set the starting query to page from
// oldest (ie., last entry in slice).
acc := expectAccounts[len(expectAccounts)-1].(*model.Account)
oldest, _ := suite.db.GetFollowRequest(ctx, acc.ID, requestingAccount.ID)
expectAccounts = expectAccounts[:len(expectAccounts)-1]
@ -232,9 +237,9 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
)
switch direction {
case "backward":
// When paging backwards (DESC) we:
// - iter from end of received accounts
case "newestToOldest":
// When paging newest to oldest (ie., first page to last page):
// - iter from start of received accounts
// - iterate backward through received accounts
// - stop when we reach last index of received accounts
// - compare each received with the first index of expected accounts
@ -245,8 +250,8 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
expect = func(i []interface{}) interface{} { return i[0] }
trunc = func(i []interface{}) []interface{} { return i[1:] }
case "forward":
// When paging forwards (ASC) we:
case "oldestToNewest":
// When paging oldest to newest (ie., last page to first page):
// - iter from end of received accounts
// - iterate backward through received accounts
// - stop when we reach first index of received accounts
@ -254,7 +259,7 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
// - after each compare, drop the last index of expected accounts
start = func(i []*model.Account) int { return len(i) - 1 }
iter = func(i int) int { return i - 1 }
check = func(idx int, i []*model.Account) bool { return idx >= 0 }
check = func(idx int, _ []*model.Account) bool { return idx >= 0 }
expect = func(i []interface{}) interface{} { return i[len(i)-1] }
trunc = func(i []interface{}) []interface{} { return i[:len(i)-1] }
}
@ -280,7 +285,14 @@ func (suite *GetTestSuite) testGetPage(limit int, direction string) {
// Parse response link header values.
values := result.Header.Values("Link")
links := linkheader.ParseMultiple(values)
filteredLinks := links.FilterByRel("next")
var filteredLinks linkheader.Links
if direction == "newestToOldest" {
filteredLinks = links.FilterByRel("next")
} else {
filteredLinks = links.FilterByRel("prev")
}
suite.NotEmpty(filteredLinks, "no next link provided with more remaining accounts on page=%d", p)
// A ref link header was set.

View file

@ -72,7 +72,6 @@ func (suite *StatusStandardTestSuite) SetupSuite() {
func (suite *StatusStandardTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.StartNoopWorkers(&suite.state)
testrig.InitTestConfig()
testrig.InitTestLog()
@ -98,6 +97,8 @@ func (suite *StatusStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
suite.processor = testrig.NewTestProcessor(&suite.state, suite.federator, suite.emailSender, suite.mediaManager)
suite.statusModule = statuses.New(suite.processor)
testrig.StartWorkers(&suite.state, suite.processor.Workers())
}
func (suite *StatusStandardTestSuite) TearDownTest() {

View file

@ -19,7 +19,7 @@ import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"io"
"net/http"
"net/http/httptest"
"strings"
@ -69,7 +69,7 @@ func (suite *StatusDeleteTestSuite) TestPostDelete() {
result := recorder.Result()
defer result.Body.Close()
b, err := ioutil.ReadAll(result.Body)
b, err := io.ReadAll(result.Body)
suite.NoError(err)
statusReply := &apimodel.Status{}

View file

@ -50,10 +50,10 @@ func (suite *StatusHistoryTestSuite) TestGetHistory() {
// Setup request.
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, target, nil)
request := httptest.NewRequest(http.MethodGet, target, nil)
request.Header.Set("accept", "application/json")
ctx, _ := testrig.CreateGinTestContext(recorder, request)
// Set auth + path params.
ctx.Set(oauth.SessionAuthorizedApplication, testApplication)
ctx.Set(oauth.SessionAuthorizedToken, testToken)

View file

@ -125,8 +125,11 @@ func (c *Caches) Sweep(threshold float64) {
c.GTS.AccountNote.Trim(threshold)
c.GTS.AccountSettings.Trim(threshold)
c.GTS.AccountStats.Trim(threshold)
c.GTS.Application.Trim(threshold)
c.GTS.Block.Trim(threshold)
c.GTS.BlockIDs.Trim(threshold)
c.GTS.BoostOfIDs.Trim(threshold)
c.GTS.Client.Trim(threshold)
c.GTS.Emoji.Trim(threshold)
c.GTS.EmojiCategory.Trim(threshold)
c.GTS.Filter.Trim(threshold)
@ -136,6 +139,7 @@ func (c *Caches) Sweep(threshold float64) {
c.GTS.FollowIDs.Trim(threshold)
c.GTS.FollowRequest.Trim(threshold)
c.GTS.FollowRequestIDs.Trim(threshold)
c.GTS.InReplyToIDs.Trim(threshold)
c.GTS.Instance.Trim(threshold)
c.GTS.List.Trim(threshold)
c.GTS.ListEntry.Trim(threshold)
@ -145,11 +149,15 @@ func (c *Caches) Sweep(threshold float64) {
c.GTS.Move.Trim(threshold)
c.GTS.Notification.Trim(threshold)
c.GTS.Poll.Trim(threshold)
c.GTS.PollVote.Trim(threshold)
c.GTS.PollVoteIDs.Trim(threshold)
c.GTS.Report.Trim(threshold)
c.GTS.Status.Trim(threshold)
c.GTS.StatusFave.Trim(threshold)
c.GTS.StatusFaveIDs.Trim(threshold)
c.GTS.Tag.Trim(threshold)
c.GTS.ThreadMute.Trim(threshold)
c.GTS.Token.Trim(threshold)
c.GTS.Tombstone.Trim(threshold)
c.GTS.User.Trim(threshold)
c.Visibility.Trim(threshold)

View file

@ -252,6 +252,32 @@ func (a *accountDB) GetInstanceAccount(ctx context.Context, domain string) (*gts
return a.GetAccountByUsernameDomain(ctx, username, domain)
}
// GetAccounts selects accounts using the given parameters.
// Unlike with other functions, the paging for GetAccounts
// is done not by ID, but by a concatenation of `[domain]/@[username]`,
// which allows callers to page through accounts in alphabetical
// order (much more useful for an admin overview of accounts,
// for example, than paging by ID (which is random) or by account
// created at date, which is not particularly interesting).
//
// Generated queries will look something like this
// (SQLite example, maxID was provided so we're paging down):
//
// SELECT "account"."id", (COALESCE("domain", '') || '/@' || "username") AS "domain_username"
// FROM "accounts" AS "account"
// WHERE ("domain_username" > '/@the_mighty_zork')
// ORDER BY "domain_username" ASC
//
// **NOTE ABOUT POSTGRES**: Postgres ordering expressions in
// this function specify COLLATE "C" to ensure that ordering
// is similar to SQLite (which uses BINARY ordering by default).
// This unfortunately means that A-Z > a-z, when ordering but
// that's an acceptable tradeoff for a query like this.
//
// See:
//
// - https://www.postgresql.org/docs/current/collation.html#COLLATION-MANAGING-STANDARD
// - https://sqlite.org/datatype3.html#collation
func (a *accountDB) GetAccounts(
ctx context.Context,
origin string,
@ -269,6 +295,11 @@ func (a *accountDB) GetAccounts(
error,
) {
var (
// We have to use different
// syntax for this query
// depending on dialect.
dbDialect = a.db.Dialect().Name()
// local users lists,
// required for some
// limiting parameters.
@ -287,10 +318,6 @@ func (a *accountDB) GetAccounts(
}
// Get paging params.
//
// Note this may be min_id OR since_id
// from the API, this gets handled below
// when checking order to reverse slice.
minID = page.GetMin()
maxID = page.GetMax()
limit = page.GetLimit()
@ -309,32 +336,50 @@ func (a *accountDB) GetAccounts(
// Select only IDs from table
Column("account.id")
// Return only accounts OLDER
// than account with maxID.
if maxID != "" {
maxIDAcct, err := a.GetAccountByID(
gtscontext.SetBarebones(ctx),
maxID,
var subQ *bun.RawQuery
if dbDialect == dialect.SQLite {
// For SQLite we can just select
// our indexed expression once
// as a column alias.
q = q.ColumnExpr(
"(COALESCE(?, ?) || ? || ?) AS ?",
bun.Ident("domain"), "",
"/@",
bun.Ident("username"),
bun.Ident("domain_username"),
)
} else {
// Create a subquery for
// Postgres to reuse.
subQ = a.db.NewRaw(
"(COALESCE(?, ?) || ? || ?) COLLATE ?",
bun.Ident("domain"), "",
"/@",
bun.Ident("username"),
bun.Ident("C"),
)
if err != nil {
return nil, fmt.Errorf("error getting maxID account %s: %w", maxID, err)
}
q = q.Where("? < ?", bun.Ident("account.created_at"), maxIDAcct.CreatedAt)
}
// Return only accounts NEWER
// than account with minID.
if minID != "" {
minIDAcct, err := a.GetAccountByID(
gtscontext.SetBarebones(ctx),
minID,
)
if err != nil {
return nil, fmt.Errorf("error getting minID account %s: %w", minID, err)
// Return only accounts with `[domain]/@[username]`
// later in the alphabet (a-z) than provided maxID.
if maxID != "" {
if dbDialect == dialect.SQLite {
// Use aliased column.
q = q.Where("? > ?", bun.Ident("domain_username"), maxID)
} else {
q = q.Where("? > ?", subQ, maxID)
}
}
q = q.Where("? > ?", bun.Ident("account.created_at"), minIDAcct.CreatedAt)
// Return only accounts with `[domain]/@[username]`
// earlier in the alphabet (a-z) than provided minID.
if minID != "" {
if dbDialect == dialect.SQLite {
// Use aliased column.
q = q.Where("? < ?", bun.Ident("domain_username"), minID)
} else {
q = q.Where("? < ?", subQ, minID)
}
}
switch status {
@ -479,13 +524,29 @@ func (a *accountDB) GetAccounts(
if order == paging.OrderAscending {
// Page up.
q = q.Order("account.created_at ASC")
// It's counterintuitive because it
// says DESC in the query, but we're
// going backwards in the alphabet,
// and a < z in a string comparison.
if dbDialect == dialect.SQLite {
q = q.OrderExpr("? DESC", bun.Ident("domain_username"))
} else {
q = q.OrderExpr("(?) DESC", subQ)
}
} else {
// Page down.
q = q.Order("account.created_at DESC")
// It's counterintuitive because it
// says ASC in the query, but we're
// going forwards in the alphabet,
// and z > a in a string comparison.
if dbDialect == dialect.SQLite {
q = q.OrderExpr("? ASC", bun.Ident("domain_username"))
} else {
q = q.OrderExpr("? ASC", subQ)
}
}
if err := q.Scan(ctx, &accountIDs); err != nil {
if err := q.Scan(ctx, &accountIDs, new([]string)); err != nil {
return nil, err
}

View file

@ -502,6 +502,80 @@ func (suite *AccountTestSuite) TestGetAccountsAll() {
suite.Len(accounts, 9)
}
func (suite *AccountTestSuite) TestGetAccountsMaxID() {
var (
ctx = context.Background()
origin = ""
status = ""
mods = false
invitedBy = ""
username = ""
displayName = ""
domain = ""
email = ""
ip netip.Addr
// Get accounts with `[domain]/@[username]`
// later in the alphabet than `/@the_mighty_zork`.
page = &paging.Page{Max: paging.MaxID("/@the_mighty_zork")}
)
accounts, err := suite.db.GetAccounts(
ctx,
origin,
status,
mods,
invitedBy,
username,
displayName,
domain,
email,
ip,
page,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(accounts, 5)
}
func (suite *AccountTestSuite) TestGetAccountsMinID() {
var (
ctx = context.Background()
origin = ""
status = ""
mods = false
invitedBy = ""
username = ""
displayName = ""
domain = ""
email = ""
ip netip.Addr
// Get accounts with `[domain]/@[username]`
// earlier in the alphabet than `/@the_mighty_zork`.
page = &paging.Page{Min: paging.MinID("/@the_mighty_zork")}
)
accounts, err := suite.db.GetAccounts(
ctx,
origin,
status,
mods,
invitedBy,
username,
displayName,
domain,
email,
ip,
page,
)
if err != nil {
suite.FailNow(err.Error())
}
suite.Len(accounts, 3)
}
func (suite *AccountTestSuite) TestGetAccountsModsOnly() {
var (
ctx = context.Background()

View file

@ -0,0 +1,84 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package migrations
import (
"context"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/uptrace/bun"
"github.com/uptrace/bun/dialect"
)
func init() {
up := func(ctx context.Context, db *bun.DB) error {
log.Info(ctx, "reindexing accounts (accounts_paging_idx); this may take a few minutes, please don't interrupt this migration!")
q := db.NewCreateIndex().
TableExpr("accounts").
Index("accounts_paging_idx").
IfNotExists()
switch d := db.Dialect().Name(); d {
case dialect.SQLite:
q = q.ColumnExpr(
"COALESCE(?, ?) || ? || ?",
bun.Ident("domain"), "",
"/@",
bun.Ident("username"),
)
// Specify C collation for Postgres to ensure
// alphabetic sort order is similar enough to
// SQLite (which uses BINARY sort by default).
//
// See:
//
// - https://www.postgresql.org/docs/current/collation.html#COLLATION-MANAGING-STANDARD
// - https://sqlite.org/datatype3.html#collation
case dialect.PG:
q = q.ColumnExpr(
"(COALESCE(?, ?) || ? || ?) COLLATE ?",
bun.Ident("domain"), "",
"/@",
bun.Ident("username"),
bun.Ident("C"),
)
default:
log.Panicf(ctx, "dialect %s was neither postgres nor sqlite", d)
}
if _, err := q.Exec(ctx); err != nil {
return err
}
return nil
}
down := func(ctx context.Context, db *bun.DB) error {
return db.RunInTx(ctx, nil, func(ctx context.Context, tx bun.Tx) error {
return nil
})
}
if err := Migrations.Register(up, down); err != nil {
panic(err)
}
}

View file

@ -104,7 +104,7 @@ func (d *Dereferencer) GetAccountByURI(ctx context.Context, requestUser string,
if accountable != nil {
// This account was updated, enqueue re-dereference featured posts.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
if err := d.dereferenceAccountFeatured(ctx, requestUser, account); err != nil {
log.Errorf(ctx, "error fetching account featured collection: %v", err)
}
@ -201,7 +201,7 @@ func (d *Dereferencer) GetAccountByUsernameDomain(ctx context.Context, requestUs
if accountable != nil {
// This account was updated, enqueue re-dereference featured posts.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
if err := d.dereferenceAccountFeatured(ctx, requestUser, account); err != nil {
log.Errorf(ctx, "error fetching account featured collection: %v", err)
}
@ -322,7 +322,7 @@ func (d *Dereferencer) RefreshAccount(
if accountable != nil {
// This account was updated, enqueue re-dereference featured posts.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
if err := d.dereferenceAccountFeatured(ctx, requestUser, latest); err != nil {
log.Errorf(ctx, "error fetching account featured collection: %v", err)
}
@ -362,7 +362,7 @@ func (d *Dereferencer) RefreshAccountAsync(
}
// Enqueue a worker function to enrich this account async.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
latest, accountable, err := d.enrichAccountSafely(ctx, requestUser, uri, account, accountable)
if err != nil {
log.Errorf(ctx, "error enriching remote account: %v", err)

View file

@ -255,7 +255,7 @@ func (d *Dereferencer) RefreshStatusAsync(
}
// Enqueue a worker function to re-fetch this status entirely async.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
latest, statusable, _, err := d.enrichStatusSafely(ctx,
requestUser,
uri,

View file

@ -56,14 +56,14 @@ func (d *Dereferencer) dereferenceThread(
}
// Enqueue dereferencing remaining status thread, (children), asychronously .
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
if err := d.DereferenceStatusDescendants(ctx, requestUser, uri, statusable); err != nil {
log.Error(ctx, err)
}
})
} else {
// This is an existing status, dereference the WHOLE thread asynchronously.
d.state.Workers.Federator.MustEnqueueCtx(ctx, func(ctx context.Context) {
d.state.Workers.Dereference.Queue.Push(func(ctx context.Context) {
if err := d.DereferenceStatusAncestors(ctx, requestUser, status); err != nil {
log.Error(ctx, err)
}

View file

@ -89,13 +89,12 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
return err
}
// Process side effects asynchronously.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
Receiving: receivingAcct,
Requesting: requestingAcct,
})
}
@ -138,13 +137,12 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA
return err
}
// Process side effects asynchronously.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
Receiving: receivingAcct,
Requesting: requestingAcct,
})
continue

View file

@ -81,12 +81,12 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre
}
// This is a new boost. Process side effects asynchronously.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: boost,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: boost,
Receiving: receivingAcct,
Requesting: requestingAcct,
})
return nil

View file

@ -19,6 +19,7 @@ package federatingdb_test
import (
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/streams/vocab"
@ -42,7 +43,7 @@ func (suite *AnnounceTestSuite) TestNewAnnounce() {
suite.NoError(err)
// should be a message heading to the processor now, which we can intercept here
msg := <-suite.fromFederator
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ActivityAnnounce, msg.APObjectType)
suite.Equal(ap.ActivityCreate, msg.APActivityType)
@ -69,7 +70,7 @@ func (suite *AnnounceTestSuite) TestAnnounceTwice() {
suite.NoError(err)
// should be a message heading to the processor now, which we can intercept here
msg := <-suite.fromFederator
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ActivityAnnounce, msg.APObjectType)
suite.Equal(ap.ActivityCreate, msg.APActivityType)
boost, ok := msg.GTSModel.(*gtsmodel.Status)
@ -94,7 +95,8 @@ func (suite *AnnounceTestSuite) TestAnnounceTwice() {
// since this is a repeat announce with the same URI, just delivered to a different inbox,
// we should have nothing in the messages channel...
suite.Empty(suite.fromFederator)
_, ok = suite.getFederatorMsg(time.Second)
suite.False(ok)
}
func TestAnnounceTestSuite(t *testing.T) {

View file

@ -99,7 +99,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
BLOCK HANDLERS
*/
func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, receiving *gtsmodel.Account, requestingAccount *gtsmodel.Account) error {
func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, receiving *gtsmodel.Account, requesting *gtsmodel.Account) error {
blockable, ok := asType.(vocab.ActivityStreamsBlock)
if !ok {
return errors.New("activityBlock: could not convert type to block")
@ -110,10 +110,10 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
return fmt.Errorf("activityBlock: could not convert Block to gts model block")
}
if block.AccountID != requestingAccount.ID {
if block.AccountID != requesting.ID {
return fmt.Errorf(
"activityBlock: requestingAccount %s is not Block actor account %s",
requestingAccount.URI, block.Account.URI,
requesting.URI, block.Account.URI,
)
}
@ -130,12 +130,12 @@ func (f *federatingDB) activityBlock(ctx context.Context, asType vocab.Type, rec
return fmt.Errorf("activityBlock: database error inserting block: %s", err)
}
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityCreate,
GTSModel: block,
ReceivingAccount: receiving,
RequestingAccount: requestingAccount,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityCreate,
GTSModel: block,
Receiving: receiving,
Requesting: requesting,
})
return nil
@ -297,7 +297,7 @@ func (f *federatingDB) createPollOptionables(
}
// Enqueue message to the fedi API worker with poll vote(s).
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APActivityType: ap.ActivityCreate,
APObjectType: ap.ActivityQuestion,
GTSModel: &gtsmodel.PollVote{
@ -308,8 +308,8 @@ func (f *federatingDB) createPollOptionables(
PollID: inReplyTo.PollID,
Poll: inReplyTo.Poll,
},
ReceivingAccount: receiver,
RequestingAccount: requester,
Receiving: receiver,
Requesting: requester,
})
return nil
@ -377,28 +377,28 @@ func (f *federatingDB) createStatusable(
// Pass the statusable URI (APIri) into the processor
// worker and do the rest of the processing asynchronously.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIri: ap.GetJSONLDId(statusable),
APObjectModel: nil,
GTSModel: nil,
ReceivingAccount: receiver,
RequestingAccount: requester,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIRI: ap.GetJSONLDId(statusable),
APObject: nil,
GTSModel: nil,
Receiving: receiver,
Requesting: requester,
})
return nil
}
// Do the rest of the processing asynchronously. The processor
// will handle inserting/updating + further dereferencing the status.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIri: nil,
GTSModel: nil,
APObjectModel: statusable,
ReceivingAccount: receiver,
RequestingAccount: requester,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APIRI: nil,
GTSModel: nil,
APObject: statusable,
Receiving: receiver,
Requesting: requester,
})
return nil
@ -439,12 +439,12 @@ func (f *federatingDB) activityFollow(ctx context.Context, asType vocab.Type, re
return fmt.Errorf("activityFollow: database error inserting follow request: %s", err)
}
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: followRequest,
ReceivingAccount: receivingAccount,
RequestingAccount: requestingAccount,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: followRequest,
Receiving: receivingAccount,
Requesting: requestingAccount,
})
return nil
@ -484,12 +484,12 @@ func (f *federatingDB) activityLike(ctx context.Context, asType vocab.Type, rece
return fmt.Errorf("activityLike: database error inserting fave: %w", err)
}
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
ReceivingAccount: receivingAccount,
RequestingAccount: requestingAccount,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
Receiving: receivingAccount,
Requesting: requestingAccount,
})
return nil
@ -536,12 +536,12 @@ func (f *federatingDB) activityFlag(ctx context.Context, asType vocab.Type, rece
return fmt.Errorf("activityFlag: database error inserting report: %w", err)
}
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFlag,
APActivityType: ap.ActivityCreate,
GTSModel: report,
ReceivingAccount: receivingAccount,
RequestingAccount: requestingAccount,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ActivityFlag,
APActivityType: ap.ActivityCreate,
GTSModel: report,
Receiving: receivingAccount,
Requesting: requestingAccount,
})
return nil

View file

@ -21,6 +21,7 @@ import (
"context"
"encoding/json"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/activity/streams"
@ -48,10 +49,10 @@ func (suite *CreateTestSuite) TestCreateNote() {
suite.NoError(err)
// should be a message heading to the processor now, which we can intercept here
msg := <-suite.fromFederator
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ObjectNote, msg.APObjectType)
suite.Equal(ap.ActivityCreate, msg.APActivityType)
suite.Equal(note, msg.APObjectModel)
suite.Equal(note, msg.APObject)
}
func (suite *CreateTestSuite) TestCreateNoteForward() {
@ -79,15 +80,15 @@ func (suite *CreateTestSuite) TestCreateNoteForward() {
suite.NoError(err)
// should be a message heading to the processor now, which we can intercept here
msg := <-suite.fromFederator
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ObjectNote, msg.APObjectType)
suite.Equal(ap.ActivityCreate, msg.APActivityType)
// nothing should be set as the model since this is a forward
suite.Nil(msg.APObjectModel)
suite.Nil(msg.APObject)
// but we should have a uri set
suite.Equal("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1", msg.APIri.String())
suite.Equal("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1", msg.APIRI.String())
}
func (suite *CreateTestSuite) TestCreateFlag1() {
@ -120,7 +121,7 @@ func (suite *CreateTestSuite) TestCreateFlag1() {
}
// should be a message heading to the processor now, which we can intercept here
msg := <-suite.fromFederator
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ActivityFlag, msg.APObjectType)
suite.Equal(ap.ActivityCreate, msg.APActivityType)

View file

@ -19,10 +19,13 @@ package federatingdb
import (
"context"
"errors"
"net/url"
"codeberg.org/gruf/go-kv"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
@ -34,43 +37,130 @@ import (
//
// The library makes this call only after acquiring a lock first.
func (f *federatingDB) Delete(ctx context.Context, id *url.URL) error {
l := log.WithContext(ctx).
WithFields(kv.Fields{
{"id", id},
}...)
l.Debug("entering Delete")
activityContext := getActivityContext(ctx)
if activityContext.internal {
return nil // Already processed.
}
requestingAcct := activityContext.requestingAcct
receivingAcct := activityContext.receivingAcct
// Extract receiving / requesting accounts.
requesting := activityContext.requestingAcct
receiving := activityContext.receivingAcct
// in a delete we only get the URI, we can't know if we have a status or a profile or something else,
// so we have to try a few different things...
if s, err := f.state.DB.GetStatusByURI(ctx, id.String()); err == nil && requestingAcct.ID == s.AccountID {
l.Debugf("deleting status: %s", s.ID)
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: s,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
})
// Serialize deleted ID URI.
// (may be status OR account)
uriStr := id.String()
var (
ok bool
err error
)
// Try delete as an account URI.
ok, err = f.deleteAccount(ctx,
requesting,
receiving,
uriStr,
)
if err != nil {
return err
} else if ok {
// success!
return nil
}
if a, err := f.state.DB.GetAccountByURI(ctx, id.String()); err == nil && requestingAcct.ID == a.ID {
l.Debugf("deleting account: %s", a.ID)
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: a,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
})
// Try delete as a status URI.
ok, err = f.deleteStatus(ctx,
requesting,
receiving,
uriStr,
)
if err != nil {
return err
} else if ok {
// success!
return nil
}
// Log at debug level, as lots of these could indicate federation
// issues between remote and this instance, or help with debugging.
log.Debugf(ctx, "received delete for unknown target: %s", uriStr)
return nil
}
func (f *federatingDB) deleteAccount(
ctx context.Context,
requesting *gtsmodel.Account,
receiving *gtsmodel.Account,
uri string, // target account
) (
bool, // success?
error, // any error
) {
account, err := f.state.DB.GetAccountByURI(ctx, uri)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return false, gtserror.Newf("error getting account: %w", err)
}
if account != nil {
// Ensure requesting account is
// only trying to delete itself.
if account.ID != requesting.ID {
// TODO: handled forwarded deletes,
// for now we silently drop this.
return true, nil
}
log.Debugf(ctx, "deleting account: %s", account.URI)
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: account,
Receiving: receiving,
Requesting: requesting,
})
return true, nil
}
return false, nil
}
func (f *federatingDB) deleteStatus(
ctx context.Context,
requesting *gtsmodel.Account,
receiving *gtsmodel.Account,
uri string, // target status
) (
bool, // success?
error, // any error
) {
status, err := f.state.DB.GetStatusByURI(ctx, uri)
if err != nil && !errors.Is(err, db.ErrNoEntries) {
return false, gtserror.Newf("error getting status: %w", err)
}
if status != nil {
// Ensure requesting account is only
// trying to delete its own statuses.
if status.AccountID != requesting.ID {
// TODO: handled forwarded deletes,
// for now we silently drop this.
return true, nil
}
log.Debugf(ctx, "deleting status: %s", status.URI)
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: status,
Receiving: receiving,
Requesting: requesting,
})
return true, nil
}
return false, nil
}

View file

@ -19,6 +19,7 @@ package federatingdb_test
import (
"context"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -34,11 +35,10 @@ import (
type FederatingDBTestSuite struct {
suite.Suite
db db.DB
tc *typeutils.Converter
fromFederator chan messages.FromFediAPI
federatingDB federatingdb.DB
state state.State
db db.DB
tc *typeutils.Converter
federatingDB federatingdb.DB
state state.State
testTokens map[string]*gtsmodel.Token
testClients map[string]*gtsmodel.Client
@ -51,6 +51,13 @@ type FederatingDBTestSuite struct {
testActivities map[string]testrig.ActivityWithSignature
}
func (suite *FederatingDBTestSuite) getFederatorMsg(timeout time.Duration) (*messages.FromFediAPI, bool) {
ctx := context.Background()
ctx, cncl := context.WithTimeout(ctx, timeout)
defer cncl()
return suite.state.Workers.Federator.Queue.PopCtx(ctx)
}
func (suite *FederatingDBTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
@ -69,13 +76,6 @@ func (suite *FederatingDBTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.StartNoopWorkers(&suite.state)
suite.fromFederator = make(chan messages.FromFediAPI, 10)
suite.state.Workers.EnqueueFediAPI = func(ctx context.Context, msgs ...messages.FromFediAPI) {
for _, msg := range msgs {
suite.fromFederator <- msg
}
}
suite.db = testrig.NewTestDB(&suite.state)
suite.testActivities = testrig.NewTestActivities(suite.testAccounts)
@ -96,13 +96,6 @@ func (suite *FederatingDBTestSuite) SetupTest() {
func (suite *FederatingDBTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StopWorkers(&suite.state)
for suite.fromFederator != nil {
select {
case <-suite.fromFederator:
default:
return
}
}
}
func createTestContext(receivingAccount *gtsmodel.Account, requestingAccount *gtsmodel.Account) context.Context {

View file

@ -170,12 +170,12 @@ func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove)
// We had a Move already or stored a new Move.
// Pass back to a worker for async processing.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityMove,
GTSModel: stubMove,
RequestingAccount: requestingAcct,
ReceivingAccount: receivingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityMove,
GTSModel: stubMove,
Requesting: requestingAcct,
Receiving: receivingAcct,
})
return nil

View file

@ -27,7 +27,6 @@ import (
"github.com/superseriousbusiness/activity/streams/vocab"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/messages"
)
type MoveTestSuite struct {
@ -78,13 +77,7 @@ func (suite *MoveTestSuite) TestMove() {
suite.move(receivingAcct, requestingAcct, moveStr1)
// Should be a message heading to the processor.
var msg messages.FromFediAPI
select {
case msg = <-suite.fromFederator:
// Fine.
case <-time.After(5 * time.Second):
suite.FailNow("", "timeout waiting for suite.fromFederator")
}
msg, _ := suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
@ -101,12 +94,7 @@ func (suite *MoveTestSuite) TestMove() {
// Should be a message heading to the processor
// since this is just a straight up retry.
select {
case msg = <-suite.fromFederator:
// Fine.
case <-time.After(5 * time.Second):
suite.FailNow("", "timeout waiting for suite.fromFederator")
}
msg, _ = suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
@ -126,12 +114,7 @@ func (suite *MoveTestSuite) TestMove() {
// Should be a message heading to the processor
// since this is just a retry with a different ID.
select {
case msg = <-suite.fromFederator:
// Fine.
case <-time.After(5 * time.Second):
suite.FailNow("", "timeout waiting for suite.fromFederator")
}
msg, _ = suite.getFederatorMsg(5 * time.Second)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
suite.Equal(ap.ActivityMove, msg.APActivityType)
}

View file

@ -81,7 +81,8 @@ func (suite *RejectTestSuite) TestRejectFollowRequest() {
suite.NoError(err)
// there should be nothing in the federator channel since nothing needs to be passed
suite.Empty(suite.fromFederator)
_, ok := suite.getFederatorMsg(time.Second)
suite.False(ok)
// the follow request should not be in the database anymore -- it's been rejected
err = suite.db.GetByID(ctx, fr.ID, &gtsmodel.FollowRequest{})

View file

@ -98,13 +98,13 @@ func (f *federatingDB) updateAccountable(ctx context.Context, receivingAcct *gts
// was delivered along with the Update, for further asynchronous
// updating of eg., avatar/header, emojis, etc. The actual db
// inserts/updates will take place there.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate,
GTSModel: requestingAcct,
APObjectModel: accountable,
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate,
GTSModel: requestingAcct,
APObject: accountable,
Receiving: receivingAcct,
Requesting: requestingAcct,
})
return nil
@ -155,13 +155,13 @@ func (f *federatingDB) updateStatusable(ctx context.Context, receivingAcct *gtsm
// Queue an UPDATE NOTE activity to our fedi API worker,
// this will handle necessary database insertions, etc.
f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityUpdate,
GTSModel: status, // original status
APObjectModel: (ap.Statusable)(statusable),
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityUpdate,
GTSModel: status, // original status
APObject: (ap.Statusable)(statusable),
Receiving: receivingAcct,
Requesting: requestingAcct,
})
return nil

View file

@ -59,9 +59,27 @@ var (
// configuration values passed to initialized http.Transport{}
// and http.Client{}, along with httpclient.Client{} specific.
type Config struct {
// MaxOpenConnsPerHost limits the max number of open connections to a host.
// MaxOpenConnsPerHost limits the max
// number of open connections to a host.
MaxOpenConnsPerHost int
// AllowRanges allows outgoing
// communications to given IP nets.
AllowRanges []netip.Prefix
// BlockRanges blocks outgoing
// communiciations to given IP nets.
BlockRanges []netip.Prefix
// TLSInsecureSkipVerify can be set to true to
// skip validation of remote TLS certificates.
//
// THIS SHOULD BE USED FOR TESTING ONLY, IF YOU
// TURN THIS ON WHILE RUNNING IN PRODUCTION YOU
// ARE LEAVING YOUR SERVER WIDE OPEN TO ATTACKS!
TLSInsecureSkipVerify bool
// MaxIdleConns: see http.Transport{}.MaxIdleConns.
MaxIdleConns int
@ -79,20 +97,6 @@ type Config struct {
// DisableCompression: see http.Transport{}.DisableCompression.
DisableCompression bool
// AllowRanges allows outgoing communications to given IP nets.
AllowRanges []netip.Prefix
// BlockRanges blocks outgoing communiciations to given IP nets.
BlockRanges []netip.Prefix
// TLSInsecureSkipVerify can be set to true to
// skip validation of remote TLS certificates.
//
// THIS SHOULD BE USED FOR TESTING ONLY, IF YOU
// TURN THIS ON WHILE RUNNING IN PRODUCTION YOU
// ARE LEAVING YOUR SERVER WIDE OPEN TO ATTACKS!
TLSInsecureSkipVerify bool
}
// Client wraps an underlying http.Client{} to provide the following:
@ -138,7 +142,8 @@ func New(cfg Config) *Client {
cfg.MaxBodySize = int64(40 * bytesize.MiB)
}
// Protect dialer with IP range sanitizer.
// Protect the dialer
// with IP range sanitizer.
d.Control = (&Sanitizer{
Allow: cfg.AllowRanges,
Block: cfg.BlockRanges,
@ -148,7 +153,7 @@ func New(cfg Config) *Client {
c.client.Timeout = cfg.Timeout
c.bodyMax = cfg.MaxBodySize
// Prepare TLS config for transport.
// Prepare transport TLS config.
tlsClientConfig := &tls.Config{
InsecureSkipVerify: cfg.TLSInsecureSkipVerify, //nolint:gosec
}
@ -334,7 +339,7 @@ func (c *Client) do(r *Request) (rsp *http.Response, retry bool, err error) {
// A retryable error.
return nil, true, err
} else if rsp.StatusCode > 500 ||
} else if rsp.StatusCode >= 500 ||
rsp.StatusCode == http.StatusTooManyRequests {
// Codes over 500 (and 429: too many requests)

View file

@ -50,10 +50,16 @@ type Request struct {
func WrapRequest(r *http.Request) Request {
var rr Request
rr.Request = r
rr.Entry = log.WithContext(r.Context()).
WithField("method", r.Method).
WithField("url", r.URL.String()).
WithField("contentType", r.Header.Get("Content-Type"))
entry := log.WithContext(r.Context())
entry = entry.WithField("method", r.Method)
entry = entry.WithField("url", r.URL.String())
if r.Body != nil {
// Only add content-type header if a request body exists.
entry = entry.WithField("contentType", r.Header.Get("Content-Type"))
}
// note our formatting library follows ptr values
entry = entry.WithField("attempt", &rr.attempts)
rr.Entry = entry
return rr
}

View file

@ -432,8 +432,8 @@ func (m *Manager) ProcessEmoji(
return nil, err
}
// Attempt to add this emoji processing item to the worker queue.
_ = m.state.Workers.Media.MustEnqueueCtx(ctx, emoji.Process)
// Attempt to add emoji item to the worker queue.
m.state.Workers.Media.Queue.Push(emoji.Process)
return emoji, nil
}

View file

@ -67,7 +67,7 @@ func (p *ProcessingEmoji) LoadEmoji(ctx context.Context) (*gtsmodel.Emoji, error
// Provided context was cancelled, e.g. request cancelled
// early. Queue this item for asynchronous processing.
log.Warnf(ctx, "reprocessing emoji %s after canceled ctx", p.emoji.ID)
go p.mgr.state.Workers.Media.Enqueue(p.Process)
p.mgr.state.Workers.Media.Queue.Push(p.Process)
}
return nil, err

View file

@ -27,7 +27,7 @@ import (
errorsv2 "codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-runners"
"codeberg.org/superseriousbusiness/exif-terminator"
terminator "codeberg.org/superseriousbusiness/exif-terminator"
"github.com/disintegration/imaging"
"github.com/h2non/filetype"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
@ -82,7 +82,7 @@ func (p *ProcessingMedia) LoadAttachment(ctx context.Context) (*gtsmodel.MediaAt
// asynchronous processing, which will
// use a background context.
log.Warnf(ctx, "reprocessing media %s after canceled ctx", p.media.ID)
go p.mgr.state.Workers.Media.Enqueue(p.Process)
p.mgr.state.Workers.Media.Queue.Push(p.Process)
}
// Media could not be retrieved FULLY,

View file

@ -20,25 +20,84 @@ package messages
import (
"net/url"
"codeberg.org/gruf/go-structr"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// FromClientAPI wraps a message that travels from the client API into the processor.
// FromClientAPI wraps a message that
// travels from the client API into the processor.
type FromClientAPI struct {
APObjectType string
// APObjectType ...
APObjectType string
// APActivityType ...
APActivityType string
GTSModel interface{}
OriginAccount *gtsmodel.Account
TargetAccount *gtsmodel.Account
// Optional GTS database model
// of the Activity / Object.
GTSModel interface{}
// Targeted object URI.
TargetURI string
// Origin is the account that
// this message originated from.
Origin *gtsmodel.Account
// Target is the account that
// this message is targeting.
Target *gtsmodel.Account
}
// FromFediAPI wraps a message that travels from the federating API into the processor.
type FromFediAPI struct {
APObjectType string
APActivityType string
APIri *url.URL
APObjectModel interface{} // Optional AP model of the Object of the Activity. Should be Accountable or Statusable.
GTSModel interface{} // Optional GTS model of the Activity or Object.
RequestingAccount *gtsmodel.Account // Remote account that posted this Activity to the inbox.
ReceivingAccount *gtsmodel.Account // Local account which owns the inbox that this Activity was posted to.
// ClientMsgIndices defines queue indices this
// message type should be accessible / stored under.
func ClientMsgIndices() []structr.IndexConfig {
return []structr.IndexConfig{
{Fields: "TargetURI", Multiple: true},
{Fields: "Origin.ID", Multiple: true},
{Fields: "Target.ID", Multiple: true},
}
}
// FromFediAPI wraps a message that
// travels from the federating API into the processor.
type FromFediAPI struct {
// APObjectType ...
APObjectType string
// APActivityType ...
APActivityType string
// Optional ActivityPub ID (IRI)
// and / or model of Activity / Object.
APIRI *url.URL
APObject interface{}
// Optional GTS database model
// of the Activity / Object.
GTSModel interface{}
// Targeted object URI.
TargetURI string
// Remote account that posted
// this Activity to the inbox.
Requesting *gtsmodel.Account
// Local account which owns the inbox
// that this Activity was posted to.
Receiving *gtsmodel.Account
}
// FederatorMsgIndices defines queue indices this
// message type should be accessible / stored under.
func FederatorMsgIndices() []structr.IndexConfig {
return []structr.IndexConfig{
{Fields: "APIRI", Multiple: true},
{Fields: "TargetURI", Multiple: true},
{Fields: "Requesting.ID", Multiple: true},
{Fields: "Receiving.ID", Multiple: true},
}
}

View file

@ -1,3 +1,20 @@
// 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 oauth
import (

View file

@ -23,26 +23,43 @@ package paging
func EitherMinID(minID, sinceID string) Boundary {
/*
Paging with `since_id` vs `min_id`:
Paging with `since_id` vs `min_id`:
limit = 4 limit = 4
+----------+ +----------+
max_id--> |xxxxxxxxxx| | | <-- max_id
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
since_id--> | | |xxxxxxxxxx| <-- min_id
+----------+ +----------+
| | | |
+----------+ +----------+
limit = 4 limit = 4
+----------+ +----------+
max_id--> |xxxxxxxxxx| | | <-- max_id
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| | |
+----------+ +----------+
|xxxxxxxxxx| |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
| | |xxxxxxxxxx|
+----------+ +----------+
since_id--> | | |xxxxxxxxxx| <-- min_id
+----------+ +----------+
| | | |
+----------+ +----------+
To sum it up in words:
when paging with since_id, max_id is used as
the cursor value, and since_id provides a
limiting value to the results.
when paging with min_id, min_id is used as
the cursor value, and max_id provides a
limiting value to the results.
But to further complicate it...
The "next" and "prev" relative links provided
in the link header are ALWAYS DESCENDING. Which
means we will ALWAYS provide next=?max_id and
prev=?min_id. *shakes fist at mastodon api*
*/
switch {
@ -57,7 +74,12 @@ func EitherMinID(minID, sinceID string) Boundary {
// SinceID ...
func SinceID(sinceID string) Boundary {
return Boundary{
Name: "since_id",
// even when a since_id query is
// provided, the next / prev rel
// links are DESCENDING with
// next:max_id and prev:min_id.
// so ALWAYS use min_id as name.
Name: "min_id",
Value: sinceID,
Order: OrderDescending,
}

View file

@ -202,8 +202,9 @@ func Page_PageFunc[WithID any](p *Page, in []WithID, get func(WithID) string) []
return in
}
// Next creates a new instance for the next returnable page, using
// given max value. This preserves original limit and max key name.
// Prev creates a new instance for the next returnable page, using
// given max value. This will always assume DESCENDING for Mastodon
// API compatibility, but in case of change it can support both.
func (p *Page) Next(lo, hi string) *Page {
if p == nil || lo == "" || hi == "" {
// no paging.
@ -216,25 +217,22 @@ func (p *Page) Next(lo, hi string) *Page {
// Set original limit.
p2.Limit = p.Limit
if p.order().Ascending() {
// When ascending, next page
// needs to start with min at
// the next highest value.
p2.Min = p.Min.new(hi)
p2.Max = p.Max.new("")
} else {
// When descending, next page
// needs to start with max at
// the next lowest value.
p2.Min = p.Min.new("")
p2.Max = p.Max.new(lo)
}
// NOTE:
// We ALWAYS assume the order
// when creating next / prev
// links is DESCENDING. It will
// always use prev: ?max_name
p2.Min = p.Min.new("")
p2.Max = p.Max.new(lo)
p2.Min.Order = OrderDescending
p2.Max.Order = OrderDescending
return p2
}
// Prev creates a new instance for the prev returnable page, using
// given min value. This preserves original limit and min key name.
// given min value. This will always assume DESCENDING for Mastodon
// API compatibility, but in case of change it can support both.
func (p *Page) Prev(lo, hi string) *Page {
if p == nil || lo == "" || hi == "" {
// no paging.
@ -247,19 +245,15 @@ func (p *Page) Prev(lo, hi string) *Page {
// Set original limit.
p2.Limit = p.Limit
if p.order().Ascending() {
// When ascending, prev page
// needs to start with max at
// the next lowest value.
p2.Min = p.Min.new("")
p2.Max = p.Max.new(lo)
} else {
// When descending, next page
// needs to start with max at
// the next lowest value.
p2.Min = p.Min.new(hi)
p2.Max = p.Max.new("")
}
// NOTE:
// We ALWAYS assume the order
// when creating next / prev
// links is DESCENDING. It will
// always use prev: ?min_name
p2.Min = p.Min.new(hi)
p2.Max = p.Max.new("")
p2.Min.Order = OrderDescending
p2.Max.Order = OrderDescending
return p2
}
@ -290,12 +284,12 @@ func (p *Page) ToLinkURL(proto, host, path string, queryParams url.Values) *url.
}
if p.Min.Value != "" {
// A page-minimum query parameter is available.
// Set page-minimum cursor value.
queryParams.Set(p.Min.Name, p.Min.Value)
}
if p.Max.Value != "" {
// A page-maximum query parameter is available.
// Set page-maximum cursor value.
queryParams.Set(p.Max.Name, p.Max.Value)
}

View file

@ -20,16 +20,14 @@ package paging_test
import (
"math/rand"
"slices"
"strconv"
"testing"
"time"
"github.com/oklog/ulid"
"github.com/stretchr/testify/assert"
"github.com/superseriousbusiness/gotosocial/internal/paging"
)
// random reader according to current-time source seed.
var randRd = rand.New(rand.NewSource(time.Now().Unix()))
type Case struct {
// Name is the test case name.
Name string
@ -63,13 +61,9 @@ func TestPage(t *testing.T) {
// Page the input slice.
out := c.Page.Page(c.Input)
// Log the results for case of error returns.
t.Logf("%s\npage=%+v input=%v expect=%v output=%v", c.Name, c.Page, c.Input, c.Expect, out)
// Check paged output is as expected.
if !slices.Equal(out, c.Expect) {
t.Error("unexpected paged output")
}
// Check paged output is expected.
assert.Equal(t, c.Expect, out,
"input=%#v page=%v", c.Input, c.Page)
})
}
}
@ -80,8 +74,7 @@ var cases = []Case{
slices.SortFunc(ids, ascending)
// Select random indices in slice.
minIdx := randRd.Intn(len(ids))
maxIdx := randRd.Intn(len(ids))
minIdx, maxIdx, _ := generateParams(len(ids))
// Select the boundaries.
minID := ids[minIdx]
@ -104,9 +97,7 @@ var cases = []Case{
slices.SortFunc(ids, ascending)
// Select random parameters in slice.
minIdx := randRd.Intn(len(ids))
maxIdx := randRd.Intn(len(ids))
limit := randRd.Intn(len(ids)) + 1
minIdx, maxIdx, limit := generateParams(len(ids))
// Select the boundaries.
minID := ids[minIdx]
@ -116,12 +107,10 @@ var cases = []Case{
expect := slices.Clone(ids)
expect = cutLower(expect, minID)
expect = cutUpper(expect, maxID)
slices.Reverse(expect)
// Now limit the slice.
if limit < len(expect) {
expect = expect[:limit]
}
slices.Reverse(expect)
// Return page and expected IDs.
return ids, &paging.Page{
@ -135,8 +124,7 @@ var cases = []Case{
slices.SortFunc(ids, ascending)
// Select random parameters in slice.
minIdx := randRd.Intn(len(ids))
maxIdx := randRd.Intn(len(ids))
minIdx, maxIdx, _ := generateParams(len(ids))
// Select the boundaries.
minID := ids[minIdx]
@ -160,8 +148,7 @@ var cases = []Case{
slices.SortFunc(ids, descending)
// Select random indices in slice.
sinceIdx := randRd.Intn(len(ids))
maxIdx := randRd.Intn(len(ids))
sinceIdx, maxIdx, _ := generateParams(len(ids))
// Select the boundaries.
sinceID := ids[sinceIdx]
@ -183,7 +170,7 @@ var cases = []Case{
slices.SortFunc(ids, descending)
// Select random indices in slice.
maxIdx := randRd.Intn(len(ids))
_, maxIdx, _ := generateParams(len(ids))
// Select the boundaries.
maxID := ids[maxIdx]
@ -202,7 +189,7 @@ var cases = []Case{
slices.SortFunc(ids, descending)
// Select random indices in slice.
sinceIdx := randRd.Intn(len(ids))
sinceIdx, _, _ := generateParams(len(ids))
// Select the boundaries.
sinceID := ids[sinceIdx]
@ -221,7 +208,7 @@ var cases = []Case{
slices.SortFunc(ids, ascending)
// Select random indices in slice.
minIdx := randRd.Intn(len(ids))
minIdx, _, _ := generateParams(len(ids))
// Select the boundaries.
minID := ids[minIdx]
@ -258,32 +245,34 @@ func cutUpper(in []string, bound string) []string {
return in
}
// random reader according to current-time source seed.
var randRd = rand.New(rand.NewSource(time.Now().Unix()))
// generateParams ...
func generateParams(n int) (minIdx int, maxIdx int, limit int) {
maxIdx = max(1, randRd.Intn(n))
minIdx = randRd.Intn(maxIdx)
limit = randRd.Intn(max(1, maxIdx-minIdx)) + 1
return
}
// generateSlice generates a new slice of len containing ascending sorted slice.
func generateSlice(len int) []string {
if len <= 0 {
if len <= 1 {
// minimum testable
// pageable amount
len = 2
}
now := time.Now()
in := make([]string, len)
for i := 0; i < len; i++ {
// Convert now to timestamp.
t := ulid.Timestamp(now)
// Create anew ulid for now.
u := ulid.MustNew(t, randRd)
// Add to slice.
in[i] = u.String()
// Bump now by 1 second.
now = now.Add(time.Second)
in[i] = strconv.Itoa(i)
}
return in
}
func ascending(a, b string) int {
func ascending(sa, sb string) int {
a, _ := strconv.Atoi(sa)
b, _ := strconv.Atoi(sb)
if a > b {
return 1
} else if a < b {
@ -292,7 +281,9 @@ func ascending(a, b string) int {
return 0
}
func descending(a, b string) int {
func descending(sa, sb string) int {
a, _ := strconv.Atoi(sa)
b, _ := strconv.Atoi(sb)
if a < b {
return 1
} else if a > b {

View file

@ -19,6 +19,7 @@ package account_test
import (
"context"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -48,7 +49,6 @@ type AccountStandardTestSuite struct {
state state.State
mediaManager *media.Manager
oauthServer oauth.Server
fromClientAPIChan chan messages.FromClientAPI
transportController transport.Controller
federator *federation.Federator
emailSender email.Sender
@ -68,6 +68,13 @@ type AccountStandardTestSuite struct {
accountProcessor account.Processor
}
func (suite *AccountStandardTestSuite) getClientMsg(timeout time.Duration) (*messages.FromClientAPI, bool) {
ctx := context.Background()
ctx, cncl := context.WithTimeout(ctx, timeout)
defer cncl()
return suite.state.Workers.Client.Queue.PopCtx(ctx)
}
func (suite *AccountStandardTestSuite) SetupSuite() {
suite.testTokens = testrig.NewTestTokens()
suite.testClients = testrig.NewTestClients()
@ -101,13 +108,6 @@ func (suite *AccountStandardTestSuite) SetupTest() {
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.fromClientAPIChan = make(chan messages.FromClientAPI, 100)
suite.state.Workers.EnqueueClientAPI = func(ctx context.Context, msgs ...messages.FromClientAPI) {
for _, msg := range msgs {
suite.fromClientAPIChan <- msg
}
}
suite.transportController = testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../testrig/media"))
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
suite.sentEmails = make(map[string]string)

View file

@ -83,16 +83,16 @@ func (p *Processor) BlockCreate(ctx context.Context, requestingAccount *gtsmodel
}
// Process block side effects (federation etc).
msgs = append(msgs, messages.FromClientAPI{
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityCreate,
GTSModel: block,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
Origin: requestingAccount,
Target: targetAccount,
})
// Batch queue accreted client api messages.
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
p.state.Workers.Client.Queue.Push(msgs...)
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}
@ -120,12 +120,12 @@ func (p *Processor) BlockRemove(ctx context.Context, requestingAccount *gtsmodel
existingBlock.TargetAccount = targetAccount
// Process block removal side effects (federation etc).
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityBlock,
APActivityType: ap.ActivityUndo,
GTSModel: existingBlock,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
Origin: requestingAccount,
Target: targetAccount,
})
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)

View file

@ -126,11 +126,11 @@ func (p *Processor) Create(
// There are side effects for creating a new account
// (confirmation emails etc), perform these async.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityCreate,
GTSModel: user,
OriginAccount: user.Account,
Origin: user.Account,
})
return user, nil

View file

@ -102,16 +102,13 @@ func (p *Processor) Delete(
// and the above Delete function will be called afterwards from the processor, to clear
// out the account's bits and bobs, and stubbify it.
func (p *Processor) DeleteSelf(ctx context.Context, account *gtsmodel.Account) gtserror.WithCode {
fromClientAPIMessage := messages.FromClientAPI{
// Process the delete side effects asynchronously.
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
OriginAccount: account,
TargetAccount: account,
}
// Process the delete side effects asynchronously.
p.state.Workers.EnqueueClientAPI(ctx, fromClientAPIMessage)
Origin: account,
Target: account,
})
return nil
}
@ -193,7 +190,8 @@ func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.
var (
// Use this slice to batch unfollow messages.
msgs = []messages.FromClientAPI{}
msgs = []*messages.FromClientAPI{}
// To avoid checking if account is local over + over
// inside the subsequent loops, just generate static
// side effects function once now.
@ -214,7 +212,7 @@ func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.
}
if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
// There was a side effect to process.
msgs = append(msgs, *msg)
msgs = append(msgs, msg)
}
}
@ -244,13 +242,13 @@ func (p *Processor) deleteAccountFollows(ctx context.Context, account *gtsmodel.
if msg := unfollowSideEffects(ctx, account, follow); msg != nil {
// There was a side effect to process.
msgs = append(msgs, *msg)
msgs = append(msgs, msg)
}
}
// Process accreted messages in serial.
for _, msg := range msgs {
if err := p.state.Workers.ProcessFromClientAPI(ctx, msg); err != nil {
if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
log.Errorf(
ctx,
"error processing %s of %s during Delete of account %s: %v",
@ -306,8 +304,8 @@ func (p *Processor) unfollowSideEffectsFunc(local bool) func(
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: follow,
OriginAccount: account,
TargetAccount: follow.TargetAccount,
Origin: account,
Target: follow.TargetAccount,
}
}
}
@ -337,7 +335,7 @@ func (p *Processor) deleteAccountStatuses(
statuses []*gtsmodel.Status
err error
maxID string
msgs = []messages.FromClientAPI{}
msgs = []*messages.FromClientAPI{}
)
statusLoop:
@ -404,29 +402,29 @@ statusLoop:
continue
}
msgs = append(msgs, messages.FromClientAPI{
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityUndo,
GTSModel: status,
OriginAccount: boost.Account,
TargetAccount: account,
Origin: boost.Account,
Target: account,
})
}
// Now prepare to Delete status.
msgs = append(msgs, messages.FromClientAPI{
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: status,
OriginAccount: account,
TargetAccount: account,
Origin: account,
Target: account,
})
}
}
// Process accreted messages in serial.
for _, msg := range msgs {
if err := p.state.Workers.ProcessFromClientAPI(ctx, msg); err != nil {
if err := p.state.Workers.Client.Process(ctx, msg); err != nil {
log.Errorf(
ctx,
"error processing %s of %s during Delete of account %s: %v",

View file

@ -117,12 +117,12 @@ func (p *Processor) FollowCreate(ctx context.Context, requestingAccount *gtsmode
} else {
// Otherwise we leave the follow request as it is,
// and we handle the rest of the process async.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: fr,
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
Origin: requestingAccount,
Target: targetAccount,
})
}
@ -143,7 +143,7 @@ func (p *Processor) FollowRemove(ctx context.Context, requestingAccount *gtsmode
}
// Batch queue accreted client api messages.
p.state.Workers.EnqueueClientAPI(ctx, msgs...)
p.state.Workers.Client.Queue.Push(msgs...)
return p.RelationshipGet(ctx, requestingAccount, targetAccountID)
}
@ -225,8 +225,8 @@ func (p *Processor) getFollowTarget(ctx context.Context, requester *gtsmodel.Acc
// If a follow and/or follow request was removed this way, one or two
// messages will be returned which should then be processed by a client
// api worker.
func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]messages.FromClientAPI, error) {
var msgs []messages.FromClientAPI
func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Account, targetAccount *gtsmodel.Account) ([]*messages.FromClientAPI, error) {
var msgs []*messages.FromClientAPI
// Get follow from requesting account to target account.
follow, err := p.state.DB.GetFollow(ctx, requestingAccount.ID, targetAccount.ID)
@ -251,7 +251,7 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
}
// Follow status changed, process side effects.
msgs = append(msgs, messages.FromClientAPI{
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
@ -259,8 +259,8 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
TargetAccountID: targetAccount.ID,
URI: follow.URI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
Origin: requestingAccount,
Target: targetAccount,
})
}
@ -287,7 +287,7 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
}
// Follow status changed, process side effects.
msgs = append(msgs, messages.FromClientAPI{
msgs = append(msgs, &messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityUndo,
GTSModel: &gtsmodel.Follow{
@ -295,8 +295,8 @@ func (p *Processor) unfollow(ctx context.Context, requestingAccount *gtsmodel.Ac
TargetAccountID: targetAccount.ID,
URI: followReq.URI,
},
OriginAccount: requestingAccount,
TargetAccount: targetAccount,
Origin: requestingAccount,
Target: targetAccount,
})
}

View file

@ -40,12 +40,12 @@ func (p *Processor) FollowRequestAccept(ctx context.Context, requestingAccount *
if follow.Account != nil {
// Only enqueue work in the case we have a request creating account stored.
// NOTE: due to how AcceptFollowRequest works, the inverse shouldn't be possible.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityAccept,
GTSModel: follow,
OriginAccount: follow.Account,
TargetAccount: follow.TargetAccount,
Origin: follow.Account,
Target: follow.TargetAccount,
})
}
@ -67,12 +67,12 @@ func (p *Processor) FollowRequestReject(ctx context.Context, requestingAccount *
if followRequest.Account != nil {
// Only enqueue work in the case we have a request creating account stored.
// NOTE: due to how GetFollowRequest works, the inverse shouldn't be possible.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityReject,
GTSModel: followRequest,
OriginAccount: followRequest.Account,
TargetAccount: followRequest.TargetAccount,
Origin: followRequest.Account,
Target: followRequest.TargetAccount,
})
}

View file

@ -25,7 +25,6 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
@ -152,18 +151,11 @@ func (suite *FollowTestSuite) TestFollowRequestLocal() {
}
// There should be a message going to the worker.
var cMsg messages.FromClientAPI
select {
case cMsg = <-suite.fromClientAPIChan:
// No problem.
case <-time.After(5 * time.Second):
suite.FailNow("timed out waiting for message")
}
cMsg, _ := suite.getClientMsg(5 * time.Second)
suite.Equal(ap.ActivityCreate, cMsg.APActivityType)
suite.Equal(ap.ActivityFollow, cMsg.APObjectType)
suite.Equal(requestingAccount.ID, cMsg.OriginAccount.ID)
suite.Equal(targetAccount.ID, cMsg.TargetAccount.ID)
suite.Equal(requestingAccount.ID, cMsg.Origin.ID)
suite.Equal(targetAccount.ID, cMsg.Target.ID)
}
func TestFollowTestS(t *testing.T) {

View file

@ -113,7 +113,7 @@ func (p *Processor) MoveSelf(
// in quick succession, so get a lock on
// this account.
lockKey := originAcct.URI
unlock := p.state.AccountLocks.Lock(lockKey)
unlock := p.state.ProcessingLocks.Lock(lockKey)
defer unlock()
// Ensure we have a valid, up-to-date representation of the target account.
@ -317,12 +317,12 @@ func (p *Processor) MoveSelf(
}
// Everything seems OK, process Move side effects async.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityMove,
GTSModel: move,
OriginAccount: originAcct,
TargetAccount: targetAcct,
Origin: originAcct,
Target: targetAcct,
})
return nil

View file

@ -70,29 +70,23 @@ func (suite *MoveTestSuite) TestMoveAccountOK() {
suite.FailNow(err.Error())
}
// There should be a msg heading back to fromClientAPI.
select {
case msg := <-suite.fromClientAPIChan:
move, ok := msg.GTSModel.(*gtsmodel.Move)
if !ok {
suite.FailNow("", "could not cast %T to *gtsmodel.Move", move)
}
now := time.Now()
suite.WithinDuration(now, move.CreatedAt, 5*time.Second)
suite.WithinDuration(now, move.UpdatedAt, 5*time.Second)
suite.WithinDuration(now, move.AttemptedAt, 5*time.Second)
suite.Zero(move.SucceededAt)
suite.NotZero(move.ID)
suite.Equal(requestingAcct.URI, move.OriginURI)
suite.NotNil(move.Origin)
suite.Equal(targetAcct.URI, move.TargetURI)
suite.NotNil(move.Target)
suite.NotZero(move.URI)
case <-time.After(5 * time.Second):
suite.FailNow("time out waiting for message")
// There should be a message going to the worker.
cMsg, _ := suite.getClientMsg(5 * time.Second)
move, ok := cMsg.GTSModel.(*gtsmodel.Move)
if !ok {
suite.FailNow("", "could not cast %T to *gtsmodel.Move", move)
}
now := time.Now()
suite.WithinDuration(now, move.CreatedAt, 5*time.Second)
suite.WithinDuration(now, move.UpdatedAt, 5*time.Second)
suite.WithinDuration(now, move.AttemptedAt, 5*time.Second)
suite.Zero(move.SucceededAt)
suite.NotZero(move.ID)
suite.Equal(requestingAcct.URI, move.OriginURI)
suite.NotNil(move.Origin)
suite.Equal(targetAcct.URI, move.TargetURI)
suite.NotNil(move.Target)
suite.NotZero(move.URI)
// Move should be in the database now.
move, err := suite.state.DB.GetMoveByOriginTarget(

View file

@ -296,11 +296,11 @@ func (p *Processor) Update(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorInternalError(fmt.Errorf("could not update account settings %s: %s", account.ID, err))
}
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityUpdate,
GTSModel: account,
OriginAccount: account,
Origin: account,
})
acctSensitive, err := p.converter.AccountToAPIAccountSensitive(ctx, account)

View file

@ -20,6 +20,7 @@ package account_test
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/ap"
@ -31,20 +32,6 @@ type AccountUpdateTestSuite struct {
AccountStandardTestSuite
}
func (suite *AccountStandardTestSuite) checkClientAPIChan(accountID string) {
msg := <-suite.fromClientAPIChan
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.OriginAccount == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(accountID, msg.OriginAccount.ID)
}
func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
testAccount := &gtsmodel.Account{}
*testAccount = *suite.testAccounts["local_account_1"]
@ -73,7 +60,17 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateSimple() {
suite.Equal(noteExpected, apiAccount.Note)
// We should have an update in the client api channel.
suite.checkClientAPIChan(testAccount.ID)
msg, _ := suite.getClientMsg(5 * time.Second)
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(testAccount.ID, msg.Origin.ID)
// Check database model of account as well.
dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)
@ -113,7 +110,17 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMention() {
suite.Equal(noteExpected, apiAccount.Note)
// We should have an update in the client api channel.
suite.checkClientAPIChan(testAccount.ID)
msg, _ := suite.getClientMsg(5 * time.Second)
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(testAccount.ID, msg.Origin.ID)
// Check database model of account as well.
dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)
@ -159,7 +166,17 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithMarkdownNote() {
suite.Equal(noteExpected, apiAccount.Note)
// We should have an update in the client api channel.
suite.checkClientAPIChan(testAccount.ID)
msg, _ := suite.getClientMsg(5 * time.Second)
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(testAccount.ID, msg.Origin.ID)
// Check database model of account as well.
dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)
@ -234,7 +251,17 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateWithFields() {
suite.EqualValues(emojisExpected, apiAccount.Emojis)
// We should have an update in the client api channel.
suite.checkClientAPIChan(testAccount.ID)
msg, _ := suite.getClientMsg(5 * time.Second)
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(testAccount.ID, msg.Origin.ID)
// Check database model of account as well.
dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)
@ -281,7 +308,17 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateNoteNotFields() {
suite.Equal(fieldsBefore, len(apiAccount.Fields))
// We should have an update in the client api channel.
suite.checkClientAPIChan(testAccount.ID)
msg, _ := suite.getClientMsg(5 * time.Second)
// Profile update.
suite.Equal(ap.ActivityUpdate, msg.APActivityType)
suite.Equal(ap.ObjectProfile, msg.APObjectType)
// Correct account updated.
if msg.Origin == nil {
suite.FailNow("expected msg.OriginAccount not to be nil")
}
suite.Equal(testAccount.ID, msg.Origin.ID)
// Check database model of account as well.
dbAccount, err := suite.db.GetAccountByID(ctx, testAccount.ID)

View file

@ -80,13 +80,13 @@ func (p *Processor) accountActionSuspend(
Text: text,
},
func(ctx context.Context) gtserror.MultiError {
if err := p.state.Workers.ProcessFromClientAPI(
if err := p.state.Workers.Client.Process(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
OriginAccount: adminAcct,
TargetAccount: targetAcct,
Origin: adminAcct,
Target: targetAcct,
},
); err != nil {
errs := gtserror.NewMultiError(1)

View file

@ -49,17 +49,17 @@ func (p *Processor) AccountApprove(
// Get a lock on the account URI,
// to ensure it's not also being
// rejected at the same time!
unlock := p.state.AccountLocks.Lock(user.Account.URI)
unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
defer unlock()
if !*user.Approved {
// Process approval side effects asynschronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityAccept,
GTSModel: user,
OriginAccount: adminAcct,
TargetAccount: user.Account,
Origin: adminAcct,
Target: user.Account,
})
}

View file

@ -52,7 +52,7 @@ func (p *Processor) AccountReject(
// Get a lock on the account URI,
// since we're going to be deleting
// it and its associated user.
unlock := p.state.AccountLocks.Lock(user.Account.URI)
unlock := p.state.ProcessingLocks.Lock(user.Account.URI)
defer unlock()
// Can't reject an account with a
@ -101,12 +101,12 @@ func (p *Processor) AccountReject(
}
// Process rejection side effects asynschronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityReject,
GTSModel: deniedUser,
OriginAccount: adminAcct,
TargetAccount: user.Account,
Origin: adminAcct,
Target: user.Account,
})
return apiAccount, nil

View file

@ -115,8 +115,12 @@ func (p *Processor) AccountsGet(
return paging.EmptyResponse(), nil
}
hi := accounts[count-1].ID
lo := accounts[0].ID
var (
loAcct = accounts[count-1]
hiAcct = accounts[0]
lo = loAcct.Domain + "/@" + loAcct.Username
hi = hiAcct.Domain + "/@" + hiAcct.Username
)
items := make([]interface{}, 0, count)
for _, account := range accounts {

View file

@ -23,6 +23,7 @@ import (
"sync"
"time"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -97,8 +98,10 @@ func (a *Actions) Run(
// we're done modifying it for now.
a.m.Unlock()
// Do the rest of the work asynchronously.
a.state.Workers.ClientAPI.Enqueue(func(ctx context.Context) {
go func() {
// Use a background context with existing values.
ctx = gtscontext.WithValues(context.Background(), ctx)
// Run the thing and collect errors.
if errs := f(ctx); errs != nil {
action.Errors = make([]string, 0, len(errs))
@ -119,7 +122,7 @@ func (a *Actions) Run(
if err := a.state.DB.UpdateAdminAction(ctx, action, "completed_at", "errors"); err != nil {
log.Errorf(ctx, "db error marking action %s as completed: %q", actionKey, err)
}
})
}()
return nil
}

View file

@ -201,15 +201,13 @@ func (p *Processor) domainBlockSideEffects(
// process an account delete message to remove
// that account's posts, media, etc.
if err := p.rangeDomainAccounts(ctx, block.Domain, func(account *gtsmodel.Account) {
cMsg := messages.FromClientAPI{
if err := p.state.Workers.Client.Process(ctx, &messages.FromClientAPI{
APObjectType: ap.ActorPerson,
APActivityType: ap.ActivityDelete,
GTSModel: block,
OriginAccount: account,
TargetAccount: account,
}
if err := p.state.Workers.ProcessFromClientAPI(ctx, cMsg); err != nil {
Origin: account,
Target: account,
}); err != nil {
errs.Append(err)
}
}); err != nil {

View file

@ -140,12 +140,12 @@ func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account
}
// Process side effects of closing the report.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityFlag,
APActivityType: ap.ActivityUpdate,
GTSModel: report,
OriginAccount: account,
TargetAccount: report.Account,
Origin: account,
Target: report.Account,
})
apimodelReport, err := p.converter.ReportToAdminAPIReport(ctx, updatedReport, account)

View file

@ -62,7 +62,7 @@ func (p *Processor) StatusGet(ctx context.Context, requestedUser string, statusI
}
if !visible {
const text = "status not vising to requesting account"
const text = "status not visible to requesting account"
return nil, gtserror.NewErrorNotFound(errors.New(text))
}

View file

@ -116,11 +116,11 @@ func (p *Processor) onExpiry(pollID string) func(context.Context, time.Time) {
// Enqueue a status update operation to the client API worker,
// this will asynchronously send an update with the Poll close time.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APActivityType: ap.ActivityUpdate,
APObjectType: ap.ObjectNote,
GTSModel: status,
OriginAccount: status.Account,
Origin: status.Account,
})
}
}

View file

@ -96,11 +96,11 @@ func (p *Processor) PollVote(ctx context.Context, requester *gtsmodel.Account, p
poll.IncrementVotes(choices)
// Enqueue worker task to handle side-effects of user poll vote(s).
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APActivityType: ap.ActivityCreate,
APObjectType: ap.ActivityQuestion,
GTSModel: vote, // the vote choices
OriginAccount: requester,
Origin: requester,
})
// Return converted API model poll.

View file

@ -95,7 +95,6 @@ func (suite *ProcessingStandardTestSuite) SetupSuite() {
func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.StartNoopWorkers(&suite.state)
testrig.InitTestConfig()
testrig.InitTestLog()
@ -124,8 +123,7 @@ func (suite *ProcessingStandardTestSuite) SetupTest() {
suite.emailSender = testrig.NewEmailSender("../../web/template/", nil)
suite.processor = processing.NewProcessor(cleaner.New(&suite.state), suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
suite.state.Workers.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
testrig.StartWorkers(&suite.state, suite.processor.Workers())
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../testrig/media")

View file

@ -91,12 +91,12 @@ func (p *Processor) Create(ctx context.Context, account *gtsmodel.Account, form
return nil, gtserror.NewErrorInternalError(err)
}
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityFlag,
GTSModel: report,
OriginAccount: account,
TargetAccount: targetAccount,
Origin: account,
Target: targetAccount,
})
apiReport, err := p.converter.ReportToAPIReport(ctx, report)

View file

@ -89,12 +89,12 @@ func (p *Processor) BoostCreate(
}
// Process side effects asynchronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: boost,
OriginAccount: requester,
TargetAccount: target.Account,
Origin: requester,
Target: target.Account,
})
return p.c.GetAPIStatus(ctx, requester, boost)
@ -141,12 +141,12 @@ func (p *Processor) BoostRemove(
if boost != nil {
// Status was boosted. Process unboost side effects asynchronously.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityUndo,
GTSModel: boost,
OriginAccount: requester,
TargetAccount: target.Account,
Origin: requester,
Target: target.Account,
})
}

View file

@ -143,11 +143,11 @@ func (p *Processor) Create(
}
// send it back to the client API worker for async side-effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: requester,
Origin: requester,
})
if status.Poll != nil {

View file

@ -51,12 +51,12 @@ func (p *Processor) Delete(ctx context.Context, requestingAccount *gtsmodel.Acco
}
// Process delete side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: targetStatus,
OriginAccount: requestingAccount,
TargetAccount: requestingAccount,
Origin: requestingAccount,
Target: requestingAccount,
})
return apiStatus, nil

View file

@ -107,12 +107,12 @@ func (p *Processor) FaveCreate(ctx context.Context, requestingAccount *gtsmodel.
}
// Process new status fave side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: gtsFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
Origin: requestingAccount,
Target: targetStatus.Account,
})
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)
@ -137,12 +137,12 @@ func (p *Processor) FaveRemove(ctx context.Context, requestingAccount *gtsmodel.
}
// Process remove status fave side effects.
p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{
p.state.Workers.Client.Queue.Push(&messages.FromClientAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityUndo,
GTSModel: existingFave,
OriginAccount: requestingAccount,
TargetAccount: targetStatus.Account,
Origin: requestingAccount,
Target: targetStatus.Account,
})
return p.c.GetAPIStatus(ctx, requestingAccount, targetStatus)

View file

@ -83,7 +83,7 @@ func (p *Processor) PinCreate(ctx context.Context, requestingAccount *gtsmodel.A
}
// Get a lock on this account.
unlock := p.state.AccountLocks.Lock(requestingAccount.URI)
unlock := p.state.ProcessingLocks.Lock(requestingAccount.URI)
defer unlock()
if !targetStatus.PinnedAt.IsZero() {
@ -148,7 +148,7 @@ func (p *Processor) PinRemove(ctx context.Context, requestingAccount *gtsmodel.A
}
// Get a lock on this account.
unlock := p.state.AccountLocks.Lock(requestingAccount.URI)
unlock := p.state.ProcessingLocks.Lock(requestingAccount.URI)
defer unlock()
if targetStatus.PinnedAt.IsZero() {

View file

@ -75,12 +75,6 @@ func (f *federate) DeleteAccount(ctx context.Context, account *gtsmodel.Account)
return nil
}
// Drop any queued outgoing AP requests to / from account,
// (this stops any queued likes, boosts, creates etc).
f.state.Workers.Delivery.Queue.Delete("ActorID", account.URI)
f.state.Workers.Delivery.Queue.Delete("ObjectID", account.URI)
f.state.Workers.Delivery.Queue.Delete("TargetID", account.URI)
// Parse relevant URI(s).
outboxIRI, err := parseURI(account.OutboxURI)
if err != nil {
@ -228,16 +222,6 @@ func (f *federate) DeleteStatus(ctx context.Context, status *gtsmodel.Status) er
return nil
}
// Drop any queued outgoing http requests for status,
// (this stops any queued likes, boosts, creates etc).
f.state.Workers.Delivery.Queue.Delete("ObjectID", status.URI)
f.state.Workers.Delivery.Queue.Delete("TargetID", status.URI)
// Ensure the status model is fully populated.
if err := f.state.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status: %w", err)
}
// Parse the outbox URI of the status author.
outboxIRI, err := parseURI(status.Account.OutboxURI)
if err != nil {

View file

@ -25,7 +25,6 @@ import (
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -42,32 +41,18 @@ import (
type clientAPI struct {
state *state.State
converter *typeutils.Converter
surface *surface
surface *Surface
federate *federate
account *account.Processor
utilF *utilF
utils *utils
}
func (p *Processor) EnqueueClientAPI(cctx context.Context, msgs ...messages.FromClientAPI) {
_ = p.workers.ClientAPI.MustEnqueueCtx(cctx, func(wctx context.Context) {
// Copy caller ctx values to worker's.
wctx = gtscontext.WithValues(wctx, cctx)
// Process worker messages.
for _, msg := range msgs {
if err := p.ProcessFromClientAPI(wctx, msg); err != nil {
log.Errorf(wctx, "error processing client API message: %v", err)
}
}
})
}
func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg *messages.FromClientAPI) error {
// Allocate new log fields slice
fields := make([]kv.Field, 3, 4)
fields[0] = kv.Field{"activityType", cMsg.APActivityType}
fields[1] = kv.Field{"objectType", cMsg.APObjectType}
fields[2] = kv.Field{"fromAccount", cMsg.OriginAccount.Username}
fields[2] = kv.Field{"fromAccount", cMsg.Origin.Username}
// Include GTSModel in logs if appropriate.
if cMsg.GTSModel != nil &&
@ -217,7 +202,7 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, cMsg messages.From
return gtserror.Newf("unhandled: %s %s", cMsg.APActivityType, cMsg.APObjectType)
}
func (p *clientAPI) CreateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
@ -241,14 +226,14 @@ func (p *clientAPI) CreateAccount(ctx context.Context, cMsg messages.FromClientA
return nil
}
func (p *clientAPI) CreateStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
}
// Update stats for the actor account.
if err := p.utilF.incrementStatusesCount(ctx, cMsg.OriginAccount, status); err != nil {
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, status); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -269,7 +254,7 @@ func (p *clientAPI) CreateStatus(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg *messages.FromClientAPI) error {
// Cast the create poll vote attached to message.
vote, ok := cMsg.GTSModel.(*gtsmodel.PollVote)
if !ok {
@ -310,14 +295,14 @@ func (p *clientAPI) CreatePollVote(ctx context.Context, cMsg messages.FromClient
return nil
}
func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg *messages.FromClientAPI) error {
followRequest, ok := cMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
}
// Update stats for the target account.
if err := p.utilF.incrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
if err := p.utils.incrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -338,7 +323,7 @@ func (p *clientAPI) CreateFollowReq(ctx context.Context, cMsg messages.FromClien
return nil
}
func (p *clientAPI) CreateLike(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateLike(ctx context.Context, cMsg *messages.FromClientAPI) error {
fave, ok := cMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
@ -364,14 +349,14 @@ func (p *clientAPI) CreateLike(ctx context.Context, cMsg messages.FromClientAPI)
return nil
}
func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
boost, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
}
// Update stats for the actor account.
if err := p.utilF.incrementStatusesCount(ctx, cMsg.OriginAccount, boost); err != nil {
if err := p.utils.incrementStatusesCount(ctx, cMsg.Origin, boost); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -396,7 +381,7 @@ func (p *clientAPI) CreateAnnounce(ctx context.Context, cMsg messages.FromClient
return nil
}
func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) CreateBlock(ctx context.Context, cMsg *messages.FromClientAPI) error {
block, ok := cMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Block", cMsg.GTSModel)
@ -430,7 +415,7 @@ func (p *clientAPI) CreateBlock(ctx context.Context, cMsg messages.FromClientAPI
return nil
}
func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
// Cast the updated Status model attached to msg.
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
@ -462,7 +447,7 @@ func (p *clientAPI) UpdateStatus(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
account, ok := cMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return gtserror.Newf("cannot cast %T -> *gtsmodel.Account", cMsg.GTSModel)
@ -475,7 +460,7 @@ func (p *clientAPI) UpdateAccount(ctx context.Context, cMsg messages.FromClientA
return nil
}
func (p *clientAPI) UpdateReport(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UpdateReport(ctx context.Context, cMsg *messages.FromClientAPI) error {
report, ok := cMsg.GTSModel.(*gtsmodel.Report)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Report", cMsg.GTSModel)
@ -494,23 +479,23 @@ func (p *clientAPI) UpdateReport(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg *messages.FromClientAPI) error {
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
// Update stats for the target account.
if err := p.utilF.decrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
if err := p.utils.decrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
if err := p.utilF.incrementFollowersCount(ctx, cMsg.TargetAccount); err != nil {
if err := p.utils.incrementFollowersCount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Update stats for the origin account.
if err := p.utilF.incrementFollowingCount(ctx, cMsg.OriginAccount); err != nil {
if err := p.utils.incrementFollowingCount(ctx, cMsg.Origin); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -525,14 +510,14 @@ func (p *clientAPI) AcceptFollow(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg *messages.FromClientAPI) error {
followReq, ok := cMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", cMsg.GTSModel)
}
// Update stats for the target account.
if err := p.utilF.decrementFollowRequestsCount(ctx, cMsg.TargetAccount); err != nil {
if err := p.utils.decrementFollowRequestsCount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -546,19 +531,19 @@ func (p *clientAPI) RejectFollowRequest(ctx context.Context, cMsg messages.FromC
return nil
}
func (p *clientAPI) UndoFollow(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UndoFollow(ctx context.Context, cMsg *messages.FromClientAPI) error {
follow, ok := cMsg.GTSModel.(*gtsmodel.Follow)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Follow", cMsg.GTSModel)
}
// Update stats for the origin account.
if err := p.utilF.decrementFollowingCount(ctx, cMsg.OriginAccount); err != nil {
if err := p.utils.decrementFollowingCount(ctx, cMsg.Origin); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Update stats for the target account.
if err := p.utilF.decrementFollowersCount(ctx, cMsg.TargetAccount); err != nil {
if err := p.utils.decrementFollowersCount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -569,7 +554,7 @@ func (p *clientAPI) UndoFollow(ctx context.Context, cMsg messages.FromClientAPI)
return nil
}
func (p *clientAPI) UndoBlock(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UndoBlock(ctx context.Context, cMsg *messages.FromClientAPI) error {
block, ok := cMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Block", cMsg.GTSModel)
@ -582,7 +567,7 @@ func (p *clientAPI) UndoBlock(ctx context.Context, cMsg messages.FromClientAPI)
return nil
}
func (p *clientAPI) UndoFave(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UndoFave(ctx context.Context, cMsg *messages.FromClientAPI) error {
statusFave, ok := cMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", cMsg.GTSModel)
@ -599,7 +584,7 @@ func (p *clientAPI) UndoFave(ctx context.Context, cMsg messages.FromClientAPI) e
return nil
}
func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg *messages.FromClientAPI) error {
status, ok := cMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", cMsg.GTSModel)
@ -610,7 +595,7 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg messages.FromClientAP
}
// Update stats for the origin account.
if err := p.utilF.decrementStatusesCount(ctx, cMsg.OriginAccount); err != nil {
if err := p.utils.decrementStatusesCount(ctx, cMsg.Origin); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -629,7 +614,7 @@ func (p *clientAPI) UndoAnnounce(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg *messages.FromClientAPI) error {
// Don't delete attachments, just unattach them:
// this request comes from the client API and the
// poster may want to use attachments again later.
@ -648,12 +633,26 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP
return gtserror.Newf("db error populating status: %w", err)
}
if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
// Drop any outgoing queued AP requests about / targeting
// this status, (stops queued likes, boosts, creates etc).
p.state.Workers.Delivery.Queue.Delete("ObjectID", status.URI)
p.state.Workers.Delivery.Queue.Delete("TargetID", status.URI)
// Drop any incoming queued client messages about / targeting
// status, (stops processing of local origin data for status).
p.state.Workers.Client.Queue.Delete("TargetURI", status.URI)
// Drop any incoming queued federator messages targeting status,
// (stops processing of remote origin data targeting this status).
p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI)
// First perform the actual status deletion.
if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
// Update stats for the origin account.
if err := p.utilF.decrementStatusesCount(ctx, cMsg.OriginAccount); err != nil {
if err := p.utils.decrementStatusesCount(ctx, cMsg.Origin); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -670,7 +669,7 @@ func (p *clientAPI) DeleteStatus(ctx context.Context, cMsg messages.FromClientAP
return nil
}
func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
// The originID of the delete, one of:
// - ID of a domain block, for which
// this account delete is a side effect.
@ -684,21 +683,41 @@ func (p *clientAPI) DeleteAccount(ctx context.Context, cMsg messages.FromClientA
} else {
// Origin is whichever account
// originated this message.
originID = cMsg.OriginAccount.ID
originID = cMsg.Origin.ID
}
if err := p.federate.DeleteAccount(ctx, cMsg.TargetAccount); err != nil {
// Extract target account.
account := cMsg.Target
// Drop any outgoing queued AP requests to / from / targeting
// this account, (stops queued likes, boosts, creates etc).
p.state.Workers.Delivery.Queue.Delete("ActorID", account.URI)
p.state.Workers.Delivery.Queue.Delete("ObjectID", account.URI)
p.state.Workers.Delivery.Queue.Delete("TargetID", account.URI)
// Drop any incoming queued client messages to / from this
// account, (stops processing of local origin data for acccount).
p.state.Workers.Client.Queue.Delete("Origin.ID", account.ID)
p.state.Workers.Client.Queue.Delete("Target.ID", account.ID)
p.state.Workers.Client.Queue.Delete("TargetURI", account.URI)
// Drop any incoming queued federator messages to this account,
// (stops processing of remote origin data targeting this account).
p.state.Workers.Federator.Queue.Delete("Receiving.ID", account.ID)
p.state.Workers.Federator.Queue.Delete("TargetURI", account.URI)
if err := p.federate.DeleteAccount(ctx, cMsg.Target); err != nil {
log.Errorf(ctx, "error federating account delete: %v", err)
}
if err := p.account.Delete(ctx, cMsg.TargetAccount, originID); err != nil {
if err := p.account.Delete(ctx, cMsg.Target, originID); err != nil {
log.Errorf(ctx, "error deleting account: %v", err)
}
return nil
}
func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) ReportAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
report, ok := cMsg.GTSModel.(*gtsmodel.Report)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Report", cMsg.GTSModel)
@ -719,28 +738,28 @@ func (p *clientAPI) ReportAccount(ctx context.Context, cMsg messages.FromClientA
return nil
}
func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) MoveAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
// Redirect each local follower of
// OriginAccount to follow move target.
p.utilF.redirectFollowers(ctx, cMsg.OriginAccount, cMsg.TargetAccount)
p.utils.redirectFollowers(ctx, cMsg.Origin, cMsg.Target)
// At this point, we know OriginAccount has the
// Move set on it. Just make sure it's populated.
if err := p.state.DB.PopulateMove(ctx, cMsg.OriginAccount.Move); err != nil {
if err := p.state.DB.PopulateMove(ctx, cMsg.Origin.Move); err != nil {
return gtserror.Newf("error populating Move: %w", err)
}
// Now send the Move message out to
// OriginAccount's (remote) followers.
if err := p.federate.MoveAccount(ctx, cMsg.OriginAccount); err != nil {
if err := p.federate.MoveAccount(ctx, cMsg.Origin); err != nil {
return gtserror.Newf("error federating account move: %w", err)
}
// Mark the move attempt as successful.
cMsg.OriginAccount.Move.SucceededAt = cMsg.OriginAccount.Move.AttemptedAt
cMsg.Origin.Move.SucceededAt = cMsg.Origin.Move.AttemptedAt
if err := p.state.DB.UpdateMove(
ctx,
cMsg.OriginAccount.Move,
cMsg.Origin.Move,
"succeeded_at",
); err != nil {
return gtserror.Newf("error marking move as successful: %w", err)
@ -749,7 +768,7 @@ func (p *clientAPI) MoveAccount(ctx context.Context, cMsg messages.FromClientAPI
return nil
}
func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
newUser, ok := cMsg.GTSModel.(*gtsmodel.User)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.User", cMsg.GTSModel)
@ -772,17 +791,17 @@ func (p *clientAPI) AcceptAccount(ctx context.Context, cMsg messages.FromClientA
return nil
}
func (p *clientAPI) RejectAccount(ctx context.Context, cMsg messages.FromClientAPI) error {
func (p *clientAPI) RejectAccount(ctx context.Context, cMsg *messages.FromClientAPI) error {
deniedUser, ok := cMsg.GTSModel.(*gtsmodel.DeniedUser)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.DeniedUser", cMsg.GTSModel)
}
// Remove the account.
if err := p.state.DB.DeleteAccount(ctx, cMsg.TargetAccount.ID); err != nil {
if err := p.state.DB.DeleteAccount(ctx, cMsg.Target.ID); err != nil {
log.Errorf(ctx,
"db error deleting account %s: %v",
cMsg.TargetAccount.ID, err,
cMsg.Target.ID, err,
)
}

View file

@ -32,7 +32,9 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/messages"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -43,6 +45,7 @@ type FromClientAPITestSuite struct {
func (suite *FromClientAPITestSuite) newStatus(
ctx context.Context,
state *state.State,
account *gtsmodel.Account,
visibility gtsmodel.Visibility,
replyToStatus *gtsmodel.Status,
@ -87,7 +90,7 @@ func (suite *FromClientAPITestSuite) newStatus(
TargetAccountID: replyToStatus.AccountID,
}
if err := suite.db.PutMention(ctx, mention); err != nil {
if err := state.DB.PutMention(ctx, mention); err != nil {
suite.FailNow(err.Error())
}
newStatus.Mentions = []*gtsmodel.Mention{mention}
@ -104,7 +107,7 @@ func (suite *FromClientAPITestSuite) newStatus(
// Put the status in the db, to mimic what would
// have already happened earlier up the flow.
if err := suite.db.PutStatus(ctx, newStatus); err != nil {
if err := state.DB.PutStatus(ctx, newStatus); err != nil {
suite.FailNow(err.Error())
}
@ -144,10 +147,11 @@ func (suite *FromClientAPITestSuite) checkStreamed(
func (suite *FromClientAPITestSuite) statusJSON(
ctx context.Context,
typeConverter *typeutils.Converter,
status *gtsmodel.Status,
requestingAccount *gtsmodel.Account,
) string {
apiStatus, err := suite.typeconverter.StatusToAPIStatus(
apiStatus, err := typeConverter.StatusToAPIStatus(
ctx,
status,
requestingAccount,
@ -167,19 +171,27 @@ func (suite *FromClientAPITestSuite) statusJSON(
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
notifStream = streams[stream.TimelineNotifications]
streams = suite.openStreams(ctx,
testStructs.Processor,
receivingAccount,
[]string{testList.ID},
)
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
notifStream = streams[stream.TimelineNotifications]
// Admin account posts a new top-level status.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
@ -193,18 +205,18 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
*follow = *suite.testFollows["local_account_1_admin_account"]
follow.Notify = util.Ptr(true)
if err := suite.db.UpdateFollow(ctx, follow); err != nil {
if err := testStructs.State.DB.UpdateFollow(ctx, follow); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -212,6 +224,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -236,7 +249,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
var notif *gtsmodel.Notification
if !testrig.WaitFor(func() bool {
var err error
notif, err = suite.db.GetNotification(
notif, err = testStructs.State.DB.GetNotification(
ctx,
gtsmodel.NotificationStatus,
receivingAccount.ID,
@ -248,7 +261,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
suite.FailNow("timed out waiting for new status notification")
}
apiNotif, err := suite.typeconverter.NotificationToAPINotification(ctx, notif, nil)
apiNotif, err := testStructs.TypeConverter.NotificationToAPINotification(ctx, notif, nil)
if err != nil {
suite.FailNow(err.Error())
}
@ -268,12 +281,15 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusWithNotification() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
@ -284,6 +300,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
// post should also show in the list stream.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_2_status_1"],
@ -292,13 +309,13 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
)
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -306,6 +323,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -328,6 +346,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReply() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
@ -338,6 +359,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
// for this, but zork mutes this thread.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_1_status_1"],
@ -351,25 +373,25 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
)
// Store the thread mute before processing new status.
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
if err := testStructs.State.DB.PutThreadMute(ctx, threadMute); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// Ensure no notification received.
notif, err := suite.db.GetNotification(
notif, err := testStructs.State.DB.GetNotification(
ctx,
gtsmodel.NotificationMention,
receivingAccount.ID,
@ -382,6 +404,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyMuted() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
@ -392,6 +417,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
// for this, but zork mutes this thread.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
@ -405,25 +431,25 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
)
// Store the thread mute before processing new status.
if err := suite.db.PutThreadMute(ctx, threadMute); err != nil {
if err := testStructs.State.DB.PutThreadMute(ctx, threadMute); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
}
// Ensure no notification received.
notif, err := suite.db.GetNotification(
notif, err := testStructs.State.DB.GetNotification(
ctx,
gtsmodel.NotificationReblog,
receivingAccount.ID,
@ -436,6 +462,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostMuted() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyOK() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
*testList = *suite.testLists["local_account_1_list_1"]
@ -444,13 +473,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
// Admin account posts a reply to turtle.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_2_status_1"],
@ -463,18 +493,18 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
// and admin are in the same list, this means the reply
// should be shown in the list.
testList.RepliesPolicy = gtsmodel.RepliesPolicyList
if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
if err := testStructs.State.DB.UpdateList(ctx, testList, "replies_policy"); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -482,6 +512,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -504,6 +535,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyListOnlyNo() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
*testList = *suite.testLists["local_account_1_list_1"]
@ -512,13 +546,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
// Admin account posts a reply to turtle.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_2_status_1"],
@ -531,23 +566,23 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
// about to remove turtle from the same list as admin,
// so the new post should not be streamed to the list.
testList.RepliesPolicy = gtsmodel.RepliesPolicyList
if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
if err := testStructs.State.DB.UpdateList(ctx, testList, "replies_policy"); err != nil {
suite.FailNow(err.Error())
}
// Remove turtle from the list.
if err := suite.db.DeleteListEntry(ctx, suite.testListEntries["local_account_1_list_1_entry_1"].ID); err != nil {
if err := testStructs.State.DB.DeleteListEntry(ctx, suite.testListEntries["local_account_1_list_1_entry_1"].ID); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -555,6 +590,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -577,6 +613,9 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusListRepliesPolicyLis
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPolicyNone() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
// We're modifying the test list so take a copy.
testList := new(gtsmodel.List)
*testList = *suite.testLists["local_account_1_list_1"]
@ -585,13 +624,14 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
// Admin account posts a reply to turtle.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
suite.testStatuses["local_account_2_status_1"],
@ -604,18 +644,18 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
// show any replies, the post should not
// be streamed to the list.
testList.RepliesPolicy = gtsmodel.RepliesPolicyNone
if err := suite.db.UpdateList(ctx, testList, "replies_policy"); err != nil {
if err := testStructs.State.DB.UpdateList(ctx, testList, "replies_policy"); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -623,6 +663,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -645,18 +686,22 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusReplyListRepliesPoli
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
// Admin account boosts a post by turtle.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
@ -665,13 +710,13 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
)
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -679,6 +724,7 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
statusJSON := suite.statusJSON(
ctx,
testStructs.TypeConverter,
status,
receivingAccount,
)
@ -701,18 +747,22 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoost() {
}
func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
postingAccount = suite.testAccounts["admin_account"]
receivingAccount = suite.testAccounts["local_account_1"]
testList = suite.testLists["local_account_1_list_1"]
streams = suite.openStreams(ctx, receivingAccount, []string{testList.ID})
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, []string{testList.ID})
homeStream = streams[stream.TimelineHome]
listStream = streams[stream.TimelineList+":"+testList.ID]
// Admin account boosts a post by turtle.
status = suite.newStatus(
ctx,
testStructs.State,
postingAccount,
gtsmodel.VisibilityPublic,
nil,
@ -725,18 +775,18 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
follow := new(gtsmodel.Follow)
*follow = *suite.testFollows["local_account_1_admin_account"]
follow.ShowReblogs = util.Ptr(false)
if err := suite.db.UpdateFollow(ctx, follow, "show_reblogs"); err != nil {
if err := testStructs.State.DB.UpdateFollow(ctx, follow, "show_reblogs"); err != nil {
suite.FailNow(err.Error())
}
// Process the new status.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: status,
OriginAccount: postingAccount,
Origin: postingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -760,30 +810,33 @@ func (suite *FromClientAPITestSuite) TestProcessCreateStatusBoostNoReblogs() {
}
func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
var (
ctx = context.Background()
deletingAccount = suite.testAccounts["local_account_1"]
receivingAccount = suite.testAccounts["local_account_2"]
deletedStatus = suite.testStatuses["local_account_1_status_1"]
boostOfDeletedStatus = suite.testStatuses["admin_account_status_4"]
streams = suite.openStreams(ctx, receivingAccount, nil)
streams = suite.openStreams(ctx, testStructs.Processor, receivingAccount, nil)
homeStream = streams[stream.TimelineHome]
)
// Delete the status from the db first, to mimic what
// would have already happened earlier up the flow
if err := suite.db.DeleteStatusByID(ctx, deletedStatus.ID); err != nil {
if err := testStructs.State.DB.DeleteStatusByID(ctx, deletedStatus.ID); err != nil {
suite.FailNow(err.Error())
}
// Process the status delete.
if err := suite.processor.Workers().ProcessFromClientAPI(
if err := testStructs.Processor.Workers().ProcessFromClientAPI(
ctx,
messages.FromClientAPI{
&messages.FromClientAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityDelete,
GTSModel: deletedStatus,
OriginAccount: deletingAccount,
Origin: deletingAccount,
},
); err != nil {
suite.FailNow(err.Error())
@ -809,7 +862,7 @@ func (suite *FromClientAPITestSuite) TestProcessStatusDelete() {
// Boost should no longer be in the database.
if !testrig.WaitFor(func() bool {
_, err := suite.db.GetStatusByID(ctx, boostOfDeletedStatus.ID)
_, err := testStructs.State.DB.GetStatusByID(ctx, boostOfDeletedStatus.ID)
return errors.Is(err, db.ErrNoEntries)
}) {
suite.FailNow("timed out waiting for status delete")

View file

@ -19,13 +19,14 @@ package workers
import (
"context"
"errors"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
@ -40,37 +41,23 @@ import (
// from the federation/ActivityPub API.
type fediAPI struct {
state *state.State
surface *surface
surface *Surface
federate *federate
account *account.Processor
utilF *utilF
utils *utils
}
func (p *Processor) EnqueueFediAPI(cctx context.Context, msgs ...messages.FromFediAPI) {
_ = p.workers.Federator.MustEnqueueCtx(cctx, func(wctx context.Context) {
// Copy caller ctx values to worker's.
wctx = gtscontext.WithValues(wctx, cctx)
// Process worker messages.
for _, msg := range msgs {
if err := p.ProcessFromFediAPI(wctx, msg); err != nil {
log.Errorf(wctx, "error processing fedi API message: %v", err)
}
}
})
}
func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Allocate new log fields slice
fields := make([]kv.Field, 3, 5)
fields[0] = kv.Field{"activityType", fMsg.APActivityType}
fields[1] = kv.Field{"objectType", fMsg.APObjectType}
fields[2] = kv.Field{"toAccount", fMsg.ReceivingAccount.Username}
fields[2] = kv.Field{"toAccount", fMsg.Receiving.Username}
if fMsg.APIri != nil {
if fMsg.APIRI != nil {
// An IRI was supplied, append to log
fields = append(fields, kv.Field{
"iri", fMsg.APIri,
"iri", fMsg.APIRI,
})
}
@ -168,7 +155,7 @@ func (p *Processor) ProcessFromFediAPI(ctx context.Context, fMsg messages.FromFe
return gtserror.Newf("unhandled: %s %s", fMsg.APActivityType, fMsg.APObjectType)
}
func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
var (
status *gtsmodel.Status
statusable ap.Statusable
@ -178,11 +165,11 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
var ok bool
switch {
case fMsg.APObjectModel != nil:
case fMsg.APObject != nil:
// A model was provided, extract this from message.
statusable, ok = fMsg.APObjectModel.(ap.Statusable)
statusable, ok = fMsg.APObject.(ap.Statusable)
if !ok {
return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObjectModel)
return gtserror.Newf("cannot cast %T -> ap.Statusable", fMsg.APObject)
}
// Create bare-bones model to pass
@ -196,7 +183,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
// statusable model, which it will use to further flesh out
// the bare bones model and insert it into the database.
status, statusable, err = p.federate.RefreshStatus(ctx,
fMsg.ReceivingAccount.Username,
fMsg.Receiving.Username,
bareStatus,
statusable,
// Force refresh within 5min window.
@ -206,15 +193,15 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return gtserror.Newf("error processing new status %s: %w", bareStatus.URI, err)
}
case fMsg.APIri != nil:
case fMsg.APIRI != nil:
// Model was not set, deref with IRI (this is a forward).
// This will also cause the status to be inserted into the db.
status, statusable, err = p.federate.GetStatusByURI(ctx,
fMsg.ReceivingAccount.Username,
fMsg.APIri,
fMsg.Receiving.Username,
fMsg.APIRI,
)
if err != nil {
return gtserror.Newf("error dereferencing forwarded status %s: %w", fMsg.APIri, err)
return gtserror.Newf("error dereferencing forwarded status %s: %w", fMsg.APIRI, err)
}
default:
@ -230,7 +217,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
}
// Update stats for the remote account.
if err := p.utilF.incrementStatusesCount(ctx, fMsg.RequestingAccount, status); err != nil {
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, status); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -248,7 +235,7 @@ func (p *fediAPI) CreateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return nil
}
func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Cast poll vote type from the worker message.
vote, ok := fMsg.GTSModel.(*gtsmodel.PollVote)
if !ok {
@ -293,7 +280,7 @@ func (p *fediAPI) CreatePollVote(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg *messages.FromFediAPI) error {
followRequest, ok := fMsg.GTSModel.(*gtsmodel.FollowRequest)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.FollowRequest", fMsg.GTSModel)
@ -310,7 +297,7 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI
}
// And update stats for the local account.
if err := p.utilF.incrementFollowRequestsCount(ctx, fMsg.ReceivingAccount); err != nil {
if err := p.utils.incrementFollowRequestsCount(ctx, fMsg.Receiving); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -330,12 +317,12 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI
}
// Update stats for the local account.
if err := p.utilF.incrementFollowersCount(ctx, fMsg.ReceivingAccount); err != nil {
if err := p.utils.incrementFollowersCount(ctx, fMsg.Receiving); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Update stats for the remote account.
if err := p.utilF.incrementFollowingCount(ctx, fMsg.RequestingAccount); err != nil {
if err := p.utils.incrementFollowingCount(ctx, fMsg.Requesting); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -350,7 +337,7 @@ func (p *fediAPI) CreateFollowReq(ctx context.Context, fMsg messages.FromFediAPI
return nil
}
func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateLike(ctx context.Context, fMsg *messages.FromFediAPI) error {
fave, ok := fMsg.GTSModel.(*gtsmodel.StatusFave)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.StatusFave", fMsg.GTSModel)
@ -372,7 +359,7 @@ func (p *fediAPI) CreateLike(ctx context.Context, fMsg messages.FromFediAPI) err
return nil
}
func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg *messages.FromFediAPI) error {
boost, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
@ -386,7 +373,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI)
boost, err = p.federate.EnrichAnnounce(
ctx,
boost,
fMsg.ReceivingAccount.Username,
fMsg.Receiving.Username,
)
if err != nil {
if gtserror.IsUnretrievable(err) {
@ -400,7 +387,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI)
}
// Update stats for the remote account.
if err := p.utilF.incrementStatusesCount(ctx, fMsg.RequestingAccount, boost); err != nil {
if err := p.utils.incrementStatusesCount(ctx, fMsg.Requesting, boost); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -420,7 +407,7 @@ func (p *fediAPI) CreateAnnounce(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
func (p *fediAPI) CreateBlock(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateBlock(ctx context.Context, fMsg *messages.FromFediAPI) error {
block, ok := fMsg.GTSModel.(*gtsmodel.Block)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Block", fMsg.GTSModel)
@ -499,7 +486,7 @@ func (p *fediAPI) CreateBlock(ctx context.Context, fMsg messages.FromFediAPI) er
return nil
}
func (p *fediAPI) CreateFlag(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) CreateFlag(ctx context.Context, fMsg *messages.FromFediAPI) error {
incomingReport, ok := fMsg.GTSModel.(*gtsmodel.Report)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Report", fMsg.GTSModel)
@ -515,7 +502,7 @@ func (p *fediAPI) CreateFlag(ctx context.Context, fMsg messages.FromFediAPI) err
return nil
}
func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Parse the old/existing account model.
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
if !ok {
@ -523,15 +510,15 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
}
// Because this was an Update, the new Accountable should be set on the message.
apubAcc, ok := fMsg.APObjectModel.(ap.Accountable)
apubAcc, ok := fMsg.APObject.(ap.Accountable)
if !ok {
return gtserror.Newf("cannot cast %T -> ap.Accountable", fMsg.APObjectModel)
return gtserror.Newf("cannot cast %T -> ap.Accountable", fMsg.APObject)
}
// Fetch up-to-date bio, avatar, header, etc.
_, _, err := p.federate.RefreshAccount(
ctx,
fMsg.ReceivingAccount.Username,
fMsg.Receiving.Username,
account,
apubAcc,
// Force refresh within 5min window.
@ -544,25 +531,25 @@ func (p *fediAPI) UpdateAccount(ctx context.Context, fMsg messages.FromFediAPI)
return nil
}
func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) AcceptFollow(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Update stats for the remote account.
if err := p.utilF.decrementFollowRequestsCount(ctx, fMsg.RequestingAccount); err != nil {
if err := p.utils.decrementFollowRequestsCount(ctx, fMsg.Requesting); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
if err := p.utilF.incrementFollowersCount(ctx, fMsg.RequestingAccount); err != nil {
if err := p.utils.incrementFollowersCount(ctx, fMsg.Requesting); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
// Update stats for the local account.
if err := p.utilF.incrementFollowingCount(ctx, fMsg.ReceivingAccount); err != nil {
if err := p.utils.incrementFollowingCount(ctx, fMsg.Receiving); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
return nil
}
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Cast the existing Status model attached to msg.
existing, ok := fMsg.GTSModel.(*gtsmodel.Status)
if !ok {
@ -570,12 +557,12 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
}
// Cast the updated ActivityPub statusable object .
apStatus, _ := fMsg.APObjectModel.(ap.Statusable)
apStatus, _ := fMsg.APObject.(ap.Statusable)
// Fetch up-to-date attach status attachments, etc.
status, _, err := p.federate.RefreshStatus(
ctx,
fMsg.ReceivingAccount.Username,
fMsg.Receiving.Username,
existing,
apStatus,
// Force refresh within 5min window.
@ -605,7 +592,7 @@ func (p *fediAPI) UpdateStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return nil
}
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg *messages.FromFediAPI) error {
// Delete attachments from this status, since this request
// comes from the federating API, and there's no way the
// poster can do a delete + redraft for it on our instance.
@ -616,12 +603,34 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return gtserror.Newf("%T not parseable as *gtsmodel.Status", fMsg.GTSModel)
}
if err := p.utilF.wipeStatus(ctx, status, deleteAttachments); err != nil {
// Try to populate status structs if possible,
// in order to more thoroughly remove them.
if err := p.state.DB.PopulateStatus(
ctx, status,
); err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.Newf("db error populating status: %w", err)
}
// Drop any outgoing queued AP requests about / targeting
// this status, (stops queued likes, boosts, creates etc).
p.state.Workers.Delivery.Queue.Delete("ObjectID", status.URI)
p.state.Workers.Delivery.Queue.Delete("TargetID", status.URI)
// Drop any incoming queued client messages about / targeting
// status, (stops processing of local origin data for status).
p.state.Workers.Client.Queue.Delete("TargetURI", status.URI)
// Drop any incoming queued federator messages targeting status,
// (stops processing of remote origin data targeting this status).
p.state.Workers.Federator.Queue.Delete("TargetURI", status.URI)
// First perform the actual status deletion.
if err := p.utils.wipeStatus(ctx, status, deleteAttachments); err != nil {
log.Errorf(ctx, "error wiping status: %v", err)
}
// Update stats for the remote account.
if err := p.utilF.decrementStatusesCount(ctx, fMsg.RequestingAccount); err != nil {
if err := p.utils.decrementStatusesCount(ctx, fMsg.Requesting); err != nil {
log.Errorf(ctx, "error updating account stats: %v", err)
}
@ -634,12 +643,28 @@ func (p *fediAPI) DeleteStatus(ctx context.Context, fMsg messages.FromFediAPI) e
return nil
}
func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
func (p *fediAPI) DeleteAccount(ctx context.Context, fMsg *messages.FromFediAPI) error {
account, ok := fMsg.GTSModel.(*gtsmodel.Account)
if !ok {
return gtserror.Newf("%T not parseable as *gtsmodel.Account", fMsg.GTSModel)
}
// Drop any outgoing queued AP requests to / from / targeting
// this account, (stops queued likes, boosts, creates etc).
p.state.Workers.Delivery.Queue.Delete("ObjectID", account.URI)
p.state.Workers.Delivery.Queue.Delete("TargetID", account.URI)
// Drop any incoming queued client messages to / from this
// account, (stops processing of local origin data for acccount).
p.state.Workers.Client.Queue.Delete("Target.ID", account.ID)
p.state.Workers.Client.Queue.Delete("TargetURI", account.URI)
// Drop any incoming queued federator messages to this account,
// (stops processing of remote origin data targeting this account).
p.state.Workers.Federator.Queue.Delete("Requesting.ID", account.ID)
p.state.Workers.Federator.Queue.Delete("TargetURI", account.URI)
// First perform the actual account deletion.
if err := p.account.Delete(ctx, account, account.ID); err != nil {
log.Errorf(ctx, "error deleting account: %v", err)
}

View file

@ -220,10 +220,7 @@ func (p *fediAPI) GetOrCreateMove(
// APActivityType: "Move"
// GTSModel: stub *gtsmodel.Move.
// ReceivingAccount: Account of inbox owner receiving the Move.
func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) error {
// The account who received the Move message.
receiver := fMsg.ReceivingAccount
func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) error {
// *gtsmodel.Move activity.
stubMove, ok := fMsg.GTSModel.(*gtsmodel.Move)
if !ok {
@ -236,7 +233,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
// Move origin and target info.
var (
originAcctURIStr = stubMove.OriginURI
originAcct = fMsg.RequestingAccount
originAcct = fMsg.Requesting
targetAcctURIStr = stubMove.TargetURI
targetAcctURI = stubMove.Target
)
@ -308,7 +305,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
// Account to which the Move is taking place.
targetAcct, targetAcctable, err := p.federate.GetAccountByURI(
ctx,
receiver.Username,
fMsg.Receiving.Username,
targetAcctURI,
)
if err != nil {
@ -340,7 +337,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
// Force refresh Move target account
// to ensure we have up-to-date version.
targetAcct, _, err = p.federate.RefreshAccount(ctx,
receiver.Username,
fMsg.Receiving.Username,
targetAcct,
targetAcctable,
dereferencing.Freshest,
@ -379,7 +376,7 @@ func (p *fediAPI) MoveAccount(ctx context.Context, fMsg messages.FromFediAPI) er
// Transfer originAcct's followers
// on this instance to targetAcct.
redirectOK := p.utilF.redirectFollowers(
redirectOK := p.utils.redirectFollowers(
ctx,
originAcct,
targetAcct,

View file

@ -42,6 +42,9 @@ type FromFediAPITestSuite struct {
// remote_account_1 boosts the first status of local_account_1
func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
boostedStatus := suite.testStatuses["local_account_1_status_1"]
boostingAccount := suite.testAccounts["remote_account_1"]
announceStatus := &gtsmodel.Status{}
@ -54,19 +57,19 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
announceStatus.Account = boostingAccount
announceStatus.Visibility = boostedStatus.Visibility
err := suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: announceStatus,
ReceivingAccount: suite.testAccounts["local_account_1"],
RequestingAccount: boostingAccount,
err := testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
APObjectType: ap.ActivityAnnounce,
APActivityType: ap.ActivityCreate,
GTSModel: announceStatus,
Receiving: suite.testAccounts["local_account_1"],
Requesting: boostingAccount,
})
suite.NoError(err)
// side effects should be triggered
// 1. status should have an ID, and be in the database
suite.NotEmpty(announceStatus.ID)
_, err = suite.db.GetStatusByID(context.Background(), announceStatus.ID)
_, err = testStructs.State.DB.GetStatusByID(context.Background(), announceStatus.ID)
suite.NoError(err)
// 2. a notification should exist for the announce
@ -77,7 +80,7 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
},
}
notif := &gtsmodel.Notification{}
err = suite.db.GetWhere(context.Background(), where, notif)
err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationReblog, notif.NotificationType)
suite.Equal(boostedStatus.AccountID, notif.TargetAccountID)
@ -87,6 +90,9 @@ func (suite *FromFediAPITestSuite) TestProcessFederationAnnounce() {
}
func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
repliedAccount := suite.testAccounts["local_account_1"]
repliedStatus := suite.testStatuses["local_account_1_status_1"]
replyingAccount := suite.testAccounts["remote_account_1"]
@ -97,7 +103,7 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
replyingAccount.FetchedAt = time.Now()
replyingAccount.SuspendedAt = time.Time{}
replyingAccount.SuspensionOrigin = ""
err := suite.state.DB.UpdateAccount(context.Background(),
err := testStructs.State.DB.UpdateAccount(context.Background(),
replyingAccount,
"fetched_at",
"suspended_at",
@ -111,27 +117,27 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
ap.AppendInReplyTo(replyingStatusable, testrig.URLMustParse(repliedStatus.URI))
// Open a websocket stream to later test the streamed status reply.
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), repliedAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// Send the replied status off to the fedi worker to be further processed.
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APObjectModel: replyingStatusable,
ReceivingAccount: repliedAccount,
RequestingAccount: replyingAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
APObject: replyingStatusable,
Receiving: repliedAccount,
Requesting: replyingAccount,
})
suite.NoError(err)
// side effects should be triggered
// 1. status should be in the database
replyingStatus, err := suite.state.DB.GetStatusByURI(context.Background(), replyingURI)
replyingStatus, err := testStructs.State.DB.GetStatusByURI(context.Background(), replyingURI)
suite.NoError(err)
// 2. a notification should exist for the mention
var notif gtsmodel.Notification
err = suite.db.GetWhere(context.Background(), []db.Where{
err = testStructs.State.DB.GetWhere(context.Background(), []db.Where{
{Key: "status_id", Value: replyingStatus.ID},
}, &notif)
suite.NoError(err)
@ -156,11 +162,14 @@ func (suite *FromFediAPITestSuite) TestProcessReplyMention() {
}
func (suite *FromFediAPITestSuite) TestProcessFave() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
favedAccount := suite.testAccounts["local_account_1"]
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications)
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), favedAccount, stream.TimelineNotifications)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@ -176,15 +185,15 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
URI: favingAccount.URI + "/faves/aaaaaaaaaaaa",
}
err := suite.db.Put(context.Background(), fave)
err := testStructs.State.DB.Put(context.Background(), fave)
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
ReceivingAccount: favedAccount,
RequestingAccount: favingAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
Receiving: favedAccount,
Requesting: favingAccount,
})
suite.NoError(err)
@ -202,7 +211,7 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
}
notif := &gtsmodel.Notification{}
err = suite.db.GetWhere(context.Background(), where, notif)
err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationFave, notif.NotificationType)
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
@ -225,12 +234,15 @@ func (suite *FromFediAPITestSuite) TestProcessFave() {
// This tests for an issue we were seeing where Misskey sends out faves to inboxes of people that don't own
// the fave, but just follow the actor who received the fave.
func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
receivingAccount := suite.testAccounts["local_account_2"]
favedAccount := suite.testAccounts["local_account_1"]
favedStatus := suite.testStatuses["local_account_1_status_1"]
favingAccount := suite.testAccounts["remote_account_1"]
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome)
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), receivingAccount, stream.TimelineHome)
suite.NoError(errWithCode)
fave := &gtsmodel.StatusFave{
@ -246,15 +258,15 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
URI: favingAccount.URI + "/faves/aaaaaaaaaaaa",
}
err := suite.db.Put(context.Background(), fave)
err := testStructs.State.DB.Put(context.Background(), fave)
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(context.Background(), messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
ReceivingAccount: receivingAccount,
RequestingAccount: favingAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(context.Background(), &messages.FromFediAPI{
APObjectType: ap.ActivityLike,
APActivityType: ap.ActivityCreate,
GTSModel: fave,
Receiving: receivingAccount,
Requesting: favingAccount,
})
suite.NoError(err)
@ -272,7 +284,7 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
}
notif := &gtsmodel.Notification{}
err = suite.db.GetWhere(context.Background(), where, notif)
err = testStructs.State.DB.GetWhere(context.Background(), where, notif)
suite.NoError(err)
suite.Equal(gtsmodel.NotificationFave, notif.NotificationType)
suite.Equal(fave.TargetAccountID, notif.TargetAccountID)
@ -287,6 +299,9 @@ func (suite *FromFediAPITestSuite) TestProcessFaveWithDifferentReceivingAccount(
}
func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
ctx := context.Background()
deletedAccount := suite.testAccounts["remote_account_1"]
@ -304,7 +319,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
URI: fmt.Sprintf("%s/follows/01FGRY72ASHBSET64353DPHK9T", deletedAccount.URI),
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, zorkFollowSatan)
err := testStructs.State.DB.Put(ctx, zorkFollowSatan)
suite.NoError(err)
satanFollowZork := &gtsmodel.Follow{
@ -317,36 +332,36 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
URI: fmt.Sprintf("%s/follows/01FGRYAVAWWPP926J175QGM0WV", receivingAccount.URI),
Notify: util.Ptr(false),
}
err = suite.db.Put(ctx, satanFollowZork)
err = testStructs.State.DB.Put(ctx, satanFollowZork)
suite.NoError(err)
// now they are mufos!
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: deletedAccount,
ReceivingAccount: receivingAccount,
RequestingAccount: deletedAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityDelete,
GTSModel: deletedAccount,
Receiving: receivingAccount,
Requesting: deletedAccount,
})
suite.NoError(err)
// local account 2 blocked foss_satan, that block should be gone now
testBlock := suite.testBlocks["local_account_2_block_remote_account_1"]
dbBlock := &gtsmodel.Block{}
err = suite.db.GetByID(ctx, testBlock.ID, dbBlock)
err = testStructs.State.DB.GetByID(ctx, testBlock.ID, dbBlock)
suite.ErrorIs(err, db.ErrNoEntries)
// the mufos should be gone now too
satanFollowsZork, err := suite.db.IsFollowing(ctx, deletedAccount.ID, receivingAccount.ID)
satanFollowsZork, err := testStructs.State.DB.IsFollowing(ctx, deletedAccount.ID, receivingAccount.ID)
suite.NoError(err)
suite.False(satanFollowsZork)
zorkFollowsSatan, err := suite.db.IsFollowing(ctx, receivingAccount.ID, deletedAccount.ID)
zorkFollowsSatan, err := testStructs.State.DB.IsFollowing(ctx, receivingAccount.ID, deletedAccount.ID)
suite.NoError(err)
suite.False(zorkFollowsSatan)
// no statuses from foss satan should be left in the database
if !testrig.WaitFor(func() bool {
s, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false)
s, err := testStructs.State.DB.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false)
return s == nil && err == db.ErrNoEntries
}) {
suite.FailNow("timeout waiting for statuses to be deleted")
@ -356,7 +371,7 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
// account data should be zeroed.
if !testrig.WaitFor(func() bool {
dbAccount, err = suite.db.GetAccountByID(ctx, deletedAccount.ID)
dbAccount, err = testStructs.State.DB.GetAccountByID(ctx, deletedAccount.ID)
return err == nil && dbAccount.DisplayName == ""
}) {
suite.FailNow("timeout waiting for statuses to be deleted")
@ -375,6 +390,9 @@ func (suite *FromFediAPITestSuite) TestProcessAccountDelete() {
}
func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
ctx := context.Background()
originAccount := suite.testAccounts["remote_account_1"]
@ -382,7 +400,7 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
// target is a locked account
targetAccount := suite.testAccounts["local_account_2"]
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already
@ -399,15 +417,15 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, satanFollowRequestTurtle)
err := testStructs.State.DB.Put(ctx, satanFollowRequestTurtle)
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
ReceivingAccount: targetAccount,
RequestingAccount: originAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
Receiving: targetAccount,
Requesting: originAccount,
})
suite.NoError(err)
@ -425,10 +443,13 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestLocked() {
suite.Equal(originAccount.ID, notif.Account.ID)
// no messages should have been sent out, since we didn't need to federate an accept
suite.Empty(&suite.httpClient.SentMessages)
suite.Empty(testStructs.HTTPClient.SentMessages)
}
func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
ctx := context.Background()
originAccount := suite.testAccounts["remote_account_1"]
@ -436,7 +457,7 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
// target is an unlocked account
targetAccount := suite.testAccounts["local_account_1"]
wssStream, errWithCode := suite.processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
wssStream, errWithCode := testStructs.Processor.Stream().Open(context.Background(), targetAccount, stream.TimelineHome)
suite.NoError(errWithCode)
// put the follow request in the database as though it had passed through the federating db already
@ -453,15 +474,15 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
Notify: util.Ptr(false),
}
err := suite.db.Put(ctx, satanFollowRequestTurtle)
err := testStructs.State.DB.Put(ctx, satanFollowRequestTurtle)
suite.NoError(err)
err = suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
ReceivingAccount: targetAccount,
RequestingAccount: originAccount,
err = testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
APObjectType: ap.ActivityFollow,
APActivityType: ap.ActivityCreate,
GTSModel: satanFollowRequestTurtle,
Receiving: targetAccount,
Requesting: originAccount,
})
suite.NoError(err)
@ -482,7 +503,7 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
// an accept message should be sent to satan's inbox
var sent []byte
if !testrig.WaitFor(func() bool {
delivery, ok := suite.state.Workers.Delivery.Queue.Pop()
delivery, ok := testStructs.State.Workers.Delivery.Queue.Pop()
if !ok {
return false
}
@ -527,28 +548,34 @@ func (suite *FromFediAPITestSuite) TestProcessFollowRequestUnlocked() {
// TestCreateStatusFromIRI checks if a forwarded status can be dereferenced by the processor.
func (suite *FromFediAPITestSuite) TestCreateStatusFromIRI() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
ctx := context.Background()
receivingAccount := suite.testAccounts["local_account_1"]
statusCreator := suite.testAccounts["remote_account_2"]
err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
ReceivingAccount: receivingAccount,
RequestingAccount: statusCreator,
APIri: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
APObjectType: ap.ObjectNote,
APActivityType: ap.ActivityCreate,
GTSModel: nil, // gtsmodel is nil because this is a forwarded status -- we want to dereference it using the iri
Receiving: receivingAccount,
Requesting: statusCreator,
APIRI: testrig.URLMustParse("http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
})
suite.NoError(err)
// status should now be in the database, attributed to remote_account_2
s, err := suite.db.GetStatusByURI(context.Background(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1")
s, err := testStructs.State.DB.GetStatusByURI(context.Background(), "http://example.org/users/Some_User/statuses/afaba698-5740-4e32-a702-af61aa543bc1")
suite.NoError(err)
suite.Equal(statusCreator.URI, s.AccountURI)
}
func (suite *FromFediAPITestSuite) TestMoveAccount() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
// We're gonna migrate foss_satan to our local admin account.
ctx := context.Background()
receivingAcct := suite.testAccounts["local_account_1"]
@ -562,12 +589,12 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
// Set alsoKnownAs on the admin account.
targetAcct.AlsoKnownAsURIs = []string{requestingAcct.URI}
if err := suite.state.DB.UpdateAccount(ctx, targetAcct, "also_known_as_uris"); err != nil {
if err := testStructs.State.DB.UpdateAccount(ctx, targetAcct, "also_known_as_uris"); err != nil {
suite.FailNow(err.Error())
}
// Remove existing follow from zork to admin account.
if err := suite.state.DB.DeleteFollowByID(
if err := testStructs.State.DB.DeleteFollowByID(
ctx,
suite.testFollows["local_account_1_admin_account"].ID,
); err != nil {
@ -575,7 +602,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
}
// Have Zork follow foss_satan instead.
if err := suite.state.DB.PutFollow(ctx, &gtsmodel.Follow{
if err := testStructs.State.DB.PutFollow(ctx, &gtsmodel.Follow{
ID: "01HRA0XZYFZC5MNWTKEBR58SSE",
URI: "http://localhost:8080/users/the_mighty_zork/follows/01HRA0XZYFZC5MNWTKEBR58SSE",
AccountID: receivingAcct.ID,
@ -585,7 +612,7 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
}
// Process the Move.
err := suite.processor.Workers().ProcessFromFediAPI(ctx, messages.FromFediAPI{
err := testStructs.Processor.Workers().ProcessFromFediAPI(ctx, &messages.FromFediAPI{
APObjectType: ap.ObjectProfile,
APActivityType: ap.ActivityMove,
GTSModel: &gtsmodel.Move{
@ -595,20 +622,20 @@ func (suite *FromFediAPITestSuite) TestMoveAccount() {
Target: testrig.URLMustParse(targetAcct.URI),
URI: "https://fossbros-anonymous.io/users/foss_satan/moves/01HRA064871MR8HGVSAFJ333GM",
},
ReceivingAccount: receivingAcct,
RequestingAccount: requestingAcct,
Receiving: receivingAcct,
Requesting: requestingAcct,
})
suite.NoError(err)
// Zork should now be following admin account.
follows, err := suite.state.DB.IsFollowing(ctx, receivingAcct.ID, targetAcct.ID)
follows, err := testStructs.State.DB.IsFollowing(ctx, receivingAcct.ID, targetAcct.ID)
if err != nil {
suite.FailNow(err.Error())
}
suite.True(follows)
// Move should be in the DB.
move, err := suite.state.DB.GetMoveByURI(ctx, "https://fossbros-anonymous.io/users/foss_satan/moves/01HRA064871MR8HGVSAFJ333GM")
move, err := testStructs.State.DB.GetMoveByURI(ctx, "https://fossbros-anonymous.io/users/foss_satan/moves/01HRA064871MR8HGVSAFJ333GM")
if err != nil {
suite.FailNow(err.Error())
}

View file

@ -25,16 +25,16 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
// surface wraps functions for 'surfacing' the result
// Surface wraps functions for 'surfacing' the result
// of processing a message, eg:
// - timelining a status
// - removing a status from timelines
// - sending a notification to a user
// - sending an email
type surface struct {
state *state.State
converter *typeutils.Converter
stream *stream.Processor
filter *visibility.Filter
emailSender email.Sender
type Surface struct {
State *state.State
Converter *typeutils.Converter
Stream *stream.Processor
Filter *visibility.Filter
EmailSender email.Sender
}

View file

@ -33,8 +33,8 @@ import (
// emailUserReportClosed emails the user who created the
// given report, to inform them the report has been closed.
func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error {
user, err := s.state.DB.GetUserByAccountID(ctx, report.Account.ID)
func (s *Surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Report) error {
user, err := s.State.DB.GetUserByAccountID(ctx, report.Account.ID)
if err != nil {
return gtserror.Newf("db error getting user: %w", err)
}
@ -51,12 +51,12 @@ func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re
return nil
}
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("db error getting instance: %w", err)
}
if err := s.state.DB.PopulateReport(ctx, report); err != nil {
if err := s.State.DB.PopulateReport(ctx, report); err != nil {
return gtserror.Newf("error populating report: %w", err)
}
@ -69,12 +69,12 @@ func (s *surface) emailUserReportClosed(ctx context.Context, report *gtsmodel.Re
ActionTakenComment: report.ActionTaken,
}
return s.emailSender.SendReportClosedEmail(user.Email, reportClosedData)
return s.EmailSender.SendReportClosedEmail(user.Email, reportClosedData)
}
// emailUserPleaseConfirm emails the given user
// to ask them to confirm their email address.
func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error {
func (s *Surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.User) error {
if user.UnconfirmedEmail == "" ||
user.UnconfirmedEmail == user.Email {
// User has already confirmed this
@ -82,7 +82,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
return nil
}
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("db error getting instance: %w", err)
}
@ -97,7 +97,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
)
// Assemble email contents and send the email.
if err := s.emailSender.SendConfirmEmail(
if err := s.EmailSender.SendConfirmEmail(
user.UnconfirmedEmail,
email.ConfirmData{
Username: user.Account.Username,
@ -116,7 +116,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
user.ConfirmationSentAt = now
user.LastEmailedAt = now
if err := s.state.DB.UpdateUser(
if err := s.State.DB.UpdateUser(
ctx,
user,
"confirmation_token",
@ -131,7 +131,7 @@ func (s *surface) emailUserPleaseConfirm(ctx context.Context, user *gtsmodel.Use
// emailUserSignupApproved emails the given user
// to inform them their sign-up has been approved.
func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.User) error {
func (s *Surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.User) error {
// User may have been approved without
// their email address being confirmed
// yet. Just send to whatever we have.
@ -140,13 +140,13 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us
emailAddr = user.UnconfirmedEmail
}
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("db error getting instance: %w", err)
}
// Assemble email contents and send the email.
if err := s.emailSender.SendSignupApprovedEmail(
if err := s.EmailSender.SendSignupApprovedEmail(
emailAddr,
email.SignupApprovedData{
Username: user.Account.Username,
@ -162,7 +162,7 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us
now := time.Now()
user.LastEmailedAt = now
if err := s.state.DB.UpdateUser(
if err := s.State.DB.UpdateUser(
ctx,
user,
"last_emailed_at",
@ -175,14 +175,14 @@ func (s *surface) emailUserSignupApproved(ctx context.Context, user *gtsmodel.Us
// emailUserSignupApproved emails the given user
// to inform them their sign-up has been approved.
func (s *surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error {
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
func (s *Surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmodel.DeniedUser) error {
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("db error getting instance: %w", err)
}
// Assemble email contents and send the email.
return s.emailSender.SendSignupRejectedEmail(
return s.EmailSender.SendSignupRejectedEmail(
deniedUser.Email,
email.SignupRejectedData{
Message: deniedUser.Message,
@ -194,13 +194,13 @@ func (s *surface) emailUserSignupRejected(ctx context.Context, deniedUser *gtsmo
// emailAdminReportOpened emails all active moderators/admins
// of this instance that a new report has been created.
func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.Report) error {
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
func (s *Surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.Report) error {
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("error getting instance: %w", err)
}
toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
toAddresses, err := s.State.DB.GetInstanceModeratorAddresses(ctx)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// No registered moderator addresses.
@ -209,7 +209,7 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R
return gtserror.Newf("error getting instance moderator addresses: %w", err)
}
if err := s.state.DB.PopulateReport(ctx, report); err != nil {
if err := s.State.DB.PopulateReport(ctx, report); err != nil {
return gtserror.Newf("error populating report: %w", err)
}
@ -221,7 +221,7 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R
ReportTargetDomain: report.TargetAccount.Domain,
}
if err := s.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
if err := s.EmailSender.SendNewReportEmail(toAddresses, reportData); err != nil {
return gtserror.Newf("error emailing instance moderators: %w", err)
}
@ -230,13 +230,13 @@ func (s *surface) emailAdminReportOpened(ctx context.Context, report *gtsmodel.R
// emailAdminNewSignup emails all active moderators/admins of this
// instance that a new account sign-up has been submitted to the instance.
func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.User) error {
instance, err := s.state.DB.GetInstance(ctx, config.GetHost())
func (s *Surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.User) error {
instance, err := s.State.DB.GetInstance(ctx, config.GetHost())
if err != nil {
return gtserror.Newf("error getting instance: %w", err)
}
toAddresses, err := s.state.DB.GetInstanceModeratorAddresses(ctx)
toAddresses, err := s.State.DB.GetInstanceModeratorAddresses(ctx)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// No registered moderator addresses.
@ -246,7 +246,7 @@ func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.Use
}
// Ensure user populated.
if err := s.state.DB.PopulateUser(ctx, newUser); err != nil {
if err := s.State.DB.PopulateUser(ctx, newUser); err != nil {
return gtserror.Newf("error populating user: %w", err)
}
@ -259,7 +259,7 @@ func (s *surface) emailAdminNewSignup(ctx context.Context, newUser *gtsmodel.Use
SignupURL: instance.URI + "/settings/admin/accounts/" + newUser.AccountID,
}
if err := s.emailSender.SendNewSignupEmail(toAddresses, newSignupData); err != nil {
if err := s.EmailSender.SendNewSignupEmail(toAddresses, newSignupData); err != nil {
return gtserror.Newf("error emailing instance moderators: %w", err)
}

View file

@ -20,18 +20,20 @@ package workers
import (
"context"
"errors"
"strings"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/id"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// notifyMentions iterates through mentions on the
// given status, and notifies each mentioned account
// that they have a new mention.
func (s *surface) notifyMentions(
func (s *Surface) notifyMentions(
ctx context.Context,
status *gtsmodel.Status,
) error {
@ -43,7 +45,7 @@ func (s *surface) notifyMentions(
mention.Status = status
// Beforehand, ensure the passed mention is fully populated.
if err := s.state.DB.PopulateMention(ctx, mention); err != nil {
if err := s.State.DB.PopulateMention(ctx, mention); err != nil {
errs.Appendf("error populating mention %s: %w", mention.ID, err)
continue
}
@ -56,7 +58,7 @@ func (s *surface) notifyMentions(
// Ensure thread not muted
// by mentioned account.
muted, err := s.state.DB.IsThreadMutedByAccount(
muted, err := s.State.DB.IsThreadMutedByAccount(
ctx,
status.ThreadID,
mention.TargetAccountID,
@ -75,7 +77,7 @@ func (s *surface) notifyMentions(
// notify mentioned
// by status author.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationMention,
mention.TargetAccount,
mention.OriginAccount,
@ -91,12 +93,12 @@ func (s *surface) notifyMentions(
// notifyFollowRequest notifies the target of the given
// follow request that they have a new follow request.
func (s *surface) notifyFollowRequest(
func (s *Surface) notifyFollowRequest(
ctx context.Context,
followReq *gtsmodel.FollowRequest,
) error {
// Beforehand, ensure the passed follow request is fully populated.
if err := s.state.DB.PopulateFollowRequest(ctx, followReq); err != nil {
if err := s.State.DB.PopulateFollowRequest(ctx, followReq); err != nil {
return gtserror.Newf("error populating follow request %s: %w", followReq.ID, err)
}
@ -107,7 +109,7 @@ func (s *surface) notifyFollowRequest(
}
// Now notify the follow request itself.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationFollowRequest,
followReq.TargetAccount,
followReq.Account,
@ -123,12 +125,12 @@ func (s *surface) notifyFollowRequest(
// they have a new follow. It will also remove any previous
// notification of a follow request, essentially replacing
// that notification.
func (s *surface) notifyFollow(
func (s *Surface) notifyFollow(
ctx context.Context,
follow *gtsmodel.Follow,
) error {
// Beforehand, ensure the passed follow is fully populated.
if err := s.state.DB.PopulateFollow(ctx, follow); err != nil {
if err := s.State.DB.PopulateFollow(ctx, follow); err != nil {
return gtserror.Newf("error populating follow %s: %w", follow.ID, err)
}
@ -139,7 +141,7 @@ func (s *surface) notifyFollow(
}
// Check if previous follow req notif exists.
prevNotif, err := s.state.DB.GetNotification(
prevNotif, err := s.State.DB.GetNotification(
gtscontext.SetBarebones(ctx),
gtsmodel.NotificationFollowRequest,
follow.TargetAccountID,
@ -152,14 +154,14 @@ func (s *surface) notifyFollow(
if prevNotif != nil {
// Previous follow request notif existed, delete it before creating new.
if err := s.state.DB.DeleteNotificationByID(ctx, prevNotif.ID); // nocollapse
if err := s.State.DB.DeleteNotificationByID(ctx, prevNotif.ID); // nocollapse
err != nil && !errors.Is(err, db.ErrNoEntries) {
return gtserror.Newf("error deleting notification %s: %w", prevNotif.ID, err)
}
}
// Now notify the follow itself.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationFollow,
follow.TargetAccount,
follow.Account,
@ -173,7 +175,7 @@ func (s *surface) notifyFollow(
// notifyFave notifies the target of the given
// fave that their status has been liked/faved.
func (s *surface) notifyFave(
func (s *Surface) notifyFave(
ctx context.Context,
fave *gtsmodel.StatusFave,
) error {
@ -183,7 +185,7 @@ func (s *surface) notifyFave(
}
// Beforehand, ensure the passed status fave is fully populated.
if err := s.state.DB.PopulateStatusFave(ctx, fave); err != nil {
if err := s.State.DB.PopulateStatusFave(ctx, fave); err != nil {
return gtserror.Newf("error populating fave %s: %w", fave.ID, err)
}
@ -195,7 +197,7 @@ func (s *surface) notifyFave(
// Ensure favee hasn't
// muted the thread.
muted, err := s.state.DB.IsThreadMutedByAccount(
muted, err := s.State.DB.IsThreadMutedByAccount(
ctx,
fave.Status.ThreadID,
fave.TargetAccountID,
@ -212,7 +214,7 @@ func (s *surface) notifyFave(
// notify status author
// of fave by account.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationFave,
fave.TargetAccount,
fave.Account,
@ -226,7 +228,7 @@ func (s *surface) notifyFave(
// notifyAnnounce notifies the status boost target
// account that their status has been boosted.
func (s *surface) notifyAnnounce(
func (s *Surface) notifyAnnounce(
ctx context.Context,
status *gtsmodel.Status,
) error {
@ -241,7 +243,7 @@ func (s *surface) notifyAnnounce(
}
// Beforehand, ensure the passed status is fully populated.
if err := s.state.DB.PopulateStatus(ctx, status); err != nil {
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status %s: %w", status.ID, err)
}
@ -253,7 +255,7 @@ func (s *surface) notifyAnnounce(
// Ensure boostee hasn't
// muted the thread.
muted, err := s.state.DB.IsThreadMutedByAccount(
muted, err := s.State.DB.IsThreadMutedByAccount(
ctx,
status.BoostOf.ThreadID,
status.BoostOfAccountID,
@ -271,7 +273,7 @@ func (s *surface) notifyAnnounce(
// notify status author
// of boost by account.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationReblog,
status.BoostOfAccount,
status.Account,
@ -283,14 +285,14 @@ func (s *surface) notifyAnnounce(
return nil
}
func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error {
func (s *Surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status) error {
// Beforehand, ensure the passed status is fully populated.
if err := s.state.DB.PopulateStatus(ctx, status); err != nil {
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status %s: %w", status.ID, err)
}
// Fetch all votes in the attached status poll.
votes, err := s.state.DB.GetPollVotes(ctx, status.PollID)
votes, err := s.State.DB.GetPollVotes(ctx, status.PollID)
if err != nil {
return gtserror.Newf("error getting poll %s votes: %w", status.PollID, err)
}
@ -300,7 +302,7 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
if status.Account.IsLocal() {
// Send a notification to the status
// author that their poll has closed!
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationPoll,
status.Account,
status.Account,
@ -319,7 +321,7 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
// notify voter that
// poll has been closed.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationPoll,
vote.Account,
status.Account,
@ -333,8 +335,8 @@ func (s *surface) notifyPollClose(ctx context.Context, status *gtsmodel.Status)
return errs.Combine()
}
func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) error {
modAccounts, err := s.state.DB.GetInstanceModerators(ctx)
func (s *Surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) error {
modAccounts, err := s.State.DB.GetInstanceModerators(ctx)
if err != nil {
if errors.Is(err, db.ErrNoEntries) {
// No registered
@ -347,18 +349,18 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
}
// Ensure user + account populated.
if err := s.state.DB.PopulateUser(ctx, newUser); err != nil {
if err := s.State.DB.PopulateUser(ctx, newUser); err != nil {
return gtserror.Newf("db error populating new user: %w", err)
}
if err := s.state.DB.PopulateAccount(ctx, newUser.Account); err != nil {
if err := s.State.DB.PopulateAccount(ctx, newUser.Account); err != nil {
return gtserror.Newf("db error populating new user's account: %w", err)
}
// Notify each moderator.
var errs gtserror.MultiError
for _, mod := range modAccounts {
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationSignup,
mod,
newUser.Account,
@ -372,7 +374,24 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
return errs.Combine()
}
// notify creates, inserts, and streams a new
func getNotifyLockURI(
notificationType gtsmodel.NotificationType,
targetAccount *gtsmodel.Account,
originAccount *gtsmodel.Account,
statusID string,
) string {
builder := strings.Builder{}
builder.WriteString("notification:?")
builder.WriteString("type=" + string(notificationType))
builder.WriteString("&target=" + targetAccount.URI)
builder.WriteString("&origin=" + originAccount.URI)
if statusID != "" {
builder.WriteString("&statusID=" + statusID)
}
return builder.String()
}
// Notify creates, inserts, and streams a new
// notification to the target account if it
// doesn't yet exist with the given parameters.
//
@ -383,7 +402,7 @@ func (s *surface) notifySignup(ctx context.Context, newUser *gtsmodel.User) erro
//
// targetAccount and originAccount must be
// set, but statusID can be an empty string.
func (s *surface) notify(
func (s *Surface) Notify(
ctx context.Context,
notificationType gtsmodel.NotificationType,
targetAccount *gtsmodel.Account,
@ -395,9 +414,24 @@ func (s *surface) notify(
return nil
}
// We're doing state-y stuff so get a
// lock on this combo of notif params.
lockURI := getNotifyLockURI(
notificationType,
targetAccount,
originAccount,
statusID,
)
unlock := s.State.ProcessingLocks.Lock(lockURI)
// Wrap the unlock so we
// can do granular unlocking.
unlock = util.DoOnce(unlock)
defer unlock()
// Make sure a notification doesn't
// already exist with these params.
if _, err := s.state.DB.GetNotification(
if _, err := s.State.DB.GetNotification(
gtscontext.SetBarebones(ctx),
notificationType,
targetAccount.ID,
@ -424,21 +458,25 @@ func (s *surface) notify(
StatusID: statusID,
}
if err := s.state.DB.PutNotification(ctx, notif); err != nil {
if err := s.State.DB.PutNotification(ctx, notif); err != nil {
return gtserror.Newf("error putting notification in database: %w", err)
}
filters, err := s.state.DB.GetFiltersForAccountID(ctx, targetAccount.ID)
// Unlock already, we're done
// with the state-y stuff.
unlock()
// Stream notification to the user.
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)
}
// Stream notification to the user.
apiNotif, err := s.converter.NotificationToAPINotification(ctx, notif, filters)
apiNotif, err := s.Converter.NotificationToAPINotification(ctx, notif, filters)
if err != nil {
return gtserror.Newf("error converting notification to api representation: %w", err)
}
s.stream.Notify(ctx, targetAccount, apiNotif)
s.Stream.Notify(ctx, targetAccount, apiNotif)
return nil
}

View file

@ -0,0 +1,115 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package workers_test
import (
"context"
"sync"
"testing"
"time"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtscontext"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/processing/workers"
)
type SurfaceNotifyTestSuite struct {
WorkersTestSuite
}
func (suite *SurfaceNotifyTestSuite) TestSpamNotifs() {
testStructs := suite.SetupTestStructs()
defer suite.TearDownTestStructs(testStructs)
surface := &workers.Surface{
State: testStructs.State,
Converter: testStructs.TypeConverter,
Stream: testStructs.Processor.Stream(),
Filter: visibility.NewFilter(testStructs.State),
EmailSender: testStructs.EmailSender,
}
var (
ctx = context.Background()
notificationType = gtsmodel.NotificationFollow
targetAccount = suite.testAccounts["local_account_1"]
originAccount = suite.testAccounts["local_account_2"]
)
// Set up a bunch of goroutines to surface
// a notification at exactly the same time.
wg := sync.WaitGroup{}
wg.Add(20)
startAt := time.Now().Add(2 * time.Second)
for i := 0; i < 20; i++ {
go func() {
defer wg.Done()
// Wait for it....
untilTick := time.Until(startAt)
<-time.Tick(untilTick)
// ...Go!
if err := surface.Notify(ctx,
notificationType,
targetAccount,
originAccount,
"",
); err != nil {
suite.FailNow(err.Error())
}
}()
}
// Wait for all notif creation
// attempts to complete.
wg.Wait()
// Get all notifs for this account.
notifs, err := testStructs.State.DB.GetAccountNotifications(
gtscontext.SetBarebones(ctx),
targetAccount.ID,
"", "", "", 0, nil,
)
if err != nil {
suite.FailNow(err.Error())
}
var gotOne bool
for _, notif := range notifs {
if notif.NotificationType == notificationType &&
notif.TargetAccountID == targetAccount.ID &&
notif.OriginAccountID == originAccount.ID {
// This is the notif...
if gotOne {
// We already had
// the notif, d'oh!
suite.FailNow("already had notif")
} else {
gotOne = true
}
}
}
}
func TestSurfaceNotifyTestSuite(t *testing.T) {
suite.Run(t, new(SurfaceNotifyTestSuite))
}

View file

@ -37,14 +37,14 @@ import (
// It will also handle notifications for any mentions attached to
// the account, and notifications for any local accounts that want
// to know when this account posts.
func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
func (s *Surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.Status) error {
// Ensure status fully populated; including account, mentions, etc.
if err := s.state.DB.PopulateStatus(ctx, status); err != nil {
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
}
// Get all local followers of the account that posted the status.
follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID)
if err != nil {
return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err)
}
@ -80,7 +80,7 @@ func (s *surface) timelineAndNotifyStatus(ctx context.Context, status *gtsmodel.
// adding the status to list timelines + home timelines of each
// follower, as appropriate, and notifying each follower of the
// new status, if the status is eligible for notification.
func (s *surface) timelineAndNotifyStatusForFollowers(
func (s *Surface) timelineAndNotifyStatusForFollowers(
ctx context.Context,
status *gtsmodel.Status,
follows []*gtsmodel.Follow,
@ -99,7 +99,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers(
// If it's not timelineable, we can just stop early, since lists
// are prettymuch subsets of the home timeline, so if it shouldn't
// appear there, it shouldn't appear in lists either.
timelineable, err := s.filter.StatusHomeTimelineable(
timelineable, err := s.Filter.StatusHomeTimelineable(
ctx, follow.Account, status,
)
if err != nil {
@ -112,7 +112,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers(
continue
}
filters, err := s.state.DB.GetFiltersForAccountID(ctx, follow.AccountID)
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)
}
@ -131,7 +131,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers(
// of this follow, if applicable.
homeTimelined, err := s.timelineStatus(
ctx,
s.state.Timelines.Home.IngestOne,
s.State.Timelines.Home.IngestOne,
follow.AccountID, // home timelines are keyed by account ID
follow.Account,
status,
@ -168,7 +168,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers(
// - This is a top-level post (not a reply or boost).
//
// That means we can officially notify this one.
if err := s.notify(ctx,
if err := s.Notify(ctx,
gtsmodel.NotificationStatus,
follow.Account,
status.Account,
@ -183,7 +183,7 @@ func (s *surface) timelineAndNotifyStatusForFollowers(
// listTimelineStatusForFollow puts the given status
// in any eligible lists owned by the given follower.
func (s *surface) listTimelineStatusForFollow(
func (s *Surface) listTimelineStatusForFollow(
ctx context.Context,
status *gtsmodel.Status,
follow *gtsmodel.Follow,
@ -198,7 +198,7 @@ func (s *surface) listTimelineStatusForFollow(
// inclusion in the list.
// Get every list entry that targets this follow's ID.
listEntries, err := s.state.DB.GetListEntriesForFollowID(
listEntries, err := s.State.DB.GetListEntriesForFollowID(
// We only need the list IDs.
gtscontext.SetBarebones(ctx),
follow.ID,
@ -226,7 +226,7 @@ func (s *surface) listTimelineStatusForFollow(
// list that this list entry belongs to.
if _, err := s.timelineStatus(
ctx,
s.state.Timelines.List.IngestOne,
s.State.Timelines.List.IngestOne,
listEntry.ListID, // list timelines are keyed by list ID
follow.Account,
status,
@ -242,7 +242,7 @@ func (s *surface) listTimelineStatusForFollow(
// listEligible checks if the given status is eligible
// for inclusion in the list that that the given listEntry
// belongs to, based on the replies policy of the list.
func (s *surface) listEligible(
func (s *Surface) listEligible(
ctx context.Context,
listEntry *gtsmodel.ListEntry,
status *gtsmodel.Status,
@ -263,7 +263,7 @@ func (s *surface) listEligible(
// We need to fetch the list that this
// entry belongs to, in order to check
// the list's replies policy.
list, err := s.state.DB.GetListByID(
list, err := s.State.DB.GetListByID(
ctx, listEntry.ListID,
)
if err != nil {
@ -283,7 +283,7 @@ func (s *surface) listEligible(
//
// Check if replied-to account is
// also included in this list.
includes, err := s.state.DB.ListIncludesAccount(
includes, err := s.State.DB.ListIncludesAccount(
ctx,
list.ID,
status.InReplyToAccountID,
@ -305,7 +305,7 @@ func (s *surface) listEligible(
//
// Check if replied-to account is
// followed by list owner account.
follows, err := s.state.DB.IsFollowing(
follows, err := s.State.DB.IsFollowing(
ctx,
list.AccountID,
status.InReplyToAccountID,
@ -335,7 +335,7 @@ func (s *surface) listEligible(
//
// If the status was inserted into the timeline, true will be returned
// + it will also be streamed to the user using the given streamType.
func (s *surface) timelineStatus(
func (s *Surface) timelineStatus(
ctx context.Context,
ingest func(context.Context, string, timeline.Timelineable) (bool, error),
timelineID string,
@ -354,26 +354,31 @@ func (s *surface) timelineStatus(
}
// The status was inserted so stream it to the user.
apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account, custom.FilterContextHome, filters)
apiStatus, err := s.Converter.StatusToAPIStatus(ctx,
status,
account,
custom.FilterContextHome,
filters,
)
if err != nil {
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
return true, err
}
s.stream.Update(ctx, account, apiStatus, streamType)
s.Stream.Update(ctx, account, apiStatus, streamType)
return true, nil
}
// deleteStatusFromTimelines completely removes the given status from all timelines.
// It will also stream deletion of the status to all open streams.
func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
if err := s.state.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
func (s *Surface) deleteStatusFromTimelines(ctx context.Context, statusID string) error {
if err := s.State.Timelines.Home.WipeItemFromAllTimelines(ctx, statusID); err != nil {
return err
}
if err := s.state.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
if err := s.State.Timelines.List.WipeItemFromAllTimelines(ctx, statusID); err != nil {
return err
}
s.stream.Delete(ctx, statusID)
s.Stream.Delete(ctx, statusID)
return nil
}
@ -381,15 +386,15 @@ func (s *surface) deleteStatusFromTimelines(ctx context.Context, statusID string
// unpreparing it from all timelines, forcing it to be prepared again (with updated
// stats, boost counts, etc) next time it's fetched by the timeline owner. This goes
// both for the status itself, and for any boosts of the status.
func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
if err := s.state.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
func (s *Surface) invalidateStatusFromTimelines(ctx context.Context, statusID string) {
if err := s.State.Timelines.Home.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
log.
WithContext(ctx).
WithField("statusID", statusID).
Errorf("error unpreparing status from home timelines: %v", err)
}
if err := s.state.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
if err := s.State.Timelines.List.UnprepareItemFromAllTimelines(ctx, statusID); err != nil {
log.
WithContext(ctx).
WithField("statusID", statusID).
@ -403,14 +408,14 @@ func (s *surface) invalidateStatusFromTimelines(ctx context.Context, statusID st
// Note that calling invalidateStatusFromTimelines takes care of the
// state in general, we just need to do this for any streams that are
// open right now.
func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error {
func (s *Surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Status) error {
// Ensure status fully populated; including account, mentions, etc.
if err := s.state.DB.PopulateStatus(ctx, status); err != nil {
if err := s.State.DB.PopulateStatus(ctx, status); err != nil {
return gtserror.Newf("error populating status with id %s: %w", status.ID, err)
}
// Get all local followers of the account that posted the status.
follows, err := s.state.DB.GetAccountLocalFollowers(ctx, status.AccountID)
follows, err := s.State.DB.GetAccountLocalFollowers(ctx, status.AccountID)
if err != nil {
return gtserror.Newf("error getting local followers of account %s: %w", status.AccountID, err)
}
@ -438,7 +443,7 @@ func (s *surface) timelineStatusUpdate(ctx context.Context, status *gtsmodel.Sta
// slice of followers of the account that posted the given status,
// pushing update messages into open list/home streams of each
// follower.
func (s *surface) timelineStatusUpdateForFollowers(
func (s *Surface) timelineStatusUpdateForFollowers(
ctx context.Context,
status *gtsmodel.Status,
follows []*gtsmodel.Follow,
@ -455,7 +460,7 @@ func (s *surface) timelineStatusUpdateForFollowers(
// If it's not timelineable, we can just stop early, since lists
// are prettymuch subsets of the home timeline, so if it shouldn't
// appear there, it shouldn't appear in lists either.
timelineable, err := s.filter.StatusHomeTimelineable(
timelineable, err := s.Filter.StatusHomeTimelineable(
ctx, follow.Account, status,
)
if err != nil {
@ -468,7 +473,7 @@ func (s *surface) timelineStatusUpdateForFollowers(
continue
}
filters, err := s.state.DB.GetFiltersForAccountID(ctx, follow.AccountID)
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)
}
@ -503,7 +508,7 @@ func (s *surface) timelineStatusUpdateForFollowers(
// listTimelineStatusUpdateForFollow pushes edits of the given status
// into any eligible lists streams opened by the given follower.
func (s *surface) listTimelineStatusUpdateForFollow(
func (s *Surface) listTimelineStatusUpdateForFollow(
ctx context.Context,
status *gtsmodel.Status,
follow *gtsmodel.Follow,
@ -518,7 +523,7 @@ func (s *surface) listTimelineStatusUpdateForFollow(
// inclusion in the list.
// Get every list entry that targets this follow's ID.
listEntries, err := s.state.DB.GetListEntriesForFollowID(
listEntries, err := s.State.DB.GetListEntriesForFollowID(
// We only need the list IDs.
gtscontext.SetBarebones(ctx),
follow.ID,
@ -559,14 +564,14 @@ func (s *surface) listTimelineStatusUpdateForFollow(
// timelineStatusUpdate streams the edited status to the user using the
// given streamType.
func (s *surface) timelineStreamStatusUpdate(
func (s *Surface) timelineStreamStatusUpdate(
ctx context.Context,
account *gtsmodel.Account,
status *gtsmodel.Status,
streamType string,
filters []*gtsmodel.Filter,
) error {
apiStatus, err := s.converter.StatusToAPIStatus(ctx, status, account, custom.FilterContextHome, filters)
apiStatus, err := s.Converter.StatusToAPIStatus(ctx, status, account, custom.FilterContextHome, filters)
if errors.Is(err, custom.ErrHideStatus) {
// Don't put this status in the stream.
return nil
@ -575,6 +580,6 @@ func (s *surface) timelineStreamStatusUpdate(
err = gtserror.Newf("error converting status %s to frontend representation: %w", status.ID, err)
return err
}
s.stream.StatusUpdate(ctx, account, apiStatus, streamType)
s.Stream.StatusUpdate(ctx, account, apiStatus, streamType)
return nil
}

View file

@ -32,20 +32,20 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/state"
)
// utilF wraps util functions used by both
// util provides util functions used by both
// the fromClientAPI and fromFediAPI functions.
type utilF struct {
type utils struct {
state *state.State
media *media.Processor
account *account.Processor
surface *surface
surface *Surface
}
// wipeStatus encapsulates common logic
// used to totally delete a status + all
// its attachments, notifications, boosts,
// and timeline entries.
func (u *utilF) wipeStatus(
func (u *utils) wipeStatus(
ctx context.Context,
statusToDelete *gtsmodel.Status,
deleteAttachments bool,
@ -152,7 +152,7 @@ func (u *utilF) wipeStatus(
// already, and the Move must be valid.
//
// Return bool will be true if all goes OK.
func (u *utilF) redirectFollowers(
func (u *utils) redirectFollowers(
ctx context.Context,
originAcct *gtsmodel.Account,
targetAcct *gtsmodel.Account,
@ -239,13 +239,13 @@ func (u *utilF) redirectFollowers(
return true
}
func (u *utilF) incrementStatusesCount(
func (u *utils) incrementStatusesCount(
ctx context.Context,
account *gtsmodel.Account,
status *gtsmodel.Status,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -271,12 +271,12 @@ func (u *utilF) incrementStatusesCount(
return nil
}
func (u *utilF) decrementStatusesCount(
func (u *utils) decrementStatusesCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -305,12 +305,12 @@ func (u *utilF) decrementStatusesCount(
return nil
}
func (u *utilF) incrementFollowersCount(
func (u *utils) incrementFollowersCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -334,12 +334,12 @@ func (u *utilF) incrementFollowersCount(
return nil
}
func (u *utilF) decrementFollowersCount(
func (u *utils) decrementFollowersCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -368,12 +368,12 @@ func (u *utilF) decrementFollowersCount(
return nil
}
func (u *utilF) incrementFollowingCount(
func (u *utils) incrementFollowingCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -397,12 +397,12 @@ func (u *utilF) incrementFollowingCount(
return nil
}
func (u *utilF) decrementFollowingCount(
func (u *utils) decrementFollowingCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -431,12 +431,12 @@ func (u *utilF) decrementFollowingCount(
return nil
}
func (u *utilF) incrementFollowRequestsCount(
func (u *utils) incrementFollowRequestsCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.
@ -460,12 +460,12 @@ func (u *utilF) incrementFollowRequestsCount(
return nil
}
func (u *utilF) decrementFollowRequestsCount(
func (u *utils) decrementFollowRequestsCount(
ctx context.Context,
account *gtsmodel.Account,
) error {
// Lock on this account since we're changing stats.
unlock := u.state.AccountLocks.Lock(account.URI)
unlock := u.state.ProcessingLocks.Lock(account.URI)
defer unlock()
// Populate stats.

View file

@ -30,9 +30,9 @@ import (
)
type Processor struct {
clientAPI clientAPI
fediAPI fediAPI
workers *workers.Workers
clientAPI *clientAPI
fediAPI *fediAPI
}
func New(
@ -45,16 +45,6 @@ func New(
media *media.Processor,
stream *stream.Processor,
) Processor {
// Init surface logic
// wrapper struct.
surface := &surface{
state: state,
converter: converter,
stream: stream,
filter: filter,
emailSender: emailSender,
}
// Init federate logic
// wrapper struct.
federate := &federate{
@ -63,8 +53,18 @@ func New(
converter: converter,
}
// Init surface logic
// wrapper struct.
surface := &Surface{
State: state,
Converter: converter,
Stream: stream,
Filter: filter,
EmailSender: emailSender,
}
// Init shared util funcs.
utilF := &utilF{
utils := &utils{
state: state,
media: media,
account: account,
@ -73,20 +73,20 @@ func New(
return Processor{
workers: &state.Workers,
clientAPI: &clientAPI{
clientAPI: clientAPI{
state: state,
converter: converter,
surface: surface,
federate: federate,
account: account,
utilF: utilF,
utils: utils,
},
fediAPI: &fediAPI{
fediAPI: fediAPI{
state: state,
surface: surface,
federate: federate,
account: account,
utilF: utilF,
utils: utils,
},
}
}

View file

@ -22,18 +22,13 @@ import (
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/cleaner"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/email"
"github.com/superseriousbusiness/gotosocial/internal/federation"
"github.com/superseriousbusiness/gotosocial/internal/filter/visibility"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/media"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/gotosocial/internal/processing"
"github.com/superseriousbusiness/gotosocial/internal/state"
"github.com/superseriousbusiness/gotosocial/internal/storage"
"github.com/superseriousbusiness/gotosocial/internal/stream"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/testrig"
)
@ -41,16 +36,6 @@ import (
type WorkersTestSuite struct {
// standard suite interfaces
suite.Suite
db db.DB
storage *storage.Driver
state state.State
mediaManager *media.Manager
typeconverter *typeutils.Converter
httpClient *testrig.MockHTTPClient
transportController transport.Controller
federator *federation.Federator
oauthServer oauth.Server
emailSender email.Sender
// standard suite models
testTokens map[string]*gtsmodel.Token
@ -68,8 +53,23 @@ type WorkersTestSuite struct {
testActivities map[string]testrig.ActivityWithSignature
testLists map[string]*gtsmodel.List
testListEntries map[string]*gtsmodel.ListEntry
}
processor *processing.Processor
// TestStructs encapsulates structs needed to
// run one test in this package. Each test should
// call SetupTestStructs to get a new TestStructs,
// and defer TearDownTestStructs to close it when
// the test is complete. The reason for doing things
// this way here is to prevent the tests in this
// package from overwriting one another's processors
// and worker queues, which was causing issues
// when running all tests at once.
type TestStructs struct {
State *state.State
Processor *processing.Processor
HTTPClient *testrig.MockHTTPClient
TypeConverter *typeutils.Converter
EmailSender email.Sender
}
func (suite *WorkersTestSuite) SetupSuite() {
@ -96,51 +96,12 @@ func (suite *WorkersTestSuite) SetupSuite() {
}
func (suite *WorkersTestSuite) SetupTest() {
suite.state.Caches.Init()
testrig.InitTestConfig()
testrig.InitTestLog()
suite.db = testrig.NewTestDB(&suite.state)
suite.state.DB = suite.db
suite.testActivities = testrig.NewTestActivities(suite.testAccounts)
suite.storage = testrig.NewInMemoryStorage()
suite.state.Storage = suite.storage
suite.typeconverter = typeutils.NewConverter(&suite.state)
testrig.StartTimelines(
&suite.state,
visibility.NewFilter(&suite.state),
suite.typeconverter,
)
suite.httpClient = testrig.NewMockHTTPClient(nil, "../../../testrig/media")
suite.httpClient.TestRemotePeople = testrig.NewTestFediPeople()
suite.httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
suite.transportController = testrig.NewTestTransportController(&suite.state, suite.httpClient)
suite.mediaManager = testrig.NewTestMediaManager(&suite.state)
suite.federator = testrig.NewTestFederator(&suite.state, suite.transportController, suite.mediaManager)
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
suite.emailSender = testrig.NewEmailSender("../../../web/template/", nil)
suite.processor = processing.NewProcessor(cleaner.New(&suite.state), suite.typeconverter, suite.federator, suite.oauthServer, suite.mediaManager, &suite.state, suite.emailSender)
testrig.StartWorkers(&suite.state, suite.processor.Workers())
suite.state.Workers.EnqueueClientAPI = suite.processor.Workers().EnqueueClientAPI
suite.state.Workers.EnqueueFediAPI = suite.processor.Workers().EnqueueFediAPI
testrig.StandardDBSetup(suite.db, suite.testAccounts)
testrig.StandardStorageSetup(suite.storage, "../../../testrig/media")
}
func (suite *WorkersTestSuite) TearDownTest() {
testrig.StandardDBTeardown(suite.db)
testrig.StandardStorageTeardown(suite.storage)
testrig.StopWorkers(&suite.state)
}
func (suite *WorkersTestSuite) openStreams(ctx context.Context, account *gtsmodel.Account, listIDs []string) map[string]*stream.Stream {
func (suite *WorkersTestSuite) openStreams(ctx context.Context, processor *processing.Processor, account *gtsmodel.Account, listIDs []string) map[string]*stream.Stream {
streams := make(map[string]*stream.Stream)
for _, streamType := range []string{
@ -148,7 +109,7 @@ func (suite *WorkersTestSuite) openStreams(ctx context.Context, account *gtsmode
stream.TimelinePublic,
stream.TimelineNotifications,
} {
stream, err := suite.processor.Stream().Open(ctx, account, streamType)
stream, err := processor.Stream().Open(ctx, account, streamType)
if err != nil {
suite.FailNow(err.Error())
}
@ -159,7 +120,7 @@ func (suite *WorkersTestSuite) openStreams(ctx context.Context, account *gtsmode
for _, listID := range listIDs {
streamType := stream.TimelineList + ":" + listID
stream, err := suite.processor.Stream().Open(ctx, account, streamType)
stream, err := processor.Stream().Open(ctx, account, streamType)
if err != nil {
suite.FailNow(err.Error())
}
@ -169,3 +130,52 @@ func (suite *WorkersTestSuite) openStreams(ctx context.Context, account *gtsmode
return streams
}
func (suite *WorkersTestSuite) SetupTestStructs() *TestStructs {
state := state.State{}
state.Caches.Init()
db := testrig.NewTestDB(&state)
state.DB = db
storage := testrig.NewInMemoryStorage()
state.Storage = storage
typeconverter := typeutils.NewConverter(&state)
testrig.StartTimelines(
&state,
visibility.NewFilter(&state),
typeconverter,
)
httpClient := testrig.NewMockHTTPClient(nil, "../../../testrig/media")
httpClient.TestRemotePeople = testrig.NewTestFediPeople()
httpClient.TestRemoteStatuses = testrig.NewTestFediStatuses()
transportController := testrig.NewTestTransportController(&state, httpClient)
mediaManager := testrig.NewTestMediaManager(&state)
federator := testrig.NewTestFederator(&state, transportController, mediaManager)
oauthServer := testrig.NewTestOauthServer(db)
emailSender := testrig.NewEmailSender("../../../web/template/", nil)
processor := processing.NewProcessor(cleaner.New(&state), typeconverter, federator, oauthServer, mediaManager, &state, emailSender)
testrig.StartWorkers(&state, processor.Workers())
testrig.StandardDBSetup(db, suite.testAccounts)
testrig.StandardStorageSetup(storage, "../../../testrig/media")
return &TestStructs{
State: &state,
Processor: processor,
HTTPClient: httpClient,
TypeConverter: typeconverter,
EmailSender: emailSender,
}
}
func (suite *WorkersTestSuite) TearDownTestStructs(testStructs *TestStructs) {
testrig.StandardDBTeardown(testStructs.State.DB)
testrig.StandardStorageTeardown(testStructs.State.Storage)
testrig.StopWorkers(testStructs.State)
}

201
internal/queue/simple.go Normal file
View file

@ -0,0 +1,201 @@
// 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 queue
import (
"context"
"sync"
"codeberg.org/gruf/go-list"
)
// frequency of GC cycles
// per no. unlocks. i.e.
// every 'gcfreq' unlocks.
const gcfreq = 1024
// SimpleQueue provides a simple concurrency safe
// queue using generics and a memory pool of list
// elements to reduce overall memory usage.
type SimpleQueue[T any] struct {
l list.List[T]
p elemPool[T]
w chan struct{}
m sync.Mutex
n uint32 // pop counter (safely wraps around)
}
// Push will push given value to the queue.
func (q *SimpleQueue[T]) Push(value T) {
q.m.Lock()
// Wrap in element.
elem := q.p.alloc()
elem.Value = value
// Push new elem to queue.
q.l.PushElemFront(elem)
if q.w != nil {
// Notify any goroutines
// blocking on q.Wait(),
// or on PopCtx(...).
close(q.w)
q.w = nil
}
q.m.Unlock()
}
// Pop will attempt to pop value from the queue.
func (q *SimpleQueue[T]) Pop() (value T, ok bool) {
q.m.Lock()
// Check for a tail (i.e. not empty).
if ok = (q.l.Tail != nil); ok {
// Extract value.
tail := q.l.Tail
value = tail.Value
// Remove tail.
q.l.Remove(tail)
q.p.free(tail)
// Every 'gcfreq' pops perform
// a garbage collection to keep
// us squeaky clean :]
if q.n++; q.n%gcfreq == 0 {
q.p.GC()
}
}
q.m.Unlock()
return
}
// PopCtx will attempt to pop value from queue, else blocking on context.
func (q *SimpleQueue[T]) PopCtx(ctx context.Context) (value T, ok bool) {
// Acquire lock.
q.m.Lock()
var elem *list.Elem[T]
for {
// Get next elem.
elem = q.l.Tail
if ok = (elem != nil); ok {
break
}
if q.w == nil {
// Create new wait channel.
q.w = make(chan struct{})
}
// Get current
// ch pointer.
ch := q.w
// Done with lock.
q.m.Unlock()
select {
// Context canceled.
case <-ctx.Done():
return
// Pushed!
case <-ch:
}
// Relock queue.
q.m.Lock()
}
// Extract value.
value = elem.Value
// Remove element.
q.l.Remove(elem)
q.p.free(elem)
// Every 'gcfreq' pops perform
// a garbage collection to keep
// us squeaky clean :]
if q.n++; q.n%gcfreq == 0 {
q.p.GC()
}
// Done with lock.
q.m.Unlock()
return
}
// Len returns the current length of the queue.
func (q *SimpleQueue[T]) Len() int {
q.m.Lock()
l := q.l.Len()
q.m.Unlock()
return l
}
// elemPool is a very simple
// list.Elem[T] memory pool.
type elemPool[T any] struct {
current []*list.Elem[T]
victim []*list.Elem[T]
}
func (p *elemPool[T]) alloc() *list.Elem[T] {
// First try the current queue
if l := len(p.current) - 1; l >= 0 {
mu := p.current[l]
p.current = p.current[:l]
return mu
}
// Next try the victim queue.
if l := len(p.victim) - 1; l >= 0 {
mu := p.victim[l]
p.victim = p.victim[:l]
return mu
}
// Lastly, alloc new.
mu := new(list.Elem[T])
return mu
}
// free will release given element to pool.
func (p *elemPool[T]) free(elem *list.Elem[T]) {
var zero T
elem.Next = nil
elem.Prev = nil
elem.Value = zero
p.current = append(p.current, elem)
}
// GC will clear out unused entries from the elemPool.
func (p *elemPool[T]) GC() {
current := p.current
p.current = nil
p.victim = current
}

View file

@ -18,7 +18,7 @@
package queue
import (
"sync/atomic"
"context"
"codeberg.org/gruf/go-structr"
)
@ -26,15 +26,14 @@ import (
// StructQueue wraps a structr.Queue{} to
// provide simple index caching by name.
type StructQueue[StructType any] struct {
queue structr.Queue[StructType]
queue structr.QueueCtx[StructType]
index map[string]*structr.Index
wait atomic.Pointer[chan struct{}]
}
// Init initializes queue with structr.QueueConfig{}.
func (q *StructQueue[T]) Init(config structr.QueueConfig[T]) {
q.index = make(map[string]*structr.Index, len(config.Indices))
q.queue = structr.Queue[T]{}
// q.queue = structr.QueueCtx[T]{}
q.queue.Init(config)
for _, cfg := range config.Indices {
q.index[cfg.Fields] = q.queue.Index(cfg.Fields)
@ -43,13 +42,22 @@ func (q *StructQueue[T]) Init(config structr.QueueConfig[T]) {
// Pop: see structr.Queue{}.PopFront().
func (q *StructQueue[T]) Pop() (value T, ok bool) {
return q.queue.PopFront()
values := q.queue.PopFrontN(1)
if ok = (len(values) > 0); !ok {
return
}
value = values[0]
return
}
// Push wraps structr.Queue{}.PushBack() to awaken those blocking on <-.Wait().
// PopCtx: see structr.QueueCtx{}.PopFront().
func (q *StructQueue[T]) PopCtx(ctx context.Context) (value T, ok bool) {
return q.queue.PopFront(ctx)
}
// Push: see structr.Queue.PushBack().
func (q *StructQueue[T]) Push(values ...T) {
q.queue.PushBack(values...)
q.broadcast()
}
// Delete pops (and drops!) all queued entries under index with key.
@ -66,31 +74,5 @@ func (q *StructQueue[T]) Len() int {
// Wait returns current wait channel, which may be
// blocked on to awaken when new value pushed to queue.
func (q *StructQueue[T]) Wait() <-chan struct{} {
var ch chan struct{}
for {
// Get channel ptr.
ptr := q.wait.Load()
if ptr != nil {
return *ptr
}
if ch == nil {
// Allocate new channel.
ch = make(chan struct{})
}
// Try set the new wait channel ptr.
if q.wait.CompareAndSwap(ptr, &ch) {
return ch
}
}
}
// broadcast safely closes wait channel if
// currently set, releasing waiting goroutines.
func (q *StructQueue[T]) broadcast() {
if ptr := q.wait.Swap(nil); ptr != nil {
close(*ptr)
}
return q.queue.Wait()
}

View file

@ -42,20 +42,21 @@ type State struct {
// DB provides access to the database.
DB db.DB
// FedLocks provides access to this state's
// mutex map of per URI federation locks.
// FedLocks provides access to this state's mutex
// map of per URI federation locks, intended for
// use in internal/federation functions.
//
// Used during account and status dereferencing,
// message processing in the FromFediAPI worker
// functions, and by the go-fed/activity library.
// and by the go-fed/activity library.
FedLocks mutexes.MutexMap
// AccountLocks provides access to this state's
// ProcessingLocks provides access to this state's
// mutex map of per URI locks, intended for use
// in internal/processing functions, for example
// when updating accounts, migrating, approving
// or rejecting an account, changing stats,
// pinned statuses, etc.
AccountLocks mutexes.MutexMap
// or rejecting an account, changing stats or
// pinned statuses, creating notifs, etc.
ProcessingLocks mutexes.MutexMap
// Storage provides access to the storage driver.
Storage *storage.Driver

View file

@ -28,6 +28,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/httpclient"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/queue"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// Delivery wraps an httpclient.Request{}
@ -99,29 +100,51 @@ func (p *WorkerPool) Init(client *httpclient.Client) {
}
// Start will attempt to start 'n' Worker{}s.
func (p *WorkerPool) Start(n int) (ok bool) {
if ok = (len(p.workers) == 0); ok {
p.workers = make([]*Worker, n)
for i := range p.workers {
p.workers[i] = new(Worker)
p.workers[i].Client = p.Client
p.workers[i].Queue = &p.Queue
ok = p.workers[i].Start() && ok
}
func (p *WorkerPool) Start(n int) {
// Check whether workers are
// set (is already running).
ok := (len(p.workers) > 0)
if ok {
return
}
// Allocate new workers slice.
p.workers = make([]*Worker, n)
for i := range p.workers {
// Allocate new Worker{}.
p.workers[i] = new(Worker)
p.workers[i].Client = p.Client
p.workers[i].Queue = &p.Queue
// Attempt to start worker.
// Return bool not useful
// here, as true = started,
// false = already running.
_ = p.workers[i].Start()
}
return
}
// Stop will attempt to stop contained Worker{}s.
func (p *WorkerPool) Stop() (ok bool) {
if ok = (len(p.workers) > 0); ok {
for i := range p.workers {
ok = p.workers[i].Stop() && ok
p.workers[i] = nil
}
p.workers = p.workers[:0]
func (p *WorkerPool) Stop() {
// Check whether workers are
// set (is currently running).
ok := (len(p.workers) == 0)
if ok {
return
}
return
// Stop all running workers.
for i := range p.workers {
// return bool not useful
// here, as true = stopped,
// false = never running.
_ = p.workers[i].Stop()
}
// Unset workers slice.
p.workers = p.workers[:0]
}
// Worker wraps an httpclient.Client{} to feed
@ -158,23 +181,13 @@ func (w *Worker) run(ctx context.Context) {
if w.Client == nil || w.Queue == nil {
panic("not yet initialized")
}
log.Infof(ctx, "%p: started delivery worker", w)
defer log.Infof(ctx, "%p: stopped delivery worker", w)
for returned := false; !returned; {
func() {
defer func() {
if r := recover(); r != nil {
log.Errorf(ctx, "recovered panic: %v", r)
}
}()
w.process(ctx)
returned = true
}()
}
log.Infof(ctx, "%p: starting worker", w)
defer log.Infof(ctx, "%p: stopped worker", w)
util.Must(func() { w.process(ctx) })
}
// process is the main delivery worker processing routine.
func (w *Worker) process(ctx context.Context) {
func (w *Worker) process(ctx context.Context) bool {
if w.Client == nil || w.Queue == nil {
// we perform this check here just
// to ensure the compiler knows these
@ -188,7 +201,7 @@ loop:
// Get next delivery.
dlv, ok := w.next(ctx)
if !ok {
return
return true
}
// Check whether backoff required.
@ -203,7 +216,7 @@ loop:
// Main ctx
// cancelled.
backoff.Stop()
return
return true
case <-w.Queue.Wait():
// A new message was

View file

@ -32,9 +32,7 @@ func testDeliveryWorkerPool(t *testing.T, sz int, input []*testrequest) {
"127.0.0.0/8",
}),
}))
if !wp.Start(sz) {
t.Fatal("failed starting pool")
}
wp.Start(sz)
defer wp.Stop()
test(t, &wp.Queue, input)
}

68
internal/util/fns.go Normal file
View file

@ -0,0 +1,68 @@
// 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 util
import (
"fmt"
"os"
"runtime"
"codeberg.org/gruf/go-errors/v2"
)
// Must executes 'fn' repeatedly until
// it successfully returns without panic.
func Must(fn func()) {
if fn == nil {
panic("nil func")
}
for !func() (done bool) {
defer func() {
if r := recover(); r != nil {
// Gather calling func frames.
pcs := make([]uintptr, 10)
n := runtime.Callers(3, pcs)
i := runtime.CallersFrames(pcs[:n])
c := gatherFrames(i, n)
const msg = "recovered panic: %v\n\n%s\n"
fmt.Fprintf(os.Stderr, msg, r, c.String())
}
}()
fn()
done = true
return
}() { //nolint
}
}
// gatherFrames collates runtime frames from a frame iterator.
func gatherFrames(iter *runtime.Frames, n int) errors.Callers {
if iter == nil {
return nil
}
frames := make([]runtime.Frame, 0, n)
for {
f, ok := iter.Next()
if !ok {
break
}
frames = append(frames, f)
}
return frames
}

View file

@ -29,45 +29,43 @@ const (
robotsTxt = `# GoToSocial robots.txt -- to edit, see internal/web/robots.go
# More info @ https://developers.google.com/search/docs/crawling-indexing/robots/intro
# Before we commence, a giant fuck you to ChatGPT in particular.
# https://platform.openai.com/docs/gptbot
User-agent: GPTBot
Disallow: /
# As of September 2023, GPTBot and ChatGPT-User are equivalent. But there's no telling
# when OpenAI might decide to change that, so block this one too.
User-agent: ChatGPT-User
Disallow: /
# And a giant fuck you to Google Bard and their other generative AI ventures too.
# https://developers.google.com/search/docs/crawling-indexing/overview-google-crawlers
User-agent: Google-Extended
Disallow: /
# Block CommonCrawl. Used in training LLMs and specifically GPT-3.
# https://commoncrawl.org/faq
# AI scrapers and the like.
# https://github.com/ai-robots-txt/ai.robots.txt/
User-agent: AdsBot-Google
User-agent: Amazonbot
User-agent: anthropic-ai
User-agent: Applebot
User-agent: AwarioRssBot
User-agent: AwarioSmartBot
User-agent: Bytespider
User-agent: CCBot
Disallow: /
# Block Omgilike/Webz.io, a "Big Web Data" engine.
# https://webz.io/blog/web-data/what-is-the-omgili-bot-and-why-is-it-crawling-your-website/
User-agent: Omgilibot
Disallow: /
# Block Faceboobot, because Meta.
# https://developers.facebook.com/docs/sharing/bot
User-agent: ChatGPT-User
User-agent: ClaudeBot
User-agent: Claude-Web
User-agent: cohere-ai
User-agent: DataForSeoBot
User-agent: FacebookBot
User-agent: FriendlyCrawler
User-agent: Google-Extended
User-agent: GoogleOther
User-agent: GPTBot
User-agent: ImagesiftBot
User-agent: magpie-crawler
User-agent: Meltwater
User-agent: omgili
User-agent: omgilibot
User-agent: peer39_crawler
User-agent: peer39_crawler/1.0
User-agent: PerplexityBot
User-agent: PiplBot
User-agent: Seekr
User-agent: YouBot
Disallow: /
# Well-known.dev crawler. Indexes stuff under /.well-known.
# https://well-known.dev/about/
User-agent: WellKnownBot
Disallow: /
# Block Amazonbot, because Amazon.
# https://developer.amazon.com/amazonbot
User-agent: Amazonbot
Disallow: /
User-agent: WellKnownBot
Disallow: /
# Rules for everything else.
User-agent: *

View file

@ -0,0 +1,142 @@
// 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 workers
import (
"context"
"codeberg.org/gruf/go-runners"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/queue"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
// FnWorkerPool wraps multiple FnWorker{}s in
// a singular struct for easy multi start / stop.
type FnWorkerPool struct {
// Queue is embedded queue.SimpleQueue{}
// passed to each of the pool Worker{}s.
Queue queue.SimpleQueue[func(context.Context)]
// internal fields.
workers []*FnWorker
}
// Start will attempt to start 'n' FnWorker{}s.
func (p *FnWorkerPool) Start(n int) {
// Check whether workers are
// set (is already running).
ok := (len(p.workers) > 0)
if ok {
return
}
// Allocate new workers slice.
p.workers = make([]*FnWorker, n)
for i := range p.workers {
// Allocate new FnWorker{}.
p.workers[i] = new(FnWorker)
p.workers[i].Queue = &p.Queue
// Attempt to start worker.
// Return bool not useful
// here, as true = started,
// false = already running.
_ = p.workers[i].Start()
}
}
// Stop will attempt to stop contained FnWorker{}s.
func (p *FnWorkerPool) Stop() {
// Check whether workers are
// set (is currently running).
ok := (len(p.workers) == 0)
if ok {
return
}
// Stop all running workers.
for i := range p.workers {
// return bool not useful
// here, as true = stopped,
// false = never running.
_ = p.workers[i].Stop()
}
// Unset workers slice.
p.workers = p.workers[:0]
}
// FnWorker wraps a queue.SimpleQueue{} which
// it feeds from to provide it with function
// tasks to execute. It does so in a single
// goroutine with state management utilities.
type FnWorker struct {
// Queue is the fn queue that FnWorker
// will feed from for upcoming tasks.
Queue *queue.SimpleQueue[func(context.Context)]
// internal fields.
service runners.Service
}
// Start will attempt to start the Worker{}.
func (w *FnWorker) Start() bool {
return w.service.GoRun(w.run)
}
// Stop will attempt to stop the Worker{}.
func (w *FnWorker) Stop() bool {
return w.service.Stop()
}
// run wraps process to restart on any panic.
func (w *FnWorker) run(ctx context.Context) {
if w.Queue == nil {
panic("not yet initialized")
}
log.Infof(ctx, "%p: starting worker", w)
defer log.Infof(ctx, "%p: stopped worker", w)
util.Must(func() { w.process(ctx) })
}
// process is the main delivery worker processing routine.
func (w *FnWorker) process(ctx context.Context) {
if w.Queue == nil {
// we perform this check here just
// to ensure the compiler knows these
// variables aren't nil in the loop,
// even if already checked by caller.
panic("not yet initialized")
}
for {
// Block until pop next func.
fn, ok := w.Queue.PopCtx(ctx)
if !ok {
return
}
// run!
fn(ctx)
}
}

Some files were not shown because too many files have changed in this diff Show more