mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2025-02-11 15:20:17 +00:00
Merge branch 'main' into webfinger_rework
This commit is contained in:
commit
54d9778c4c
|
@ -1,8 +1,9 @@
|
||||||
.github
|
.github
|
||||||
cmd
|
.vscode
|
||||||
|
archive
|
||||||
|
dist
|
||||||
docs
|
docs
|
||||||
example
|
example
|
||||||
internal
|
|
||||||
scripts
|
scripts
|
||||||
test
|
test
|
||||||
testrig
|
testrig
|
||||||
|
|
|
@ -38,6 +38,7 @@ steps:
|
||||||
- apk update --no-cache && apk add git
|
- apk update --no-cache && apk add git
|
||||||
- CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./...
|
- CGO_ENABLED=0 GTS_DB_TYPE="sqlite" GTS_DB_ADDRESS=":memory:" go test ./...
|
||||||
- CGO_ENABLED=0 ./test/cliparsing.sh
|
- CGO_ENABLED=0 ./test/cliparsing.sh
|
||||||
|
- CGO_ENABLED=0 ./test/envparsing.sh
|
||||||
when:
|
when:
|
||||||
event:
|
event:
|
||||||
include:
|
include:
|
||||||
|
@ -145,6 +146,6 @@ steps:
|
||||||
|
|
||||||
---
|
---
|
||||||
kind: signature
|
kind: signature
|
||||||
hmac: f3cf4e422d9ce7dc0a881da429db628232e2f9e91383ee5a33cf4f13542c0a23
|
hmac: adfcc11559717e4e371e714f3ac19ab528208f678961436f316f491bf82de8ad
|
||||||
|
|
||||||
...
|
...
|
||||||
|
|
66
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
66
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
name: Bug Report
|
||||||
|
description: Create a report to help us improve
|
||||||
|
title: "[bug] Issue Title"
|
||||||
|
labels: [bug]
|
||||||
|
assignees:
|
||||||
|
-
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this bug report! Please be cautious with the sensitive information/logs while filing the issue.
|
||||||
|
- type: textarea
|
||||||
|
id: desc
|
||||||
|
attributes:
|
||||||
|
label: Describe the bug with a clear and concise description of what the bug is.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: GoToSocial-Version
|
||||||
|
attributes:
|
||||||
|
label: What's your GoToSocial Version?
|
||||||
|
description: Enter the Version of your GoToSocial Installation
|
||||||
|
placeholder: e.g. v0.3.4
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: GoToSocial-Arch
|
||||||
|
attributes:
|
||||||
|
label: GoToSocial Arch
|
||||||
|
description: What architecture do you use and did you do a Binary or Docker installation?
|
||||||
|
placeholder: e.g. arm64 Docker or arm64 Binary
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-happened
|
||||||
|
attributes:
|
||||||
|
label: What happened?
|
||||||
|
description: Enter exactly what happened.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: what-expected
|
||||||
|
attributes:
|
||||||
|
label: What you expected to happen?
|
||||||
|
description: Enter what you expected to happen.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: how-to-reproduce
|
||||||
|
attributes:
|
||||||
|
label: How to reproduce it?
|
||||||
|
description: As minimally and precisely as possible.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: anything-else
|
||||||
|
attributes:
|
||||||
|
label: Anything else we need to know?
|
||||||
|
validations:
|
||||||
|
required: false
|
8
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
8
.github/ISSUE_TEMPLATE/custom.md
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
---
|
||||||
|
name: Custom issue template
|
||||||
|
about: Describe this issue template's purpose here.
|
||||||
|
title: ''
|
||||||
|
labels: ''
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
44
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
44
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
name: Feature request
|
||||||
|
description: Suggest an idea for this project
|
||||||
|
title: "[feature] Issue Title"
|
||||||
|
labels: [enhancement]
|
||||||
|
assignees:
|
||||||
|
-
|
||||||
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out this feature request!
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: desc
|
||||||
|
attributes:
|
||||||
|
label: Is your feature request related to a problem ?
|
||||||
|
description: Give a clear and concise description of what the problem is.
|
||||||
|
placeholder: ex. I'm always frustrated when [...]
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: prop-solution
|
||||||
|
attributes:
|
||||||
|
label: Describe the solution you'd like.
|
||||||
|
description: A clear and concise description of what you want to happen.
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: alternatives
|
||||||
|
attributes:
|
||||||
|
label: Describe alternatives you've considered.
|
||||||
|
description: A clear and concise description of any alternative solutions or features you've considered. If nothing, please enter `NONE`
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
|
- type: textarea
|
||||||
|
id: additional-ctxt
|
||||||
|
attributes:
|
||||||
|
label: Additional context.
|
||||||
|
description: Add any other context or screenshots about the feature request here.
|
||||||
|
validations:
|
||||||
|
required: false
|
8
.gitignore
vendored
8
.gitignore
vendored
|
@ -20,4 +20,10 @@ dist/
|
||||||
web/assets/swagger.yaml
|
web/assets/swagger.yaml
|
||||||
|
|
||||||
# exludes docker-volume from exemple/docker-compose
|
# exludes docker-volume from exemple/docker-compose
|
||||||
example/docker-compose/docker-volume
|
example/docker-compose/docker-volume
|
||||||
|
|
||||||
|
# excludes debug build
|
||||||
|
cmd/gotosocial/__debug_bin
|
||||||
|
|
||||||
|
# ignore f0x' nix-shell
|
||||||
|
shell.nix
|
|
@ -3,16 +3,12 @@ project_name: gotosocial
|
||||||
before:
|
before:
|
||||||
# https://goreleaser.com/customization/hooks/
|
# https://goreleaser.com/customization/hooks/
|
||||||
hooks:
|
hooks:
|
||||||
# tidy up and lint
|
|
||||||
- go mod tidy
|
|
||||||
- go fmt ./...
|
|
||||||
# generate the swagger.yaml file using go-swagger and bundle it into the assets directory
|
# generate the swagger.yaml file using go-swagger and bundle it into the assets directory
|
||||||
- swagger generate spec -o docs/api/swagger.yaml --scan-models
|
- swagger generate spec -o web/assets/swagger.yaml --scan-models
|
||||||
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" docs/api/swagger.yaml
|
- sed -i "s/REPLACE_ME/{{ incpatch .Version }}/" web/assets/swagger.yaml
|
||||||
- cp docs/api/swagger.yaml web/assets/swagger.yaml
|
# bundle web assets
|
||||||
# install and bundle the web assets and styling
|
- yarn install --cwd web/source
|
||||||
- yarn install --cwd web/gotosocial-styling
|
- scripts/bundle.sh
|
||||||
- node web/gotosocial-styling/index.js --build-dir="web/assets"
|
|
||||||
builds:
|
builds:
|
||||||
# https://goreleaser.com/customization/build/
|
# https://goreleaser.com/customization/build/
|
||||||
-
|
-
|
||||||
|
@ -67,6 +63,10 @@ dockers:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- web
|
- web
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- cmd
|
||||||
|
- internal
|
||||||
-
|
-
|
||||||
use: buildx
|
use: buildx
|
||||||
goos: linux
|
goos: linux
|
||||||
|
@ -82,6 +82,10 @@ dockers:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- web
|
- web
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- cmd
|
||||||
|
- internal
|
||||||
-
|
-
|
||||||
use: buildx
|
use: buildx
|
||||||
goos: linux
|
goos: linux
|
||||||
|
@ -98,6 +102,10 @@ dockers:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- web
|
- web
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- cmd
|
||||||
|
- internal
|
||||||
-
|
-
|
||||||
use: buildx
|
use: buildx
|
||||||
goos: linux
|
goos: linux
|
||||||
|
@ -114,6 +122,10 @@ dockers:
|
||||||
- "--label=org.opencontainers.image.version={{.Version}}"
|
- "--label=org.opencontainers.image.version={{.Version}}"
|
||||||
extra_files:
|
extra_files:
|
||||||
- web
|
- web
|
||||||
|
- go.mod
|
||||||
|
- go.sum
|
||||||
|
- cmd
|
||||||
|
- internal
|
||||||
docker_manifests:
|
docker_manifests:
|
||||||
- name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }}
|
- name_template: superseriousbusiness/{{ .ProjectName }}:{{ .Version }}
|
||||||
image_templates:
|
image_templates:
|
||||||
|
@ -136,7 +148,8 @@ archives:
|
||||||
- README.md
|
- README.md
|
||||||
- CHANGELOG*
|
- CHANGELOG*
|
||||||
# web assets
|
# web assets
|
||||||
- web
|
- web/assets
|
||||||
|
- web/template
|
||||||
# example config files
|
# example config files
|
||||||
- example/config.yaml
|
- example/config.yaml
|
||||||
- example/gotosocial.service
|
- example/gotosocial.service
|
||||||
|
|
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Launch Package",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}/cmd/gotosocial",
|
||||||
|
"args": [
|
||||||
|
"testrig", "start"
|
||||||
|
],
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6
.vscode/settings.json
vendored
Normal file
6
.vscode/settings.json
vendored
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{
|
||||||
|
"go.lintTool":"golangci-lint",
|
||||||
|
"go.lintFlags": [
|
||||||
|
"--fast"
|
||||||
|
]
|
||||||
|
}
|
|
@ -28,7 +28,8 @@ Check the [issues](https://github.com/superseriousbusiness/gotosocial/issues) to
|
||||||
- [Updating Swagger docs](#updating-swagger-docs)
|
- [Updating Swagger docs](#updating-swagger-docs)
|
||||||
- [CI/CD configuration](#cicd-configuration)
|
- [CI/CD configuration](#cicd-configuration)
|
||||||
- [Release Checklist](#release-checklist)
|
- [Release Checklist](#release-checklist)
|
||||||
- [Building releases and Docker containers](#building-releases-and-docker-containers)
|
- [What if something goes wrong?](#what-if-something-goes-wrong)
|
||||||
|
- [Building Docker containers](#building-docker-containers)
|
||||||
- [With GoReleaser](#with-goreleaser)
|
- [With GoReleaser](#with-goreleaser)
|
||||||
- [Manually](#manually)
|
- [Manually](#manually)
|
||||||
- [Financial Compensation](#financial-compensation)
|
- [Financial Compensation](#financial-compensation)
|
||||||
|
@ -70,19 +71,21 @@ To work with the stylesheet for templates, you need [Node.js](https://nodejs.org
|
||||||
To install Yarn dependencies:
|
To install Yarn dependencies:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn install --cwd web/gotosocial-styling
|
yarn install --cwd web/source
|
||||||
```
|
```
|
||||||
|
|
||||||
To recompile bundles:
|
To recompile bundles:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
node web/gotosocial-styling/index.js --build-dir="web/assets"
|
BUDO_BUILD=1 node web/source
|
||||||
```
|
```
|
||||||
|
|
||||||
You can do automatic live-reloads of bundles with:
|
Or you can run livereloading in development. It will start a webserver (ip/port printed in console, default localhost:8081), while also keeping the bundles
|
||||||
|
up-to-date on disk. You can access the user/admin panels at localhost:8080/user, localhost:8080/admin, or run in tandem with the GoToSocial testrig, which will also
|
||||||
|
serve the updated bundles from disk.
|
||||||
|
|
||||||
``` bash
|
``` bash
|
||||||
NODE_ENV=development node web/gotosocial-styling/index.js --build-dir="web/assets"
|
NODE_ENV=development node web/source
|
||||||
```
|
```
|
||||||
|
|
||||||
### Golang forking quirks
|
### Golang forking quirks
|
||||||
|
@ -185,7 +188,7 @@ Finally, to run tests against both database types one after the other, use:
|
||||||
|
|
||||||
### CLI Tests
|
### CLI Tests
|
||||||
|
|
||||||
In [./test/cliparsing.sh](./test/cliparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected.
|
In [./test/cliparsing.sh](./test/cliparsing.sh) and [./test/envparsing.sh](./test/envparsing.sh) there are a bunch of tests for making sure that CLI flags, config, and environment variables get parsed as expected.
|
||||||
|
|
||||||
Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`.
|
Although these tests *are* part of the CI/CD testing process, you probably won't need to worry too much about running them yourself. That is, unless you're messing about with code inside the `main` package in `cmd/gotosocial`, or inside the `config` package in `internal/config`.
|
||||||
|
|
||||||
|
@ -299,7 +302,9 @@ That is: Delete the tag.
|
||||||
|
|
||||||
Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them.
|
Either way, once we've fixed the issue, we just start from the top of this list again. Version numbers are cheap. It's cheap to burn them.
|
||||||
|
|
||||||
## Building releases and Docker containers
|
## Building Docker containers
|
||||||
|
|
||||||
|
For both of the below methods, you need to have [Docker buildx](https://docs.docker.com/buildx/working-with-buildx/) installed.
|
||||||
|
|
||||||
### With GoReleaser
|
### With GoReleaser
|
||||||
|
|
||||||
|
@ -311,22 +316,30 @@ Normally, these processes are handled by Drone (see CI/CD above). However, you c
|
||||||
|
|
||||||
To do this, first [install GoReleaser](https://goreleaser.com/install/).
|
To do this, first [install GoReleaser](https://goreleaser.com/install/).
|
||||||
|
|
||||||
Then, to create snapshot builds, do:
|
Then install GoSwagger as described in [the Swagger section](#updating-swagger-docs).
|
||||||
|
|
||||||
|
Then install Node and Yarn as described in [Stylesheet / Web dev](#stylesheet--web-dev).
|
||||||
|
|
||||||
|
Finally, to create a snapshot build, do:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
goreleaser release --rm-dist --snapshot
|
goreleaser --rm-dist --snapshot
|
||||||
```
|
```
|
||||||
|
|
||||||
If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and a snapshot Docker image should be built (check your terminal output for version).
|
If all goes according to plan, you should now have a bunch of multiple-architecture binaries and tars inside the `./dist` folder, and snapshot Docker images should be built (check your terminal output for version).
|
||||||
|
|
||||||
### Manually
|
### Manually
|
||||||
|
|
||||||
If you prefer a simple approach with fewer dependencies, you can also just build a Docker container manually in the following way:
|
If you prefer a simple approach to building a Docker container, with fewer dependencies, you can also just build in the following way:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./scripts/build.sh && docker build -t superseriousbusiness/gotosocial:latest .
|
./scripts/build.sh && docker buildx build -t superseriousbusiness/gotosocial:latest .
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The above command first builds the `gotosocial` binary, then invokes Docker buildx to build the container image.
|
||||||
|
|
||||||
|
You don't need to install go-swagger, Node, or Yarn to build Docker images this way.
|
||||||
|
|
||||||
## Financial Compensation
|
## Financial Compensation
|
||||||
|
|
||||||
Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations.
|
Right now there's no structure in place for financial compensation for pull requests and code. This is simply because there's no money being made on the project apart from the very small weekly Liberapay donations.
|
||||||
|
|
35
Dockerfile
35
Dockerfile
|
@ -1,26 +1,31 @@
|
||||||
# syntax=docker/dockerfile:1.3
|
# syntax=docker/dockerfile:1.3
|
||||||
|
# stage 1: generate up-to-date swagger.yaml to put in the final container
|
||||||
|
FROM --platform=${BUILDPLATFORM} quay.io/goswagger/swagger:v0.29.0 AS swagger
|
||||||
|
|
||||||
# bundle the admin webapp
|
COPY go.mod /go/src/github.com/superseriousbusiness/gotosocial/go.mod
|
||||||
FROM --platform=${BUILDPLATFORM} node:17.6.0-alpine3.15 AS admin_builder
|
COPY go.sum /go/src/github.com/superseriousbusiness/gotosocial/go.sum
|
||||||
RUN apk update && apk upgrade --no-cache
|
COPY cmd /go/src/github.com/superseriousbusiness/gotosocial/cmd
|
||||||
RUN apk add git
|
COPY internal /go/src/github.com/superseriousbusiness/gotosocial/internal
|
||||||
|
WORKDIR /go/src/github.com/superseriousbusiness/gotosocial
|
||||||
|
RUN swagger generate spec -o /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml --scan-models
|
||||||
|
|
||||||
RUN git clone https://github.com/superseriousbusiness/gotosocial-admin
|
# stage 2: generate the web/assets/dist bundles
|
||||||
WORKDIR /gotosocial-admin
|
FROM --platform=${BUILDPLATFORM} node:16.15.1-alpine3.15 AS bundler
|
||||||
|
|
||||||
RUN npm install
|
COPY web web
|
||||||
RUN node index.js
|
RUN yarn install --cwd web/source && \
|
||||||
|
BUDO_BUILD=1 node web/source && \
|
||||||
|
rm -r web/source
|
||||||
|
|
||||||
FROM --platform=${TARGETPLATFORM} alpine:3.15.0 AS executor
|
# stage 3: build the executor container
|
||||||
|
FROM --platform=${TARGETPLATFORM} alpine:3.15.4 as executor
|
||||||
|
|
||||||
# copy over the binary from the first stage
|
# copy the dist binary created by goreleaser or build.sh
|
||||||
COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial
|
COPY --chown=1000:1000 gotosocial /gotosocial/gotosocial
|
||||||
|
|
||||||
# copy over the web directory with templates etc
|
# copy over the web directories with templates, assets etc
|
||||||
COPY --chown=1000:1000 web /gotosocial/web
|
COPY --chown=1000:1000 --from=bundler web /gotosocial/web
|
||||||
|
COPY --chown=1000:1000 --from=swagger /go/src/github.com/superseriousbusiness/gotosocial/swagger.yaml web/assets/swagger.yaml
|
||||||
# copy over the admin directory
|
|
||||||
COPY --chown=1000:1000 --from=admin_builder /gotosocial-admin/public /gotosocial/web/assets/admin
|
|
||||||
|
|
||||||
WORKDIR "/gotosocial"
|
WORKDIR "/gotosocial"
|
||||||
ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ]
|
ENTRYPOINT [ "/gotosocial/gotosocial", "server", "start" ]
|
||||||
|
|
|
@ -165,7 +165,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// build client api modules
|
// build client api modules
|
||||||
authModule := auth.New(dbService, oauthServer, idp)
|
authModule := auth.New(dbService, idp, processor)
|
||||||
accountModule := account.New(processor)
|
accountModule := account.New(processor)
|
||||||
instanceModule := instance.New(processor)
|
instanceModule := instance.New(processor)
|
||||||
appsModule := app.New(processor)
|
appsModule := app.New(processor)
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
// build client api modules
|
// build client api modules
|
||||||
authModule := auth.New(dbService, oauthServer, idp)
|
authModule := auth.New(dbService, idp, processor)
|
||||||
accountModule := account.New(processor)
|
accountModule := account.New(processor)
|
||||||
instanceModule := instance.New(processor)
|
instanceModule := instance.New(processor)
|
||||||
appsModule := app.New(processor)
|
appsModule := app.New(processor)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Admin Control Panel
|
# Admin Control Panel
|
||||||
|
|
||||||
[gotosocial-admin](https://github.com/superseriousbusiness/gotosocial-admin) is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin).
|
The GoToSocial admin panel is a simple webclient that uses the [admin api routes](https://docs.gotosocial.org/en/latest/api/swagger/#operations-tag-admin) to manage your instance. It uses the same OAUTH mechanism as normal clients (with scope: admin), and as such can be hosted anywhere, separately from your instance, or run locally. A public installation is available here: [https://gts.superseriousbusiness.org/admin](https://gts.superseriousbusiness.org/admin).
|
||||||
|
|
||||||
## Using the panel
|
## Using the panel
|
||||||
To use the Admin API your account has to be promoted as such:
|
To use the Admin API your account has to be promoted as such:
|
||||||
|
@ -16,11 +16,11 @@ any other client.
|
||||||
|
|
||||||
You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export.
|
You can change the instance's settings like the title and descriptions, and add/remove/change domain blocks including a bulk import/export.
|
||||||
|
|
||||||
## Installing the panel
|
## Building the panel
|
||||||
Build requirements: some version of [Node.js](https://nodejs.org) and yarn.
|
Build requirements: some version of [Node.js](https://nodejs.org) and yarn.
|
||||||
```
|
```
|
||||||
git clone https://github.com/superseriousbusiness/gotosocial-admin.git && cd gotosocial-admin
|
yarn install --cwd web/source
|
||||||
yarn install
|
BUDO_BUILD=1 node web/source
|
||||||
node index.js
|
|
||||||
```
|
```
|
||||||
This will compile a static bundle in `public/`, which can be copied to any webhost, or put into your GoToSocial installation in the `web/admin` directory.
|
|
||||||
|
See also: [Contributing.md Stylesheet / Web dev](https://github.com/superseriousbusiness/gotosocial/blob/main/CONTRIBUTING.md#stylesheet--web-dev)
|
|
@ -692,7 +692,7 @@ definitions:
|
||||||
text_url:
|
text_url:
|
||||||
description: |-
|
description: |-
|
||||||
A shorter URL for the attachment.
|
A shorter URL for the attachment.
|
||||||
Not currently used.
|
In our case, we just give the URL again since we don't create smaller URLs.
|
||||||
type: string
|
type: string
|
||||||
x-go-name: TextURL
|
x-go-name: TextURL
|
||||||
type:
|
type:
|
||||||
|
@ -1894,8 +1894,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Application:
|
- OAuth2 Application:
|
||||||
- write:accounts
|
- write:accounts
|
||||||
|
@ -1924,6 +1926,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -1952,6 +1958,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:blocks
|
- write:blocks
|
||||||
|
@ -1999,6 +2009,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:follows
|
- write:follows
|
||||||
|
@ -2029,6 +2043,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -2059,6 +2077,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -2134,6 +2156,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -2162,6 +2188,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:blocks
|
- write:blocks
|
||||||
|
@ -2190,6 +2220,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:follows
|
- write:follows
|
||||||
|
@ -2215,6 +2249,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:accounts
|
- write:accounts
|
||||||
|
@ -2247,6 +2287,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -2313,6 +2357,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:accounts
|
- write:accounts
|
||||||
|
@ -2335,6 +2385,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -2372,6 +2426,12 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2406,10 +2466,18 @@ paths:
|
||||||
$ref: '#/definitions/emoji'
|
$ref: '#/definitions/emoji'
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"409":
|
"409":
|
||||||
description: conflict -- domain/shortcode combo for emoji already exists
|
description: conflict -- domain/shortcode combo for emoji already exists
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2439,10 +2507,16 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2511,8 +2585,16 @@ paths:
|
||||||
$ref: '#/definitions/domainBlock'
|
$ref: '#/definitions/domainBlock'
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2537,10 +2619,16 @@ paths:
|
||||||
$ref: '#/definitions/domainBlock'
|
$ref: '#/definitions/domainBlock'
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2564,10 +2652,16 @@ paths:
|
||||||
$ref: '#/definitions/domainBlock'
|
$ref: '#/definitions/domainBlock'
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2599,8 +2693,16 @@ paths:
|
||||||
asynchronously after the request completes.
|
asynchronously after the request completes.
|
||||||
"400":
|
"400":
|
||||||
description: bad request
|
description: bad request
|
||||||
|
"401":
|
||||||
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2660,10 +2762,14 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"422":
|
"403":
|
||||||
description: unprocessable
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal server error
|
||||||
summary: Register a new application on this instance.
|
summary: Register a new application on this instance.
|
||||||
tags:
|
tags:
|
||||||
- apps
|
- apps
|
||||||
|
@ -2714,6 +2820,10 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:blocks
|
- read:blocks
|
||||||
|
@ -2753,10 +2863,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
|
||||||
description: forbidden
|
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:follows
|
- read:follows
|
||||||
|
@ -2785,10 +2897,10 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
|
||||||
description: forbidden
|
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
|
@ -2817,10 +2929,10 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
|
||||||
description: forbidden
|
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal server error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
|
@ -2843,6 +2955,8 @@ paths:
|
||||||
description: Instance information.
|
description: Instance information.
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/instance'
|
$ref: '#/definitions/instance'
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal error
|
||||||
summary: View instance information.
|
summary: View instance information.
|
||||||
|
@ -2909,6 +3023,14 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- admin
|
- admin
|
||||||
|
@ -2952,10 +3074,10 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
|
||||||
description: forbidden
|
|
||||||
"422":
|
"422":
|
||||||
description: unprocessable
|
description: unprocessable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:media
|
- write:media
|
||||||
|
@ -2982,10 +3104,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
"404":
|
||||||
description: forbidden
|
description: not found
|
||||||
"422":
|
"406":
|
||||||
description: unprocessable
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:media
|
- read:media
|
||||||
|
@ -3036,10 +3160,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
"404":
|
||||||
description: forbidden
|
description: not found
|
||||||
"422":
|
"406":
|
||||||
description: unprocessable
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:media
|
- write:media
|
||||||
|
@ -3141,6 +3267,12 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"404":
|
||||||
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:search
|
- read:search
|
||||||
|
@ -3226,10 +3358,14 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3263,6 +3399,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3288,10 +3428,14 @@ paths:
|
||||||
description: bad request
|
description: bad request
|
||||||
"401":
|
"401":
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
|
"403":
|
||||||
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:statuses
|
- read:statuses
|
||||||
|
@ -3324,6 +3468,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:statuses
|
- read:statuses
|
||||||
|
@ -3354,6 +3502,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3386,6 +3538,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- read:accounts
|
- read:accounts
|
||||||
|
@ -3419,6 +3575,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3481,6 +3641,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3511,6 +3675,10 @@ paths:
|
||||||
description: forbidden
|
description: forbidden
|
||||||
"404":
|
"404":
|
||||||
description: not found
|
description: not found
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
|
"500":
|
||||||
|
description: internal server error
|
||||||
security:
|
security:
|
||||||
- OAuth2 Bearer:
|
- OAuth2 Bearer:
|
||||||
- write:statuses
|
- write:statuses
|
||||||
|
@ -3778,6 +3946,8 @@ paths:
|
||||||
description: unauthorized
|
description: unauthorized
|
||||||
"403":
|
"403":
|
||||||
description: forbidden
|
description: forbidden
|
||||||
|
"406":
|
||||||
|
description: not acceptable
|
||||||
"500":
|
"500":
|
||||||
description: internal error
|
description: internal error
|
||||||
security:
|
security:
|
||||||
|
|
38
docs/configuration/advanced.md
Normal file
38
docs/configuration/advanced.md
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
# Advanced
|
||||||
|
|
||||||
|
Advanced settings options are provided for the sake of allowing admins to tune their instance to their liking.
|
||||||
|
|
||||||
|
These are set to sensible defaults, so most server admins won't need to touch them or think about them.
|
||||||
|
|
||||||
|
**Changing these settings if you don't know what you're doing may break your instance**.
|
||||||
|
|
||||||
|
## Settings
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
#############################
|
||||||
|
##### ADVANCED SETTINGS #####
|
||||||
|
#############################
|
||||||
|
|
||||||
|
# Advanced settings pertaining to http timeouts, security, cookies, and more.
|
||||||
|
#
|
||||||
|
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
|
||||||
|
#
|
||||||
|
# Most users will not need to (and should not) touch these settings, since
|
||||||
|
# they are set to sensible defaults, and may break if they are changed.
|
||||||
|
#
|
||||||
|
# Nevertheless, they are provided for the sake of allowing server admins to
|
||||||
|
# tweak their instance for performance or security reasons.
|
||||||
|
|
||||||
|
# String. Value of the SameSite attribute of cookies set by GoToSocial.
|
||||||
|
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
|
||||||
|
# fine in most cases. If you want to harden your instance against CSRF attacks
|
||||||
|
# and don't mind if some login-related things might break, you can set this
|
||||||
|
# to 'strict' instead.
|
||||||
|
#
|
||||||
|
# For an overview of what this does, see:
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||||
|
#
|
||||||
|
# Options: ["lax", "strict"]
|
||||||
|
# Default: "lax"
|
||||||
|
advanced-cookies-samesite: "lax"
|
||||||
|
```
|
|
@ -428,3 +428,30 @@ syslog-protocol: "udp"
|
||||||
# String. Address:port to send syslog logs to. Leave empty to connect to local syslog.
|
# String. Address:port to send syslog logs to. Leave empty to connect to local syslog.
|
||||||
# Default: "localhost:514"
|
# Default: "localhost:514"
|
||||||
syslog-address: "localhost:514"
|
syslog-address: "localhost:514"
|
||||||
|
|
||||||
|
#############################
|
||||||
|
##### ADVANCED SETTINGS #####
|
||||||
|
#############################
|
||||||
|
|
||||||
|
# Advanced settings pertaining to http timeouts, security, cookies, and more.
|
||||||
|
#
|
||||||
|
# ONLY ADJUST THESE SETTINGS IF YOU KNOW WHAT YOU ARE DOING!
|
||||||
|
#
|
||||||
|
# Most users will not need to (and should not) touch these settings, since
|
||||||
|
# they are set to sensible defaults, and may break if they are changed.
|
||||||
|
#
|
||||||
|
# Nevertheless, they are provided for the sake of allowing server admins to
|
||||||
|
# tweak their instance for performance or security reasons.
|
||||||
|
|
||||||
|
# String. Value of the SameSite attribute of cookies set by GoToSocial.
|
||||||
|
# Defaults to 'lax' to ensure that the OIDC flow does not break, which is
|
||||||
|
# fine in most cases. If you want to harden your instance against CSRF attacks
|
||||||
|
# and don't mind if some login-related things might break, you can set this
|
||||||
|
# to 'strict' instead.
|
||||||
|
#
|
||||||
|
# For an overview of what this does, see:
|
||||||
|
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite
|
||||||
|
#
|
||||||
|
# Options: ["lax", "strict"]
|
||||||
|
# Default: "lax"
|
||||||
|
advanced-cookies-samesite: "lax"
|
||||||
|
|
2
go.sum
2
go.sum
|
@ -663,8 +663,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4 h1:HVyaeDAYux4pnY+D/SiwmLOR36ewZ4iGQIIrtnuCjFA=
|
|
||||||
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
|
||||||
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
|
golang.org/x/net v0.0.0-20220524220425-1d687d428aca h1:xTaFYiPROfpPhqrfTIDXj0ri1SpfueYT951s4bAuDO8=
|
||||||
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
golang.org/x/net v0.0.0-20220524220425-1d687d428aca/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||||
|
|
|
@ -23,12 +23,11 @@
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
)
|
)
|
||||||
|
@ -61,58 +60,51 @@
|
||||||
// description: "An OAuth2 access token for the newly-created account."
|
// description: "An OAuth2 access token for the newly-created account."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/oauthToken"
|
// "$ref": "#/definitions/oauthToken"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal error
|
// description: internal server error
|
||||||
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "accountCreatePOSTHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, false, false)
|
authed, err := oauth.Authed(c, true, true, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("parsing request form")
|
|
||||||
form := &model.AccountCreateRequest{}
|
form := &model.AccountCreateRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil || form == nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("could not parse form from request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Tracef("validating form %+v", form)
|
|
||||||
if err := validateCreateAccount(form); err != nil {
|
if err := validateCreateAccount(form); err != nil {
|
||||||
l.Debugf("error validating form: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
clientIP := c.ClientIP()
|
clientIP := c.ClientIP()
|
||||||
l.Tracef("attempting to parse client ip address %s", clientIP)
|
|
||||||
signUpIP := net.ParseIP(clientIP)
|
signUpIP := net.ParseIP(clientIP)
|
||||||
if signUpIP == nil {
|
if signUpIP == nil {
|
||||||
l.Debugf("error validating sign up ip address %s", clientIP)
|
err := errors.New("ip address could not be parsed from request")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "ip address could not be parsed from request"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form.IP = signUpIP
|
form.IP = signUpIP
|
||||||
|
|
||||||
ti, err := m.processor.AccountCreate(c.Request.Context(), authed, form)
|
ti, errWithCode := m.processor.AccountCreate(c.Request.Context(), authed, form)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Errorf("internal server error while creating new account: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -122,6 +114,10 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) {
|
||||||
// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
|
// validateCreateAccount checks through all the necessary prerequisites for creating a new account,
|
||||||
// according to the provided account create request. If the account isn't eligible, an error will be returned.
|
// according to the provided account create request. If the account isn't eligible, an error will be returned.
|
||||||
func validateCreateAccount(form *model.AccountCreateRequest) error {
|
func validateCreateAccount(form *model.AccountCreateRequest) error {
|
||||||
|
if form == nil {
|
||||||
|
return errors.New("form was nil")
|
||||||
|
}
|
||||||
|
|
||||||
if !config.GetAccountsRegistrationOpen() {
|
if !config.GetAccountsRegistrationOpen() {
|
||||||
return errors.New("registration is not open for this server")
|
return errors.New("registration is not open for this server")
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,13 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -57,32 +58,35 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
func (m *Module) AccountDeletePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AccountDeletePOSTHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Tracef("retrieved account %+v", authed.Account.ID)
|
|
||||||
|
|
||||||
form := &model.AccountDeleteRequest{}
|
form := &model.AccountDeleteRequest{}
|
||||||
if err := c.ShouldBind(&form); err != nil {
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Password == "" {
|
if form.Password == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no password provided in account delete request"})
|
err = errors.New("no password provided in account delete request")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form.DeleteOriginID = authed.Account.ID
|
form.DeleteOriginID = authed.Account.ID
|
||||||
|
|
||||||
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
|
if errWithCode := m.processor.AccountDeleteLocal(c.Request.Context(), authed, form); errWithCode != nil {
|
||||||
l.Debugf("could not delete account: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -53,34 +54,38 @@
|
||||||
// '200':
|
// '200':
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountGETHandler(c *gin.Context) {
|
func (m *Module) AccountGETHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
|
acctInfo, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, targetAcctID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
logrus.Debug(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,15 +19,15 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -98,68 +98,67 @@
|
||||||
// description: "The newly updated account."
|
// description: "The newly updated account."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
|
func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "accountUpdateCredentialsPATCHHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Tracef("retrieved account %+v", authed.Account.ID)
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form, err := parseUpdateAccountForm(c)
|
form, err := parseUpdateAccountForm(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// if everything on the form is nil, then nothing has been set and we shouldn't continue
|
acctSensitive, errWithCode := m.processor.AccountUpdate(c.Request.Context(), authed, form)
|
||||||
if form.Discoverable == nil &&
|
if errWithCode != nil {
|
||||||
form.Bot == nil &&
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
form.DisplayName == nil &&
|
|
||||||
form.Note == nil &&
|
|
||||||
form.Avatar == nil &&
|
|
||||||
form.Header == nil &&
|
|
||||||
form.Locked == nil &&
|
|
||||||
form.Source.Privacy == nil &&
|
|
||||||
form.Source.Sensitive == nil &&
|
|
||||||
form.Source.Language == nil &&
|
|
||||||
form.FieldsAttributes == nil {
|
|
||||||
l.Debugf("could not parse form from request")
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acctSensitive, err := m.processor.AccountUpdate(c.Request.Context(), authed, form)
|
|
||||||
if err != nil {
|
|
||||||
l.Debugf("could not update account: %s", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Tracef("conversion successful, returning OK and apisensitive account %+v", acctSensitive)
|
|
||||||
c.JSON(http.StatusOK, acctSensitive)
|
c.JSON(http.StatusOK, acctSensitive)
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) {
|
func parseUpdateAccountForm(c *gin.Context) (*model.UpdateCredentialsRequest, error) {
|
||||||
// parse main fields from request
|
|
||||||
form := &model.UpdateCredentialsRequest{
|
form := &model.UpdateCredentialsRequest{
|
||||||
Source: &model.UpdateSource{},
|
Source: &model.UpdateSource{},
|
||||||
}
|
}
|
||||||
if err := c.ShouldBind(&form); err != nil || form == nil {
|
|
||||||
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
return nil, fmt.Errorf("could not parse form from request: %s", err)
|
return nil, fmt.Errorf("could not parse form from request: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form == nil ||
|
||||||
|
(form.Discoverable == nil &&
|
||||||
|
form.Bot == nil &&
|
||||||
|
form.DisplayName == nil &&
|
||||||
|
form.Note == nil &&
|
||||||
|
form.Avatar == nil &&
|
||||||
|
form.Header == nil &&
|
||||||
|
form.Locked == nil &&
|
||||||
|
form.Source.Privacy == nil &&
|
||||||
|
form.Source.Sensitive == nil &&
|
||||||
|
form.Source.Language == nil &&
|
||||||
|
form.FieldsAttributes == nil) {
|
||||||
|
return nil, errors.New("empty form submitted")
|
||||||
|
}
|
||||||
|
|
||||||
// parse source field-by-field
|
// parse source field-by-field
|
||||||
sourceMap := c.PostFormMap("source")
|
sourceMap := c.PostFormMap("source")
|
||||||
|
|
||||||
|
|
|
@ -26,7 +26,6 @@
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/account"
|
||||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
@ -65,7 +64,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandler()
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount := &apimodel.Account{}
|
apimodelAccount := &apimodel.Account{}
|
||||||
|
@ -104,7 +103,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUnl
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b1, err := ioutil.ReadAll(result1.Body)
|
b1, err := ioutil.ReadAll(result1.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount1 := &apimodel.Account{}
|
apimodelAccount1 := &apimodel.Account{}
|
||||||
|
@ -185,7 +184,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerGet
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount := &apimodel.Account{}
|
apimodelAccount := &apimodel.Account{}
|
||||||
|
@ -227,7 +226,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerTwo
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount := &apimodel.Account{}
|
apimodelAccount := &apimodel.Account{}
|
||||||
|
@ -271,7 +270,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerWit
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount := &apimodel.Account{}
|
apimodelAccount := &apimodel.Account{}
|
||||||
|
@ -313,8 +312,8 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerEmp
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
suite.Equal(`{"error":"empty form submitted"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() {
|
func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpdateSource() {
|
||||||
|
@ -348,7 +347,7 @@ func (suite *AccountUpdateTestSuite) TestAccountUpdateCredentialsPATCHHandlerUpd
|
||||||
|
|
||||||
// check the response
|
// check the response
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// unmarshal the returned account
|
// unmarshal the returned account
|
||||||
apimodelAccount := &apimodel.Account{}
|
apimodelAccount := &apimodel.Account{}
|
||||||
|
|
|
@ -21,10 +21,9 @@
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -47,30 +46,31 @@
|
||||||
// '200':
|
// '200':
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
|
func (m *Module) AccountVerifyGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "accountVerifyGETHandler")
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
authed, err := oauth.Authed(c, true, false, false, true)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
|
acctSensitive, errWithCode := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error getting account from processor: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,33 +56,38 @@
|
||||||
// description: Your relationship to this account.
|
// description: Your relationship to this account.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/accountRelationship"
|
// "$ref": "#/definitions/accountRelationship"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
|
func (m *Module) AccountBlockPOSTHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID)
|
relationship, errWithCode := m.processor.AccountBlockCreate(c.Request.Context(), authed, targetAcctID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,13 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,39 +77,45 @@
|
||||||
// description: Your relationship to this account.
|
// description: Your relationship to this account.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/accountRelationship"
|
// "$ref": "#/definitions/accountRelationship"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
|
func (m *Module) AccountFollowPOSTHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form := &model.AccountFollowRequest{}
|
form := &model.AccountFollowRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.ID = targetAcctID
|
form.ID = targetAcctID
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form)
|
relationship, errWithCode := m.processor.AccountFollowCreate(c.Request.Context(), authed, form)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,33 +58,38 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountFollowersGETHandler(c *gin.Context) {
|
func (m *Module) AccountFollowersGETHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID)
|
followers, errWithCode := m.processor.AccountFollowersGet(c.Request.Context(), authed, targetAcctID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,33 +58,38 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
|
func (m *Module) AccountFollowingGETHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID)
|
following, errWithCode := m.processor.AccountFollowingGet(c.Request.Context(), authed, targetAcctID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,24 +43,25 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/accountRelationship"
|
// "$ref": "#/definitions/accountRelationship"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AccountRelationshipsGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -69,8 +70,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||||
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
// check fallback -- let's be generous and see if maybe it's just set as 'id'?
|
||||||
id := c.Query("id")
|
id := c.Query("id")
|
||||||
if id == "" {
|
if id == "" {
|
||||||
l.Debug("no account id specified in query")
|
err = errors.New("no account id(s) specified in query")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
targetAccountIDs = append(targetAccountIDs, id)
|
targetAccountIDs = append(targetAccountIDs, id)
|
||||||
|
@ -80,8 +81,8 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
for _, targetAccountID := range targetAccountIDs {
|
for _, targetAccountID := range targetAccountIDs {
|
||||||
r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID)
|
r, errWithCode := m.processor.AccountRelationshipGet(c.Request.Context(), authed, targetAccountID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
relationships = append(relationships, *r)
|
relationships = append(relationships, *r)
|
||||||
|
|
|
@ -19,13 +19,14 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -110,31 +111,32 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/status"
|
// "$ref": "#/definitions/status"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AccountStatusesGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
authed, err := oauth.Authed(c, false, false, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
l.Debug("no account id specified in query")
|
err := errors.New("no account id specified")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,8 +145,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -155,8 +157,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if excludeRepliesString != "" {
|
if excludeRepliesString != "" {
|
||||||
i, err := strconv.ParseBool(excludeRepliesString)
|
i, err := strconv.ParseBool(excludeRepliesString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing exclude replies string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", ExcludeRepliesKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude replies query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
excludeReplies = i
|
excludeReplies = i
|
||||||
|
@ -167,8 +169,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if excludeReblogsString != "" {
|
if excludeReblogsString != "" {
|
||||||
i, err := strconv.ParseBool(excludeReblogsString)
|
i, err := strconv.ParseBool(excludeReblogsString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing exclude reblogs string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", ExcludeReblogsKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse exclude reblogs query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
excludeReblogs = i
|
excludeReblogs = i
|
||||||
|
@ -191,8 +193,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if pinnedString != "" {
|
if pinnedString != "" {
|
||||||
i, err := strconv.ParseBool(pinnedString)
|
i, err := strconv.ParseBool(pinnedString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing pinned string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", PinnedKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pinnedOnly = i
|
pinnedOnly = i
|
||||||
|
@ -203,8 +205,8 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if mediaOnlyString != "" {
|
if mediaOnlyString != "" {
|
||||||
i, err := strconv.ParseBool(mediaOnlyString)
|
i, err := strconv.ParseBool(mediaOnlyString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing media only string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", OnlyMediaKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse media only query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
mediaOnly = i
|
mediaOnly = i
|
||||||
|
@ -215,19 +217,21 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) {
|
||||||
if publicOnlyString != "" {
|
if publicOnlyString != "" {
|
||||||
i, err := strconv.ParseBool(publicOnlyString)
|
i, err := strconv.ParseBool(publicOnlyString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing public only string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", OnlyPublicKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse public only query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
publicOnly = i
|
publicOnly = i
|
||||||
}
|
}
|
||||||
|
|
||||||
statuses, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
resp, errWithCode := m.processor.AccountStatusesGet(c.Request.Context(), authed, targetAcctID, limit, excludeReplies, excludeReblogs, maxID, minID, pinnedOnly, mediaOnly, publicOnly)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor account statuses get: %s", errWithCode)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, statuses)
|
if resp.LinkHeader != "" {
|
||||||
|
c.Header("Link", resp.LinkHeader)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,47 @@ type AccountStatusesTestSuite struct {
|
||||||
AccountStandardTestSuite
|
AccountStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
|
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnly() {
|
||||||
|
// set up the request
|
||||||
|
// we're getting statuses of admin
|
||||||
|
targetAccount := suite.testAccounts["admin_account"]
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx := suite.newContext(recorder, http.MethodGet, nil, fmt.Sprintf("/api/v1/accounts/%s/statuses?limit=20&only_media=false&only_public=true", targetAccount.ID), "")
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: account.IDKey,
|
||||||
|
Value: targetAccount.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
suite.accountModule.AccountStatusesGETHandler(ctx)
|
||||||
|
|
||||||
|
// 1. we should have OK because our request was valid
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
// 2. we should have no error message in the result body
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
// check the response
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
// unmarshal the returned statuses
|
||||||
|
apimodelStatuses := []*apimodel.Status{}
|
||||||
|
err = json.Unmarshal(b, &apimodelStatuses)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotEmpty(apimodelStatuses)
|
||||||
|
|
||||||
|
for _, s := range apimodelStatuses {
|
||||||
|
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01G36SF3V6Y6V5BF9P4R7PQG7G&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=false&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *AccountStatusesTestSuite) TestGetStatusesPublicOnlyMediaOnly() {
|
||||||
// set up the request
|
// set up the request
|
||||||
// we're getting statuses of admin
|
// we're getting statuses of admin
|
||||||
targetAccount := suite.testAccounts["admin_account"]
|
targetAccount := suite.testAccounts["admin_account"]
|
||||||
|
@ -74,6 +114,8 @@ func (suite *AccountStatusesTestSuite) TestGetStatusesMediaOnly() {
|
||||||
suite.NotEmpty(s.MediaAttachments)
|
suite.NotEmpty(s.MediaAttachments)
|
||||||
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
suite.Equal(apimodel.VisibilityPublic, s.Visibility)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suite.Equal(`<http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&max_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="next", <http://localhost:8080/api/v1/accounts/01F8MH17FWEB39HZJ76B6VXSKF/statuses?limit=20&min_id=01F8MH75CBF9JFX4ZAD54N0W0R&exclude_replies=false&exclude_reblogs=false&pinned_only=false&only_media=true&only_public=true>; rel="prev"`, result.Header.Get("link"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAccountStatusesTestSuite(t *testing.T) {
|
func TestAccountStatusesTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -19,10 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,33 +56,38 @@
|
||||||
// description: Your relationship to this account.
|
// description: Your relationship to this account.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/accountRelationship"
|
// "$ref": "#/definitions/accountRelationship"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
|
func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) {
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID)
|
relationship, errWithCode := m.processor.AccountBlockRemove(c.Request.Context(), authed, targetAcctID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
package account
|
package account
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,37 +56,38 @@
|
||||||
// description: Your relationship to this account.
|
// description: Your relationship to this account.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/accountRelationship"
|
// "$ref": "#/definitions/accountRelationship"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
|
func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AccountUnfollowPOSTHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug(err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
l.Debug(err)
|
err := errors.New("no account id specified")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID)
|
relationship, errWithCode := m.processor.AccountFollowRemove(c.Request.Context(), authed, targetAcctID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debug(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,14 @@
|
||||||
package admin
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -72,53 +74,47 @@
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
// '403':
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
|
func (m *Module) AccountActionPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "AccountActionPOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed...
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// with an admin account
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the form from the request context
|
|
||||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
|
||||||
form := &model.AdminAccountActionRequest{}
|
form := &model.AdminAccountActionRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.Type == "" {
|
if form.Type == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no type specified"})
|
err := errors.New("no type specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetAcctID := c.Param(IDKey)
|
targetAcctID := c.Param(IDKey)
|
||||||
if targetAcctID == "" {
|
if targetAcctID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
form.TargetAccountID = targetAcctID
|
form.TargetAccountID = targetAcctID
|
||||||
|
|
||||||
if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil {
|
if errWithCode := m.processor.AdminAccountAction(c.Request.Context(), authed, form); errWithCode != nil {
|
||||||
l.Debugf("error performing account action: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -7,9 +25,9 @@
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -86,33 +104,33 @@
|
||||||
// Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead.
|
// Note that if a list has been imported, then an `array` of newly created domain blocks will be returned instead.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/domainBlock"
|
// "$ref": "#/definitions/domainBlock"
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
|
func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "DomainBlocksPOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed with an admin account
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -121,49 +139,43 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) {
|
||||||
if importString != "" {
|
if importString != "" {
|
||||||
i, err := strconv.ParseBool(importString)
|
i, err := strconv.ParseBool(importString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing import string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", ImportQueryKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse import query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
imp = i
|
imp = i
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the media create form from the request context
|
|
||||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
|
||||||
form := &model.DomainBlockCreateRequest{}
|
form := &model.DomainBlockCreateRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
|
||||||
l.Tracef("validating form %+v", form)
|
|
||||||
if err := validateCreateDomainBlock(form, imp); err != nil {
|
if err := validateCreateDomainBlock(form, imp); err != nil {
|
||||||
l.Debugf("error validating form: %s", err)
|
err := fmt.Errorf("error validating form: %s", err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if imp {
|
if imp {
|
||||||
// we're importing multiple blocks
|
// we're importing multiple blocks
|
||||||
domainBlocks, err := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
|
domainBlocks, errWithCode := m.processor.AdminDomainBlocksImport(c.Request.Context(), authed, form)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error importing domain blocks: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, domainBlocks)
|
c.JSON(http.StatusOK, domainBlocks)
|
||||||
} else {
|
return
|
||||||
// we're just creating one block
|
|
||||||
domainBlock, err := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
|
|
||||||
if err != nil {
|
|
||||||
l.Debugf("error creating domain block: %s", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.JSON(http.StatusOK, domainBlock)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// we're just creating one block
|
||||||
|
domainBlock, errWithCode := m.processor.AdminDomainBlockCreate(c.Request.Context(), authed, form)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, domainBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {
|
func validateCreateDomainBlock(form *model.DomainBlockCreateRequest, imp bool) error {
|
||||||
|
|
|
@ -1,11 +1,31 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,48 +56,46 @@
|
||||||
// description: The domain block that was just deleted.
|
// description: The domain block that was just deleted.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/domainBlock"
|
// "$ref": "#/definitions/domainBlock"
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
|
func (m *Module) DomainBlockDELETEHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "DomainBlockDELETEHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed with an admin account
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domainBlockID := c.Param(IDKey)
|
domainBlockID := c.Param(IDKey)
|
||||||
if domainBlockID == "" {
|
if domainBlockID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
err := errors.New("no domain block id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID)
|
domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(c.Request.Context(), authed, domainBlockID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error deleting domain block: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,32 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,41 +57,40 @@
|
||||||
// description: The requested domain block.
|
// description: The requested domain block.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/domainBlock"
|
// "$ref": "#/definitions/domainBlock"
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "DomainBlockGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed with an admin account
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
domainBlockID := c.Param(IDKey)
|
domainBlockID := c.Param(IDKey)
|
||||||
if domainBlockID == "" {
|
if domainBlockID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"})
|
err := errors.New("no domain block id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,17 +99,16 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) {
|
||||||
if exportString != "" {
|
if exportString != "" {
|
||||||
i, err := strconv.ParseBool(exportString)
|
i, err := strconv.ParseBool(exportString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing export string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
export = i
|
export = i
|
||||||
}
|
}
|
||||||
|
|
||||||
domainBlock, err := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
|
domainBlock, errWithCode := m.processor.AdminDomainBlockGet(c.Request.Context(), authed, domainBlockID, export)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error getting domain block: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,31 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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
|
package admin
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -43,35 +62,33 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/domainBlock"
|
// "$ref": "#/definitions/domainBlock"
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "DomainBlocksGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed with an admin account
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -80,17 +97,16 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) {
|
||||||
if exportString != "" {
|
if exportString != "" {
|
||||||
i, err := strconv.ParseBool(exportString)
|
i, err := strconv.ParseBool(exportString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing export string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", ExportQueryKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
export = i
|
export = i
|
||||||
}
|
}
|
||||||
|
|
||||||
domainBlocks, err := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
|
domainBlocks, errWithCode := m.processor.AdminDomainBlocksGet(c.Request.Context(), authed, export)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error getting domain blocks: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -24,9 +24,9 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
)
|
)
|
||||||
|
@ -69,59 +69,52 @@
|
||||||
// description: The newly-created emoji.
|
// description: The newly-created emoji.
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/emoji"
|
// "$ref": "#/definitions/emoji"
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '409':
|
// '409':
|
||||||
// description: conflict -- domain/shortcode combo for emoji already exists
|
// description: conflict -- domain/shortcode combo for emoji already exists
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
"func": "emojiCreatePOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed with an admin account
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything*
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the media create form from the request context
|
|
||||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
|
||||||
form := &model.EmojiCreateRequest{}
|
form := &model.EmojiCreateRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
|
||||||
l.Tracef("validating form %+v", form)
|
|
||||||
if err := validateCreateEmoji(form); err != nil {
|
if err := validateCreateEmoji(form); err != nil {
|
||||||
l.Debugf("error validating form: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
|
apiEmoji, errWithCode := m.processor.AdminEmojiCreate(c.Request.Context(), authed, form)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error creating emoji: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -129,7 +122,6 @@ func (m *Module) EmojiCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCreateEmoji(form *model.EmojiCreateRequest) error {
|
func validateCreateEmoji(form *model.EmojiCreateRequest) error {
|
||||||
// check there actually is an image attached and it's not size 0
|
|
||||||
if form.Image == nil || form.Image.Size == 0 {
|
if form.Image == nil || form.Image.Size == 0 {
|
||||||
return errors.New("no emoji given")
|
return errors.New("no emoji given")
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,21 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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
|
package admin_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
@ -120,7 +138,7 @@ func (suite *EmojiCreateTestSuite) TestEmojiCreateAlreadyExists() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotEmpty(b)
|
suite.NotEmpty(b)
|
||||||
|
|
||||||
suite.Equal(`{"error":"conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
suite.Equal(`{"error":"Conflict: emoji with shortcode rainbow already exists"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmojiCreateTestSuite(t *testing.T) {
|
func TestEmojiCreateTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -23,9 +23,10 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,39 +55,34 @@
|
||||||
// '200':
|
// '200':
|
||||||
// description: |-
|
// description: |-
|
||||||
// Echos the number of days requested. The cleanup is performed asynchronously after the request completes.
|
// Echos the number of days requested. The cleanup is performed asynchronously after the request completes.
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
|
func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "MediaCleanupPOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// make sure we're authed...
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// with an admin account
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debugf("user %s not an admin", authed.User.ID)
|
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the form from the request context
|
|
||||||
l.Tracef("parsing request form: %+v", c.Request.Form)
|
|
||||||
form := &model.MediaCleanupRequest{}
|
form := &model.MediaCleanupRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("error parsing form %+v: %s", c.Request.Form, err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,8 +97,7 @@ func (m *Module) MediaCleanupPOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil {
|
if errWithCode := m.processor.AdminMediaPrune(c.Request.Context(), remoteCacheDays); errWithCode != nil {
|
||||||
l.Debugf("error starting prune of remote media: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,18 +22,16 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// these consts are used to ensure users can't spam huge entries into our database
|
||||||
const (
|
const (
|
||||||
// permitted length for most fields
|
formFieldLen = 64
|
||||||
formFieldLen = 64
|
|
||||||
// redirect can be a bit bigger because we probably need to encode data in the redirect uri
|
|
||||||
formRedirectLen = 512
|
formRedirectLen = 512
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -64,56 +62,63 @@
|
||||||
// description: "The newly-created application."
|
// description: "The newly-created application."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/application"
|
// "$ref": "#/definitions/application"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '422':
|
// '401':
|
||||||
// description: unprocessable
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal error
|
// description: internal server error
|
||||||
func (m *Module) AppsPOSTHandler(c *gin.Context) {
|
func (m *Module) AppsPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AppsPOSTHandler")
|
|
||||||
l.Trace("entering AppsPOSTHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
authed, err := oauth.Authed(c, false, false, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form := &model.ApplicationCreateRequest{}
|
form := &model.ApplicationCreateRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// check lengths of fields before proceeding so the user can't spam huge entries into the database
|
|
||||||
if len(form.ClientName) > formFieldLen {
|
if len(form.ClientName) > formFieldLen {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("client_name must be less than %d bytes", formFieldLen)})
|
err := fmt.Errorf("client_name must be less than %d bytes", formFieldLen)
|
||||||
return
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
}
|
|
||||||
if len(form.Website) > formFieldLen {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("website must be less than %d bytes", formFieldLen)})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(form.RedirectURIs) > formRedirectLen {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("redirect_uris must be less than %d bytes", formRedirectLen)})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(form.Scopes) > formFieldLen {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("scopes must be less than %d bytes", formFieldLen)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiApp, err := m.processor.AppCreate(c.Request.Context(), authed, form)
|
if len(form.RedirectURIs) > formRedirectLen {
|
||||||
if err != nil {
|
err := fmt.Errorf("redirect_uris must be less than %d bytes", formRedirectLen)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(form.Scopes) > formFieldLen {
|
||||||
|
err := fmt.Errorf("scopes must be less than %d bytes", formFieldLen)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(form.Website) > formFieldLen {
|
||||||
|
err := fmt.Errorf("website must be less than %d bytes", formFieldLen)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
apiApp, errWithCode := m.processor.AppCreate(c.Request.Context(), authed, form)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,17 +66,17 @@
|
||||||
|
|
||||||
// Module implements the ClientAPIModule interface for
|
// Module implements the ClientAPIModule interface for
|
||||||
type Module struct {
|
type Module struct {
|
||||||
db db.DB
|
db db.DB
|
||||||
server oauth.Server
|
idp oidc.IDP
|
||||||
idp oidc.IDP
|
processor processing.Processor
|
||||||
}
|
}
|
||||||
|
|
||||||
// New returns a new auth module
|
// New returns a new auth module
|
||||||
func New(db db.DB, server oauth.Server, idp oidc.IDP) api.ClientModule {
|
func New(db db.DB, idp oidc.IDP, processor processing.Processor) api.ClientModule {
|
||||||
return &Module{
|
return &Module{
|
||||||
db: db,
|
db: db,
|
||||||
server: server,
|
idp: idp,
|
||||||
idp: idp,
|
processor: processor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,29 +19,42 @@
|
||||||
package auth_test
|
package auth_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
|
"codeberg.org/gruf/go-store/kv"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/memstore"
|
"github.com/gin-contrib/sessions/memstore"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/auth"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/messages"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/processing"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/router"
|
"github.com/superseriousbusiness/gotosocial/internal/router"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AuthStandardTestSuite struct {
|
type AuthStandardTestSuite struct {
|
||||||
suite.Suite
|
suite.Suite
|
||||||
db db.DB
|
db db.DB
|
||||||
idp oidc.IDP
|
storage *kv.KVStore
|
||||||
oauthServer oauth.Server
|
mediaManager media.Manager
|
||||||
|
federator federation.Federator
|
||||||
|
processor processing.Processor
|
||||||
|
emailSender email.Sender
|
||||||
|
idp oidc.IDP
|
||||||
|
oauthServer oauth.Server
|
||||||
|
|
||||||
// standard suite models
|
// standard suite models
|
||||||
testTokens map[string]*gtsmodel.Token
|
testTokens map[string]*gtsmodel.Token
|
||||||
|
@ -69,39 +82,53 @@ func (suite *AuthStandardTestSuite) SetupSuite() {
|
||||||
|
|
||||||
func (suite *AuthStandardTestSuite) SetupTest() {
|
func (suite *AuthStandardTestSuite) SetupTest() {
|
||||||
testrig.InitTestConfig()
|
testrig.InitTestConfig()
|
||||||
suite.db = testrig.NewTestDB()
|
|
||||||
testrig.InitTestLog()
|
testrig.InitTestLog()
|
||||||
|
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||||
|
|
||||||
|
suite.db = testrig.NewTestDB()
|
||||||
|
suite.storage = testrig.NewTestStorage()
|
||||||
|
suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage)
|
||||||
|
suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker)
|
||||||
|
suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil)
|
||||||
|
suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker)
|
||||||
|
|
||||||
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
suite.oauthServer = testrig.NewTestOauthServer(suite.db)
|
||||||
var err error
|
var err error
|
||||||
suite.idp, err = oidc.NewIDP(context.Background())
|
suite.idp, err = oidc.NewIDP(context.Background())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
suite.authModule = auth.New(suite.db, suite.oauthServer, suite.idp).(*auth.Module)
|
suite.authModule = auth.New(suite.db, suite.idp, suite.processor).(*auth.Module)
|
||||||
testrig.StandardDBSetup(suite.db, nil)
|
testrig.StandardDBSetup(suite.db, suite.testAccounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AuthStandardTestSuite) TearDownTest() {
|
func (suite *AuthStandardTestSuite) TearDownTest() {
|
||||||
testrig.StandardDBTeardown(suite.db)
|
testrig.StandardDBTeardown(suite.db)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string) (*gin.Context, *httptest.ResponseRecorder) {
|
func (suite *AuthStandardTestSuite) newContext(requestMethod string, requestPath string, requestBody []byte, bodyContentType string) (*gin.Context, *httptest.ResponseRecorder) {
|
||||||
// create the recorder and gin test context
|
// create the recorder and gin test context
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, engine := gin.CreateTestContext(recorder)
|
ctx, engine := gin.CreateTestContext(recorder)
|
||||||
|
|
||||||
// load templates into the engine
|
// load templates into the engine
|
||||||
testrig.ConfigureTemplatesWithGin(engine)
|
testrig.ConfigureTemplatesWithGin(engine, "../../../../web/template")
|
||||||
|
|
||||||
// create the request
|
// create the request
|
||||||
protocol := config.GetProtocol()
|
protocol := config.GetProtocol()
|
||||||
host := config.GetHost()
|
host := config.GetHost()
|
||||||
baseURI := fmt.Sprintf("%s://%s", protocol, host)
|
baseURI := fmt.Sprintf("%s://%s", protocol, host)
|
||||||
requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath)
|
requestURI := fmt.Sprintf("%s/%s", baseURI, requestPath)
|
||||||
ctx.Request = httptest.NewRequest(requestMethod, requestURI, nil) // the endpoint we're hitting
|
|
||||||
|
ctx.Request = httptest.NewRequest(requestMethod, requestURI, bytes.NewReader(requestBody)) // the endpoint we're hitting
|
||||||
ctx.Request.Header.Set("accept", "text/html")
|
ctx.Request.Header.Set("accept", "text/html")
|
||||||
|
|
||||||
|
if bodyContentType != "" {
|
||||||
|
ctx.Request.Header.Set("Content-Type", bodyContentType)
|
||||||
|
}
|
||||||
|
|
||||||
// trigger the session middleware on the context
|
// trigger the session middleware on the context
|
||||||
store := memstore.NewStore(make([]byte, 32), make([]byte, 32))
|
store := memstore.NewStore(make([]byte, 32), make([]byte, 32))
|
||||||
store.Options(router.SessionOptions())
|
store.Options(router.SessionOptions())
|
||||||
|
|
|
@ -23,9 +23,6 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
@ -33,18 +30,22 @@
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// helpfulAdvice is a handy hint to users;
|
||||||
|
// particularly important during the login flow
|
||||||
|
var helpfulAdvice = "If you arrived at this error during a login/oauth flow, please try clearing your session cookies and logging in again; if problems persist, make sure you're using the correct credentials"
|
||||||
|
|
||||||
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
|
// AuthorizeGETHandler should be served as GET at https://example.org/oauth/authorize
|
||||||
// The idea here is to present an oauth authorize page to the user, with a button
|
// The idea here is to present an oauth authorize page to the user, with a button
|
||||||
// that they have to click to accept.
|
// that they have to click to accept.
|
||||||
func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AuthorizeGETHandler")
|
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
||||||
c.HTML(http.StatusNotAcceptable, "error.tmpl", gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,56 +53,75 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
|
// If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page.
|
||||||
userID, ok := s.Get(sessionUserID).(string)
|
userID, ok := s.Get(sessionUserID).(string)
|
||||||
if !ok || userID == "" {
|
if !ok || userID == "" {
|
||||||
l.Trace("userid was empty, parsing form then redirecting to sign in page")
|
|
||||||
form := &model.OAuthAuthorize{}
|
form := &model.OAuthAuthorize{}
|
||||||
if err := c.Bind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("invalid auth form: %s", err)
|
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Debugf("parsed auth form: %+v", form)
|
|
||||||
|
|
||||||
if err := extractAuthForm(s, form); err != nil {
|
if errWithCode := saveAuthFormToSession(s, form); errWithCode != nil {
|
||||||
l.Debugf(fmt.Sprintf("error parsing form at /oauth/authorize: %s", err))
|
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusSeeOther, AuthSignInPath)
|
c.Redirect(http.StatusSeeOther, AuthSignInPath)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can use the client_id on the session to retrieve info about the app associated with the client_id
|
// use session information to validate app, user, and account for this request
|
||||||
clientID, ok := s.Get(sessionClientID).(string)
|
clientID, ok := s.Get(sessionClientID).(string)
|
||||||
if !ok || clientID == "" {
|
if !ok || clientID == "" {
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no client_id found in session"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
app := >smodel.Application{}
|
|
||||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{
|
err := fmt.Errorf("key %s was not found in session", sessionClientID)
|
||||||
"error": fmt.Sprintf("no application found for client id %s", clientID),
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
})
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app := >smodel.Application{}
|
||||||
|
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||||
|
m.clearSession(s)
|
||||||
|
safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
if err == db.ErrNoEntries {
|
||||||
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
|
|
||||||
// or thier account has been disabled.
|
|
||||||
user := >smodel.User{}
|
user := >smodel.User{}
|
||||||
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
if err == db.ErrNoEntries {
|
||||||
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
if err == db.ErrNoEntries {
|
||||||
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
|
||||||
|
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,25 +129,27 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
redirect, ok := s.Get(sessionRedirectURI).(string)
|
redirect, ok := s.Get(sessionRedirectURI).(string)
|
||||||
if !ok || redirect == "" {
|
if !ok || redirect == "" {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no redirect_uri found in session"})
|
err := fmt.Errorf("key %s was not found in session", sessionRedirectURI)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
scope, ok := s.Get(sessionScope).(string)
|
scope, ok := s.Get(sessionScope).(string)
|
||||||
if !ok || scope == "" {
|
if !ok || scope == "" {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": "no scope found in session"})
|
err := fmt.Errorf("key %s was not found in session", sessionScope)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// the authorize template will display a form to the user where they can get some information
|
// the authorize template will display a form to the user where they can get some information
|
||||||
// about the app that's trying to authorize, and the scope of the request.
|
// about the app that's trying to authorize, and the scope of the request.
|
||||||
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
|
// They can then approve it if it looks OK to them, which will POST to the AuthorizePOSTHandler
|
||||||
l.Trace("serving authorize html")
|
|
||||||
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
|
c.HTML(http.StatusOK, "authorize.tmpl", gin.H{
|
||||||
"appname": app.Name,
|
"appname": app.Name,
|
||||||
"appwebsite": app.Website,
|
"appwebsite": app.Website,
|
||||||
"redirect": redirect,
|
"redirect": redirect,
|
||||||
sessionScope: scope,
|
"scope": scope,
|
||||||
"user": acct.Username,
|
"user": acct.Username,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -136,13 +158,10 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) {
|
||||||
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
|
// At this point we assume that the user has A) logged in and B) accepted that the app should act for them,
|
||||||
// so we should proceed with the authentication flow and generate an oauth token for them if we can.
|
// so we should proceed with the authentication flow and generate an oauth token for them if we can.
|
||||||
func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "AuthorizePOSTHandler")
|
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
|
|
||||||
// We need to retrieve the original form submitted to the authorizeGEThandler, and
|
// We need to retrieve the original form submitted to the authorizeGEThandler, and
|
||||||
// recreate it on the request so that it can be used further by the oauth2 library.
|
// recreate it on the request so that it can be used further by the oauth2 library.
|
||||||
// So first fetch all the values from the session.
|
|
||||||
|
|
||||||
errs := []string{}
|
errs := []string{}
|
||||||
|
|
||||||
forceLogin, ok := s.Get(sessionForceLogin).(string)
|
forceLogin, ok := s.Get(sessionForceLogin).(string)
|
||||||
|
@ -152,77 +171,107 @@ func (m *Module) AuthorizePOSTHandler(c *gin.Context) {
|
||||||
|
|
||||||
responseType, ok := s.Get(sessionResponseType).(string)
|
responseType, ok := s.Get(sessionResponseType).(string)
|
||||||
if !ok || responseType == "" {
|
if !ok || responseType == "" {
|
||||||
errs = append(errs, "session missing response_type")
|
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionResponseType))
|
||||||
}
|
}
|
||||||
|
|
||||||
clientID, ok := s.Get(sessionClientID).(string)
|
clientID, ok := s.Get(sessionClientID).(string)
|
||||||
if !ok || clientID == "" {
|
if !ok || clientID == "" {
|
||||||
errs = append(errs, "session missing client_id")
|
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionClientID))
|
||||||
}
|
}
|
||||||
|
|
||||||
redirectURI, ok := s.Get(sessionRedirectURI).(string)
|
redirectURI, ok := s.Get(sessionRedirectURI).(string)
|
||||||
if !ok || redirectURI == "" {
|
if !ok || redirectURI == "" {
|
||||||
errs = append(errs, "session missing redirect_uri")
|
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionRedirectURI))
|
||||||
}
|
}
|
||||||
|
|
||||||
scope, ok := s.Get(sessionScope).(string)
|
scope, ok := s.Get(sessionScope).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
errs = append(errs, "session missing scope")
|
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionScope))
|
||||||
}
|
}
|
||||||
|
|
||||||
userID, ok := s.Get(sessionUserID).(string)
|
userID, ok := s.Get(sessionUserID).(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
errs = append(errs, "session missing userid")
|
errs = append(errs, fmt.Sprintf("key %s was not found in session", sessionUserID))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errs) != 0 {
|
||||||
|
errs = append(errs, helpfulAdvice)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(errors.New("one or more missing keys on session during AuthorizePOSTHandler"), errs...), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// redirect the user if they have not confirmed their email yet, thier account has not been approved yet,
|
|
||||||
// or thier account has been disabled.
|
|
||||||
user := >smodel.User{}
|
user := >smodel.User{}
|
||||||
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
if err := m.db.GetByID(c.Request.Context(), userID, user); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
safe := fmt.Sprintf("user with id %s could not be retrieved", userID)
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
if err == db.ErrNoEntries {
|
||||||
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
acct, err := m.db.GetAccountByID(c.Request.Context(), user.AccountID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.HTML(http.StatusInternalServerError, "error.tmpl", gin.H{"error": err.Error()})
|
safe := fmt.Sprintf("account with id %s could not be retrieved", user.AccountID)
|
||||||
return
|
var errWithCode gtserror.WithCode
|
||||||
}
|
if err == db.ErrNoEntries {
|
||||||
if !ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ensureUserIsAuthorizedOrRedirect(c, user, acct) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// we're done with the session now, so just clear it out
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
|
|
||||||
if len(errs) != 0 {
|
// we have to set the values on the request form
|
||||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": strings.Join(errs, ": ")})
|
// so that they're picked up by the oauth server
|
||||||
return
|
c.Request.Form = url.Values{
|
||||||
|
sessionForceLogin: {forceLogin},
|
||||||
|
sessionResponseType: {responseType},
|
||||||
|
sessionClientID: {clientID},
|
||||||
|
sessionRedirectURI: {redirectURI},
|
||||||
|
sessionScope: {scope},
|
||||||
|
sessionUserID: {userID},
|
||||||
}
|
}
|
||||||
|
|
||||||
// now set the values on the request
|
if err := m.processor.OAuthHandleAuthorizeRequest(c.Writer, c.Request); err != nil {
|
||||||
values := url.Values{}
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice), m.processor.InstanceGet)
|
||||||
values.Set(sessionForceLogin, forceLogin)
|
|
||||||
values.Set(sessionResponseType, responseType)
|
|
||||||
values.Set(sessionClientID, clientID)
|
|
||||||
values.Set(sessionRedirectURI, redirectURI)
|
|
||||||
values.Set(sessionScope, scope)
|
|
||||||
values.Set(sessionUserID, userID)
|
|
||||||
c.Request.Form = values
|
|
||||||
l.Tracef("values on request set to %+v", c.Request.Form)
|
|
||||||
|
|
||||||
// and proceed with authorization using the oauth2 library
|
|
||||||
if err := m.server.HandleAuthorizeRequest(c.Writer, c.Request); err != nil {
|
|
||||||
c.HTML(http.StatusBadRequest, "error.tmpl", gin.H{"error": err.Error()})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractAuthForm checks the given OAuthAuthorize form, and stores
|
// saveAuthFormToSession checks the given OAuthAuthorize form,
|
||||||
// the values in the form into the session.
|
// and stores the values in the form into the session.
|
||||||
func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
|
func saveAuthFormToSession(s sessions.Session, form *model.OAuthAuthorize) gtserror.WithCode {
|
||||||
// these fields are *required* so check 'em
|
if form == nil {
|
||||||
if form.ResponseType == "" || form.ClientID == "" || form.RedirectURI == "" {
|
err := errors.New("OAuthAuthorize form was nil")
|
||||||
return errors.New("missing one of: response_type, client_id or redirect_uri")
|
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.ResponseType == "" {
|
||||||
|
err := errors.New("field response_type was not set on OAuthAuthorize form")
|
||||||
|
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.ClientID == "" {
|
||||||
|
err := errors.New("field client_id was not set on OAuthAuthorize form")
|
||||||
|
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.RedirectURI == "" {
|
||||||
|
err := errors.New("field redirect_uri was not set on OAuthAuthorize form")
|
||||||
|
return gtserror.NewErrorBadRequest(err, err.Error(), helpfulAdvice)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set default scope to read
|
// set default scope to read
|
||||||
|
@ -237,29 +286,33 @@ func extractAuthForm(s sessions.Session, form *model.OAuthAuthorize) error {
|
||||||
s.Set(sessionRedirectURI, form.RedirectURI)
|
s.Set(sessionRedirectURI, form.RedirectURI)
|
||||||
s.Set(sessionScope, form.Scope)
|
s.Set(sessionScope, form.Scope)
|
||||||
s.Set(sessionState, uuid.NewString())
|
s.Set(sessionState, uuid.NewString())
|
||||||
return s.Save()
|
|
||||||
|
if err := s.Save(); err != nil {
|
||||||
|
err := fmt.Errorf("error saving form values onto session: %s", err)
|
||||||
|
return gtserror.NewErrorInternalError(err, helpfulAdvice)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) bool {
|
func ensureUserIsAuthorizedOrRedirect(ctx *gin.Context, user *gtsmodel.User, account *gtsmodel.Account) (redirected bool) {
|
||||||
if user.ConfirmedAt.IsZero() {
|
if user.ConfirmedAt.IsZero() {
|
||||||
ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath)
|
ctx.Redirect(http.StatusSeeOther, CheckYourEmailPath)
|
||||||
return false
|
redirected = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.Approved {
|
if !user.Approved {
|
||||||
ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath)
|
ctx.Redirect(http.StatusSeeOther, WaitForApprovalPath)
|
||||||
return false
|
redirected = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if user.Disabled {
|
if user.Disabled || !account.SuspendedAt.IsZero() {
|
||||||
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
|
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
|
||||||
return false
|
redirected = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !account.SuspendedAt.IsZero() {
|
return
|
||||||
ctx.Redirect(http.StatusSeeOther, AccountDisabledPath)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ func (suite *AuthAuthorizeTestSuite) TestAccountAuthorizeHandler() {
|
||||||
}
|
}
|
||||||
|
|
||||||
doTest := func(testCase authorizeHandlerTestCase) {
|
doTest := func(testCase authorizeHandlerTestCase) {
|
||||||
ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath)
|
ctx, recorder := suite.newContext(http.MethodGet, auth.OauthAuthorizePath, nil, "")
|
||||||
|
|
||||||
user := suite.testUsers["unconfirmed_account"]
|
user := suite.testUsers["unconfirmed_account"]
|
||||||
account := suite.testAccounts["unconfirmed_account"]
|
account := suite.testAccounts["unconfirmed_account"]
|
||||||
|
|
|
@ -30,7 +30,9 @@
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
"github.com/superseriousbusiness/gotosocial/internal/oidc"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
|
@ -40,11 +42,14 @@
|
||||||
func (m *Module) CallbackGETHandler(c *gin.Context) {
|
func (m *Module) CallbackGETHandler(c *gin.Context) {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
|
|
||||||
// first make sure the state set in the cookie is the same as the state returned from the external provider
|
// check the query vs session state parameter to mitigate csrf
|
||||||
|
// https://auth0.com/docs/secure/attack-protection/state-parameters
|
||||||
|
|
||||||
state := c.Query(callbackStateParam)
|
state := c.Query(callbackStateParam)
|
||||||
if state == "" {
|
if state == "" {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "state query not found on callback"})
|
err := fmt.Errorf("%s parameter not found on callback query", callbackStateParam)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,84 +57,104 @@ func (m *Module) CallbackGETHandler(c *gin.Context) {
|
||||||
savedState, ok := savedStateI.(string)
|
savedState, ok := savedStateI.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
|
err := fmt.Errorf("key %s was not found in session", sessionState)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if state != savedState {
|
if state != savedState {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "state mismatch"})
|
err := errors.New("mismatch between query state and session state")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// retrieve stored claims using code
|
||||||
code := c.Query(callbackCodeParam)
|
code := c.Query(callbackCodeParam)
|
||||||
|
if code == "" {
|
||||||
claims, err := m.idp.HandleCallback(c.Request.Context(), code)
|
|
||||||
if err != nil {
|
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
err := fmt.Errorf("%s parameter not found on callback query", callbackCodeParam)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can use the client_id on the session to retrieve info about the app associated with the client_id
|
claims, errWithCode := m.idp.HandleCallback(c.Request.Context(), code)
|
||||||
|
if errWithCode != nil {
|
||||||
|
m.clearSession(s)
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can use the client_id on the session to retrieve
|
||||||
|
// info about the app associated with the client_id
|
||||||
clientID, ok := s.Get(sessionClientID).(string)
|
clientID, ok := s.Get(sessionClientID).(string)
|
||||||
if !ok || clientID == "" {
|
if !ok || clientID == "" {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "no client_id found in session during callback"})
|
err := fmt.Errorf("key %s was not found in session", sessionClientID)
|
||||||
return
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
}
|
|
||||||
app := >smodel.Application{}
|
|
||||||
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
|
||||||
m.clearSession(s)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("no application found for client id %s", clientID)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user, err := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
|
app := >smodel.Application{}
|
||||||
if err != nil {
|
if err := m.db.GetWhere(c.Request.Context(), []db.Where{{Key: sessionClientID, Value: clientID}}, app); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
safe := fmt.Sprintf("application for %s %s could not be retrieved", sessionClientID, clientID)
|
||||||
|
var errWithCode gtserror.WithCode
|
||||||
|
if err == db.ErrNoEntries {
|
||||||
|
errWithCode = gtserror.NewErrorBadRequest(err, safe, helpfulAdvice)
|
||||||
|
} else {
|
||||||
|
errWithCode = gtserror.NewErrorInternalError(err, safe, helpfulAdvice)
|
||||||
|
}
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
user, errWithCode := m.parseUserFromClaims(c.Request.Context(), claims, net.IP(c.ClientIP()), app.ID)
|
||||||
|
if errWithCode != nil {
|
||||||
|
m.clearSession(s)
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Set(sessionUserID, user.ID)
|
s.Set(sessionUserID, user.ID)
|
||||||
if err := s.Save(); err != nil {
|
if err := s.Save(); err != nil {
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, error) {
|
func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, ip net.IP, appID string) (*gtsmodel.User, gtserror.WithCode) {
|
||||||
if claims.Email == "" {
|
if claims.Email == "" {
|
||||||
return nil, errors.New("no email returned in claims")
|
err := errors.New("no email returned in claims")
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// see if we already have a user for this email address
|
// see if we already have a user for this email address
|
||||||
|
// if so, we don't need to continue + create one
|
||||||
user := >smodel.User{}
|
user := >smodel.User{}
|
||||||
err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user)
|
err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: claims.Email}}, user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// we do! so we can just return it
|
|
||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != db.ErrNoEntries {
|
if err != db.ErrNoEntries {
|
||||||
// we have an actual error in the database
|
err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||||
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// maybe we have an unconfirmed user
|
// maybe we have an unconfirmed user
|
||||||
err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user)
|
err = m.db.GetWhere(ctx, []db.Where{{Key: "unconfirmed_email", Value: claims.Email}}, user)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// user is unconfirmed so return an error
|
err := fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
|
||||||
return nil, fmt.Errorf("user with email address %s is unconfirmed", claims.Email)
|
return nil, gtserror.NewErrorForbidden(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != db.ErrNoEntries {
|
if err != db.ErrNoEntries {
|
||||||
// we have an actual error in the database
|
err := fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
||||||
return nil, fmt.Errorf("error checking database for email %s: %s", claims.Email, err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// we don't have a confirmed or unconfirmed user with the claimed email address
|
// we don't have a confirmed or unconfirmed user with the claimed email address
|
||||||
|
@ -138,10 +163,10 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
// check if the email address is available for use; if it's not there's nothing we can so
|
// check if the email address is available for use; if it's not there's nothing we can so
|
||||||
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
|
emailAvailable, err := m.db.IsEmailAvailable(ctx, claims.Email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("email %s not available: %s", claims.Email, err)
|
return nil, gtserror.NewErrorBadRequest(err)
|
||||||
}
|
}
|
||||||
if !emailAvailable {
|
if !emailAvailable {
|
||||||
return nil, fmt.Errorf("email %s in use", claims.Email)
|
return nil, gtserror.NewErrorConflict(fmt.Errorf("email address %s is not available", claims.Email))
|
||||||
}
|
}
|
||||||
|
|
||||||
// now we need a username
|
// now we need a username
|
||||||
|
@ -149,12 +174,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
|
|
||||||
// make sure claims.Name is defined since we'll be using that for the username
|
// make sure claims.Name is defined since we'll be using that for the username
|
||||||
if claims.Name == "" {
|
if claims.Name == "" {
|
||||||
return nil, errors.New("no name returned in claims")
|
err := errors.New("no name returned in claims")
|
||||||
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we can just use claims.Name as-is
|
// check if we can just use claims.Name as-is
|
||||||
err = validate.Username(claims.Name)
|
if err = validate.Username(claims.Name); err == nil {
|
||||||
if err == nil {
|
|
||||||
// the name we have on the claims is already a valid username
|
// the name we have on the claims is already a valid username
|
||||||
username = claims.Name
|
username = claims.Name
|
||||||
} else {
|
} else {
|
||||||
|
@ -166,12 +191,12 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
// lowercase the whole thing
|
// lowercase the whole thing
|
||||||
lower := strings.ToLower(underscored)
|
lower := strings.ToLower(underscored)
|
||||||
// see if this is valid....
|
// see if this is valid....
|
||||||
if err := validate.Username(lower); err == nil {
|
if err := validate.Username(lower); err != nil {
|
||||||
// we managed to get a valid username
|
err := fmt.Errorf("couldn't parse a valid username from claims.Name value of %s: %s", claims.Name, err)
|
||||||
username = lower
|
return nil, gtserror.NewErrorBadRequest(err, err.Error())
|
||||||
} else {
|
|
||||||
return nil, fmt.Errorf("couldn't parse a valid username from claims.Name value of %s", claims.Name)
|
|
||||||
}
|
}
|
||||||
|
// we managed to get a valid username
|
||||||
|
username = lower
|
||||||
}
|
}
|
||||||
|
|
||||||
var iString string
|
var iString string
|
||||||
|
@ -185,7 +210,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
for i := 1; !found; i++ {
|
for i := 1; !found; i++ {
|
||||||
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
|
usernameAvailable, err := m.db.IsUsernameAvailable(ctx, username+iString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
if usernameAvailable {
|
if usernameAvailable {
|
||||||
// no error so we've found a username that works
|
// no error so we've found a username that works
|
||||||
|
@ -223,7 +248,7 @@ func (m *Module) parseUserFromClaims(ctx context.Context, claims *oidc.Claims, i
|
||||||
// create the user! this will also create an account and store it in the database so we don't need to do that here
|
// create the user! this will also create an account and store it in the database so we don't need to do that here
|
||||||
user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin)
|
user, err = m.db.NewSignup(ctx, username, "", requireApproval, claims.Email, password, ip, "", appID, emailVerified, admin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating user: %s", err)
|
return nil, gtserror.NewErrorInternalError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return user, nil
|
||||||
|
|
|
@ -21,14 +21,14 @@
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"golang.org/x/crypto/bcrypt"
|
"golang.org/x/crypto/bcrypt"
|
||||||
)
|
)
|
||||||
|
@ -41,64 +41,62 @@ type login struct {
|
||||||
|
|
||||||
// SignInGETHandler should be served at https://example.org/auth/sign_in.
|
// SignInGETHandler should be served at https://example.org/auth/sign_in.
|
||||||
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
||||||
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler
|
// The form will then POST to the sign in page, which will be handled by SignInPOSTHandler.
|
||||||
|
// If an idp provider is set, then the user will be redirected to that to do their sign in.
|
||||||
func (m *Module) SignInGETHandler(c *gin.Context) {
|
func (m *Module) SignInGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "SignInGETHandler")
|
|
||||||
l.Trace("entering sign in handler")
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if m.idp != nil {
|
if m.idp == nil {
|
||||||
s := sessions.Default(c)
|
// no idp provider, use our own funky little sign in page
|
||||||
|
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
|
||||||
stateI := s.Get(sessionState)
|
|
||||||
state, ok := stateI.(string)
|
|
||||||
if !ok {
|
|
||||||
m.clearSession(s)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "state not found in session"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
redirect := m.idp.AuthCodeURL(state)
|
|
||||||
l.Debugf("redirecting to external idp at %s", redirect)
|
|
||||||
c.Redirect(http.StatusSeeOther, redirect)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.HTML(http.StatusOK, "sign-in.tmpl", gin.H{})
|
|
||||||
|
// idp provider is in use, so redirect to it
|
||||||
|
s := sessions.Default(c)
|
||||||
|
|
||||||
|
stateI := s.Get(sessionState)
|
||||||
|
state, ok := stateI.(string)
|
||||||
|
if !ok {
|
||||||
|
m.clearSession(s)
|
||||||
|
err := fmt.Errorf("key %s was not found in session", sessionState)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusSeeOther, m.idp.AuthCodeURL(state))
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
|
// SignInPOSTHandler should be served at https://example.org/auth/sign_in.
|
||||||
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
// The idea is to present a sign in page to the user, where they can enter their username and password.
|
||||||
// The handler will then redirect to the auth handler served at /auth
|
// The handler will then redirect to the auth handler served at /auth
|
||||||
func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "SignInPOSTHandler")
|
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
|
|
||||||
form := &login{}
|
form := &login{}
|
||||||
if err := c.ShouldBind(form); err != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
m.clearSession(s)
|
m.clearSession(s)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Tracef("parsed form: %+v", form)
|
|
||||||
|
|
||||||
userid, err := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
|
userid, errWithCode := m.ValidatePassword(c.Request.Context(), form.Email, form.Password)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
c.String(http.StatusForbidden, err.Error())
|
// don't clear session here, so the user can just press back and try again
|
||||||
m.clearSession(s)
|
// if they accidentally gave the wrong password or something
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Set(sessionUserID, userid)
|
s.Set(sessionUserID, userid)
|
||||||
if err := s.Save(); err != nil {
|
if err := s.Save(); err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
err := fmt.Errorf("error saving user id onto session: %s", err)
|
||||||
m.clearSession(s)
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err, helpfulAdvice), m.processor.InstanceGet)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("redirecting to auth page")
|
|
||||||
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
c.Redirect(http.StatusFound, OauthAuthorizePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,42 +104,34 @@ func (m *Module) SignInPOSTHandler(c *gin.Context) {
|
||||||
// The goal is to authenticate the password against the one for that email
|
// The goal is to authenticate the password against the one for that email
|
||||||
// address stored in the database. If OK, we return the userid (a ulid) for that user,
|
// address stored in the database. If OK, we return the userid (a ulid) for that user,
|
||||||
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
|
// so that it can be used in further Oauth flows to generate a token/retreieve an oauth client from the db.
|
||||||
func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (userid string, err error) {
|
func (m *Module) ValidatePassword(ctx context.Context, email string, password string) (string, gtserror.WithCode) {
|
||||||
l := logrus.WithField("func", "ValidatePassword")
|
|
||||||
|
|
||||||
// make sure an email/password was provided and bail if not
|
|
||||||
if email == "" || password == "" {
|
if email == "" || password == "" {
|
||||||
l.Debug("email or password was not provided")
|
err := errors.New("email or password was not provided")
|
||||||
return incorrectPassword()
|
return incorrectPassword(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// first we select the user from the database based on email address, bail if no user found for that email
|
user := >smodel.User{}
|
||||||
gtsUser := >smodel.User{}
|
if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, user); err != nil {
|
||||||
|
err := fmt.Errorf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
||||||
if err := m.db.GetWhere(ctx, []db.Where{{Key: "email", Value: email}}, gtsUser); err != nil {
|
return incorrectPassword(err)
|
||||||
l.Debugf("user %s was not retrievable from db during oauth authorization attempt: %s", email, err)
|
|
||||||
return incorrectPassword()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure a password is actually set and bail if not
|
if user.EncryptedPassword == "" {
|
||||||
if gtsUser.EncryptedPassword == "" {
|
err := fmt.Errorf("encrypted password for user %s was empty for some reason", user.Email)
|
||||||
l.Warnf("encrypted password for user %s was empty for some reason", gtsUser.Email)
|
return incorrectPassword(err)
|
||||||
return incorrectPassword()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// compare the provided password with the encrypted one from the db, bail if they don't match
|
if err := bcrypt.CompareHashAndPassword([]byte(user.EncryptedPassword), []byte(password)); err != nil {
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(gtsUser.EncryptedPassword), []byte(password)); err != nil {
|
err := fmt.Errorf("password hash didn't match for user %s during login attempt: %s", user.Email, err)
|
||||||
l.Debugf("password hash didn't match for user %s during login attempt: %s", gtsUser.Email, err)
|
return incorrectPassword(err)
|
||||||
return incorrectPassword()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we've made it this far the email/password is correct, so we can just return the id of the user.
|
return user.ID, nil
|
||||||
userid = gtsUser.ID
|
|
||||||
l.Tracef("returning (%s, %s)", userid, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// incorrectPassword is just a little helper function to use in the ValidatePassword function
|
// incorrectPassword wraps the given error in a gtserror.WithCode, and returns
|
||||||
func incorrectPassword() (string, error) {
|
// only a generic 'safe' error message to the user, to not give any info away.
|
||||||
return "", errors.New("password/email combination was incorrect")
|
func incorrectPassword(err error) (string, gtserror.WithCode) {
|
||||||
|
safeErr := fmt.Errorf("password/email combination was incorrect")
|
||||||
|
return "", gtserror.NewErrorUnauthorized(err, safeErr.Error(), helpfulAdvice)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,56 +22,94 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type tokenBody struct {
|
type tokenRequestForm struct {
|
||||||
|
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
|
||||||
|
Code *string `form:"code" json:"code" xml:"code"`
|
||||||
|
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
|
||||||
ClientID *string `form:"client_id" json:"client_id" xml:"client_id"`
|
ClientID *string `form:"client_id" json:"client_id" xml:"client_id"`
|
||||||
ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"`
|
ClientSecret *string `form:"client_secret" json:"client_secret" xml:"client_secret"`
|
||||||
Code *string `form:"code" json:"code" xml:"code"`
|
|
||||||
GrantType *string `form:"grant_type" json:"grant_type" xml:"grant_type"`
|
|
||||||
RedirectURI *string `form:"redirect_uri" json:"redirect_uri" xml:"redirect_uri"`
|
|
||||||
Scope *string `form:"scope" json:"scope" xml:"scope"`
|
Scope *string `form:"scope" json:"scope" xml:"scope"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
|
// TokenPOSTHandler should be served as a POST at https://example.org/oauth/token
|
||||||
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
|
// The idea here is to serve an oauth access token to a user, which can be used for authorizing against non-public APIs.
|
||||||
func (m *Module) TokenPOSTHandler(c *gin.Context) {
|
func (m *Module) TokenPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "TokenPOSTHandler")
|
|
||||||
l.Trace("entered TokenPOSTHandler")
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form := &tokenBody{}
|
help := []string{}
|
||||||
if err := c.ShouldBind(form); err == nil {
|
|
||||||
c.Request.Form = url.Values{}
|
form := &tokenRequestForm{}
|
||||||
if form.ClientID != nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
c.Request.Form.Set("client_id", *form.ClientID)
|
api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), err.Error()))
|
||||||
}
|
return
|
||||||
if form.ClientSecret != nil {
|
|
||||||
c.Request.Form.Set("client_secret", *form.ClientSecret)
|
|
||||||
}
|
|
||||||
if form.Code != nil {
|
|
||||||
c.Request.Form.Set("code", *form.Code)
|
|
||||||
}
|
|
||||||
if form.GrantType != nil {
|
|
||||||
c.Request.Form.Set("grant_type", *form.GrantType)
|
|
||||||
}
|
|
||||||
if form.RedirectURI != nil {
|
|
||||||
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
|
|
||||||
}
|
|
||||||
if form.Scope != nil {
|
|
||||||
c.Request.Form.Set("scope", *form.Scope)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.server.HandleTokenRequest(c.Writer, c.Request); err != nil {
|
c.Request.Form = url.Values{}
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
|
var grantType string
|
||||||
|
if form.GrantType != nil {
|
||||||
|
grantType = *form.GrantType
|
||||||
|
c.Request.Form.Set("grant_type", grantType)
|
||||||
|
} else {
|
||||||
|
help = append(help, "grant_type was not set in the token request form, but must be set to authorization_code or client_credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if form.ClientID != nil {
|
||||||
|
c.Request.Form.Set("client_id", *form.ClientID)
|
||||||
|
} else {
|
||||||
|
help = append(help, "client_id was not set in the token request form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.ClientSecret != nil {
|
||||||
|
c.Request.Form.Set("client_secret", *form.ClientSecret)
|
||||||
|
} else {
|
||||||
|
help = append(help, "client_secret was not set in the token request form")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.RedirectURI != nil {
|
||||||
|
c.Request.Form.Set("redirect_uri", *form.RedirectURI)
|
||||||
|
} else {
|
||||||
|
help = append(help, "redirect_uri was not set in the token request form")
|
||||||
|
}
|
||||||
|
|
||||||
|
var code string
|
||||||
|
if form.Code != nil {
|
||||||
|
if grantType != "authorization_code" {
|
||||||
|
help = append(help, "a code was provided in the token request form, but grant_type was not set to authorization_code")
|
||||||
|
} else {
|
||||||
|
code = *form.Code
|
||||||
|
c.Request.Form.Set("code", code)
|
||||||
|
}
|
||||||
|
} else if grantType == "authorization_code" {
|
||||||
|
help = append(help, "code was not set in the token request form, but must be set since grant_type is authorization_code")
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.Scope != nil {
|
||||||
|
c.Request.Form.Set("scope", *form.Scope)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(help) != 0 {
|
||||||
|
api.OAuthErrorHandler(c, gtserror.NewErrorBadRequest(oauth.InvalidRequest(), help...))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
token, errWithCode := m.processor.OAuthHandleTokenRequest(c.Request)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.OAuthErrorHandler(c, errWithCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Cache-Control", "no-store")
|
||||||
|
c.Header("Pragma", "no-cache")
|
||||||
|
c.JSON(http.StatusOK, token)
|
||||||
}
|
}
|
||||||
|
|
215
internal/api/client/auth/token_test.go
Normal file
215
internal/api/client/auth/token_test.go
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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 auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenTestSuite struct {
|
||||||
|
AuthStandardTestSuite
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TokenTestSuite) TestPOSTTokenEmptyForm() {
|
||||||
|
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", []byte{}, "")
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.authModule.TokenPOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: grant_type was not set in the token request form, but must be set to authorization_code or client_credentials: client_id was not set in the token request form: client_secret was not set in the token request form: redirect_uri was not set in the token request form"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TokenTestSuite) TestRetrieveClientCredentialsOK() {
|
||||||
|
testClient := suite.testClients["local_account_1"]
|
||||||
|
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": testClient.ID,
|
||||||
|
"client_secret": testClient.Secret,
|
||||||
|
"redirect_uri": "http://localhost:8080",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.authModule.TokenPOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
t := &apimodel.Token{}
|
||||||
|
err = json.Unmarshal(b, t)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal("Bearer", t.TokenType)
|
||||||
|
suite.NotEmpty(t.AccessToken)
|
||||||
|
suite.NotEmpty(t.CreatedAt)
|
||||||
|
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
|
||||||
|
|
||||||
|
// there should be a token in the database now too
|
||||||
|
dbToken := >smodel.Token{}
|
||||||
|
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(dbToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeOK() {
|
||||||
|
testClient := suite.testClients["local_account_1"]
|
||||||
|
testUserAuthorizationToken := suite.testTokens["local_account_1_user_authorization_token"]
|
||||||
|
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"client_id": testClient.ID,
|
||||||
|
"client_secret": testClient.Secret,
|
||||||
|
"redirect_uri": "http://localhost:8080",
|
||||||
|
"code": testUserAuthorizationToken.Code,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.authModule.TokenPOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusOK, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
t := &apimodel.Token{}
|
||||||
|
err = json.Unmarshal(b, t)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal("Bearer", t.TokenType)
|
||||||
|
suite.NotEmpty(t.AccessToken)
|
||||||
|
suite.NotEmpty(t.CreatedAt)
|
||||||
|
suite.WithinDuration(time.Now(), time.Unix(t.CreatedAt, 0), 1*time.Minute)
|
||||||
|
|
||||||
|
dbToken := >smodel.Token{}
|
||||||
|
err = suite.db.GetWhere(context.Background(), []db.Where{{Key: "access", Value: t.AccessToken}}, dbToken)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(dbToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeNoCode() {
|
||||||
|
testClient := suite.testClients["local_account_1"]
|
||||||
|
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{
|
||||||
|
"grant_type": "authorization_code",
|
||||||
|
"client_id": testClient.ID,
|
||||||
|
"client_secret": testClient.Secret,
|
||||||
|
"redirect_uri": "http://localhost:8080",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.authModule.TokenPOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: code was not set in the token request form, but must be set since grant_type is authorization_code"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *TokenTestSuite) TestRetrieveAuthorizationCodeWrongGrantType() {
|
||||||
|
testClient := suite.testClients["local_account_1"]
|
||||||
|
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": testClient.ID,
|
||||||
|
"client_secret": testClient.Secret,
|
||||||
|
"redirect_uri": "http://localhost:8080",
|
||||||
|
"code": "peepeepoopoo",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
ctx, recorder := suite.newContext(http.MethodPost, "oauth/token", bodyBytes, w.FormDataContentType())
|
||||||
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
|
|
||||||
|
suite.authModule.TokenPOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"invalid_request","error_description":"Bad Request: a code was provided in the token request form, but grant_type was not set to authorization_code"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenTestSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &TokenTestSuite{})
|
||||||
|
}
|
|
@ -19,13 +19,13 @@
|
||||||
package blocks
|
package blocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,24 +80,25 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/account"
|
// "$ref": "#/definitions/account"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) BlocksGETHandler(c *gin.Context) {
|
func (m *Module) BlocksGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -118,8 +119,8 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -127,8 +128,7 @@ func (m *Module) BlocksGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit)
|
resp, errWithCode := m.processor.BlocksGet(c.Request.Context(), authed, maxID, sinceID, limit)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor BlocksGet: %s", errWithCode)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,18 +5,25 @@
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EmojisGETHandler returns a list of custom emojis enabled on the instance
|
// EmojisGETHandler returns a list of custom emojis enabled on the instance
|
||||||
func (m *Module) EmojisGETHandler(c *gin.Context) {
|
func (m *Module) EmojisGETHandler(c *gin.Context) {
|
||||||
|
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emojis, errWithCode := m.processor.CustomEmojisGet(c)
|
emojis, errWithCode := m.processor.CustomEmojisGet(c)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,29 +1,26 @@
|
||||||
package favourites
|
package favourites
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FavouritesGETHandler handles GETting favourites.
|
// FavouritesGETHandler handles GETting favourites.
|
||||||
func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,8 +41,8 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -53,13 +50,12 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
|
resp, errWithCode := m.processor.FavedTimelineGet(c.Request.Context(), authed, maxID, minID, limit)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor FavedTimelineGet: %s", errWithCode)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.LinkHeader != "" {
|
if resp.LinkHeader != "" {
|
||||||
c.Header("Link", resp.LinkHeader)
|
c.Header("Link", resp.LinkHeader)
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, resp.Statuses)
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
package fileserver
|
package fileserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -34,17 +36,9 @@
|
||||||
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
|
// Note: to mitigate scraping attempts, no information should be given out on a bad request except "404 page not found".
|
||||||
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
|
// Don't give away account ids or media ids or anything like that; callers shouldn't be able to infer anything.
|
||||||
func (m *FileServer) ServeFile(c *gin.Context) {
|
func (m *FileServer) ServeFile(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "ServeFile",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Trace("received request")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
authed, err := oauth.Authed(c, false, false, false, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.String(http.StatusNotFound, "404 page not found")
|
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,29 +47,29 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
||||||
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
|
// "FILE_NAME" consists of two parts, the attachment's database id, a period, and the file extension.
|
||||||
accountID := c.Param(AccountIDKey)
|
accountID := c.Param(AccountIDKey)
|
||||||
if accountID == "" {
|
if accountID == "" {
|
||||||
l.Debug("missing accountID from request")
|
err := fmt.Errorf("missing %s from request", AccountIDKey)
|
||||||
c.String(http.StatusNotFound, "404 page not found")
|
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaType := c.Param(MediaTypeKey)
|
mediaType := c.Param(MediaTypeKey)
|
||||||
if mediaType == "" {
|
if mediaType == "" {
|
||||||
l.Debug("missing mediaType from request")
|
err := fmt.Errorf("missing %s from request", MediaTypeKey)
|
||||||
c.String(http.StatusNotFound, "404 page not found")
|
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaSize := c.Param(MediaSizeKey)
|
mediaSize := c.Param(MediaSizeKey)
|
||||||
if mediaSize == "" {
|
if mediaSize == "" {
|
||||||
l.Debug("missing mediaSize from request")
|
err := fmt.Errorf("missing %s from request", MediaSizeKey)
|
||||||
c.String(http.StatusNotFound, "404 page not found")
|
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fileName := c.Param(FileNameKey)
|
fileName := c.Param(FileNameKey)
|
||||||
if fileName == "" {
|
if fileName == "" {
|
||||||
l.Debug("missing fileName from request")
|
err := fmt.Errorf("missing %s from request", FileNameKey)
|
||||||
c.String(http.StatusNotFound, "404 page not found")
|
api.ErrorHandler(c, gtserror.NewErrorNotFound(err), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,8 +80,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
||||||
FileName: fileName,
|
FileName: fileName,
|
||||||
})
|
})
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Errorf(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +88,7 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
||||||
// if the content is a ReadCloser, close it when we're done
|
// if the content is a ReadCloser, close it when we're done
|
||||||
if closer, ok := content.Content.(io.ReadCloser); ok {
|
if closer, ok := content.Content.(io.ReadCloser); ok {
|
||||||
if err := closer.Close(); err != nil {
|
if err := closer.Close(); err != nil {
|
||||||
l.Errorf("error closing readcloser: %s", err)
|
logrus.Errorf("ServeFile: error closing readcloser: %s", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -103,9 +96,9 @@ func (m *FileServer) ServeFile(c *gin.Context) {
|
||||||
// TODO: if the requester only accepts text/html we should try to serve them *something*.
|
// TODO: if the requester only accepts text/html we should try to serve them *something*.
|
||||||
// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will
|
// This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will
|
||||||
// attempt to look up the content to provide a preview of the link, and they ask for text/html.
|
// attempt to look up the content to provide a preview of the link, and they ask for text/html.
|
||||||
format, err := api.NegotiateAccept(c, api.Offer(content.ContentType))
|
format, err := api.NegotiateAccept(c, api.MIME(content.ContentType))
|
||||||
if errWithCode != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,19 @@
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FiltersGETHandler returns a list of filters set by/for the authed account
|
// FiltersGETHandler returns a list of filters set by/for the authed account
|
||||||
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
func (m *Module) FiltersGETHandler(c *gin.Context) {
|
||||||
|
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
package followrequest
|
package followrequest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,43 +62,34 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal server error
|
// description: internal server error
|
||||||
func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
|
func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "FollowRequestAuthorizePOSTHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
|
||||||
l.Debugf("couldn't auth: %s", err)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originAccountID := c.Param(IDKey)
|
originAccountID := c.Param(IDKey)
|
||||||
if originAccountID == "" {
|
if originAccountID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID)
|
relationship, errWithCode := m.processor.FollowRequestAccept(c.Request.Context(), authed, originAccountID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debug(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -82,6 +82,34 @@ func (suite *AuthorizeTestSuite) TestAuthorize() {
|
||||||
suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b))
|
suite.Equal(`{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","following":false,"showing_reblogs":false,"notifying":false,"followed_by":true,"blocking":false,"blocked_by":false,"muting":false,"muting_notifications":false,"requested":false,"domain_blocking":false,"endorsed":false,"note":""}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *AuthorizeTestSuite) TestAuthorizeNoFR() {
|
||||||
|
requestingAccount := suite.testAccounts["remote_account_2"]
|
||||||
|
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx := suite.newContext(recorder, http.MethodPost, []byte{}, fmt.Sprintf("/api/v1/follow_requests/%s/authorize", requestingAccount.ID), "")
|
||||||
|
|
||||||
|
ctx.Params = gin.Params{
|
||||||
|
gin.Param{
|
||||||
|
Key: followrequest.IDKey,
|
||||||
|
Value: requestingAccount.ID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
suite.followRequestModule.FollowRequestAuthorizePOSTHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusNotFound, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
// check the response
|
||||||
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Not Found"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
func TestAuthorizeTestSuite(t *testing.T) {
|
func TestAuthorizeTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &AuthorizeTestSuite{})
|
suite.Run(t, &AuthorizeTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,10 +21,9 @@
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -71,34 +70,27 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) FollowRequestGETHandler(c *gin.Context) {
|
func (m *Module) FollowRequestGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "FollowRequestGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
|
||||||
l.Debugf("couldn't auth: %s", err)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed)
|
accts, errWithCode := m.processor.FollowRequestsGet(c.Request.Context(), authed)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -70,7 +70,7 @@ func (suite *GetTestSuite) TestGet() {
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
|
|
||||||
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":true,"bot":false,"created_at":"2020-08-10T12:13:28.00Z","note":"i'm a real son of a gun","url":"http://example.org/@some_user","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
|
suite.Equal(`[{"id":"01FHMQX3GAABWSM0S2VZEC2SWC","username":"some_user","acct":"some_user@example.org","display_name":"some user","locked":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":"","header_static":"","followers_count":0,"following_count":0,"statuses_count":0,"last_status_at":"","emojis":[],"fields":[]}]`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetTestSuite(t *testing.T) {
|
func TestGetTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package followrequest
|
package followrequest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,43 +60,34 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal server error
|
// description: internal server error
|
||||||
func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) {
|
func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "FollowRequestRejectPOSTHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
|
||||||
l.Debugf("couldn't auth: %s", err)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originAccountID := c.Param(IDKey)
|
originAccountID := c.Param(IDKey)
|
||||||
if originAccountID == "" {
|
if originAccountID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no follow request origin account id provided"})
|
err := errors.New("no account id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID)
|
relationship, errWithCode := m.processor.FollowRequestReject(c.Request.Context(), authed, originAccountID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debug(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,9 @@
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
@ -30,22 +30,19 @@
|
||||||
// description: "Instance information."
|
// description: "Instance information."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/instance"
|
// "$ref": "#/definitions/instance"
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal error
|
// description: internal error
|
||||||
func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
|
func (m *Module) InstanceInformationGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "InstanceInformationGETHandler")
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
host := config.GetHost()
|
instance, errWithCode := m.processor.InstanceGet(c.Request.Context(), config.GetHost())
|
||||||
|
if errWithCode != nil {
|
||||||
instance, err := m.processor.InstanceGet(c.Request.Context(), host)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
if err != nil {
|
|
||||||
l.Debugf("error getting instance from processor: %s", err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
package instance
|
package instance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -82,52 +82,51 @@
|
||||||
// description: "The newly updated instance."
|
// description: "The newly updated instance."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/instance"
|
// "$ref": "#/definitions/instance"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "InstanceUpdatePATCHHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// only admins can update instance settings
|
|
||||||
if !authed.User.Admin {
|
if !authed.User.Admin {
|
||||||
l.Debug("user is not an admin so cannot update instance settings")
|
err := errors.New("user is not an admin so cannot update instance settings")
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not an admin"})
|
api.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("parsing request form")
|
|
||||||
form := &model.InstanceSettingsUpdateRequest{}
|
form := &model.InstanceSettingsUpdateRequest{}
|
||||||
if err := c.ShouldBind(&form); err != nil || form == nil {
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
l.Debugf("could not parse form from request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debugf("parsed form: %+v", form)
|
|
||||||
|
|
||||||
// if everything on the form is nil, then nothing has been set and we shouldn't continue
|
|
||||||
if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil {
|
if form.Title == nil && form.ContactUsername == nil && form.ContactEmail == nil && form.ShortDescription == nil && form.Description == nil && form.Terms == nil && form.Avatar == nil && form.Header == nil {
|
||||||
l.Debugf("could not parse form from request")
|
err := errors.New("empty form submitted")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "empty form submitted"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
|
i, errWithCode := m.processor.InstancePatch(c.Request.Context(), form)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error with instance patch request: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
|
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
"github.com/superseriousbusiness/gotosocial/internal/api/client/instance"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/testrig"
|
"github.com/superseriousbusiness/gotosocial/testrig"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,7 +63,7 @@ func (suite *InstancePatchTestSuite) TestInstancePatch1() {
|
||||||
b, err := io.ReadAll(result.Body)
|
b, err := io.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.00Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.00Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
|
suite.Equal(`{"uri":"http://localhost:8080","title":"Example Instance","description":"","short_description":"","email":"someone@example.org","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","contact_account":{"id":"01F8MH17FWEB39HZJ76B6VXSKF","username":"admin","acct":"admin","display_name":"","locked":false,"bot":false,"created_at":"2022-05-17T13:10:59.000Z","note":"","url":"http://localhost:8080/@admin","avatar":"","avatar_static":"","header":"","header_static":"","followers_count":1,"following_count":1,"statuses_count":4,"last_status_at":"2021-10-20T10:41:37.000Z","emojis":[],"fields":[]},"max_toot_chars":5000}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
func (suite *InstancePatchTestSuite) TestInstancePatch2() {
|
||||||
|
@ -125,6 +126,67 @@ func (suite *InstancePatchTestSuite) TestInstancePatch3() {
|
||||||
suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b))
|
suite.Equal(`{"uri":"http://localhost:8080","title":"localhost:8080","description":"","short_description":"\u003cp\u003eThis is some html, which is \u003cem\u003eallowed\u003c/em\u003e in short descriptions.\u003c/p\u003e","email":"","version":"","registrations":true,"approval_required":true,"invites_enabled":false,"urls":{"streaming_api":"wss://localhost:8080"},"stats":{"domain_count":0,"status_count":16,"user_count":4},"thumbnail":"","max_toot_chars":5000}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *InstancePatchTestSuite) TestInstancePatch4() {
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
// set up the request
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Bad Request: empty form submitted"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *InstancePatchTestSuite) TestInstancePatch5() {
|
||||||
|
requestBody, w, err := testrig.CreateMultipartFormData(
|
||||||
|
"", "",
|
||||||
|
map[string]string{
|
||||||
|
"short_description": "<p>This is some html, which is <em>allowed</em> in short descriptions.</p>",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
bodyBytes := requestBody.Bytes()
|
||||||
|
|
||||||
|
// set up the request
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx := suite.newContext(recorder, http.MethodPatch, bodyBytes, instance.InstanceInformationPath, w.FormDataContentType())
|
||||||
|
|
||||||
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedToken, oauth.DBTokenToToken(suite.testTokens["local_account_1"]))
|
||||||
|
ctx.Set(oauth.SessionAuthorizedApplication, suite.testApplications["application_1"])
|
||||||
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
|
|
||||||
|
// call the handler
|
||||||
|
suite.instanceModule.InstanceUpdatePATCHHandler(ctx)
|
||||||
|
|
||||||
|
suite.Equal(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
|
result := recorder.Result()
|
||||||
|
defer result.Body.Close()
|
||||||
|
|
||||||
|
b, err := io.ReadAll(result.Body)
|
||||||
|
suite.NoError(err)
|
||||||
|
|
||||||
|
suite.Equal(`{"error":"Forbidden: user is not an admin so cannot update instance settings"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
func TestInstancePatchTestSuite(t *testing.T) {
|
func TestInstancePatchTestSuite(t *testing.T) {
|
||||||
suite.Run(t, &InstancePatchTestSuite{})
|
suite.Run(t, &InstancePatchTestSuite{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,12 +5,19 @@
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ListsGETHandler returns a list of lists created by/for the authed account
|
// ListsGETHandler returns a list of lists created by/for the authed account
|
||||||
func (m *Module) ListsGETHandler(c *gin.Context) {
|
func (m *Module) ListsGETHandler(c *gin.Context) {
|
||||||
|
if _, err := oauth.Authed(c, true, true, true, true); err != nil {
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,11 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -80,46 +79,36 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
|
||||||
// description: forbidden
|
|
||||||
// '422':
|
// '422':
|
||||||
// description: unprocessable
|
// description: unprocessable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) MediaCreatePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "statusCreatePOSTHandler")
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
authed, err := oauth.Authed(c, true, true, true, true) // posting new media is serious business so we want *everything*
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the media create form from the request context
|
|
||||||
l.Tracef("parsing request form: %s", c.Request.Form)
|
|
||||||
form := &model.AttachmentRequest{}
|
form := &model.AttachmentRequest{}
|
||||||
if err := c.ShouldBind(&form); err != nil {
|
if err := c.ShouldBind(&form); err != nil {
|
||||||
l.Debugf("error parsing form: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Errorf("could not parse form: %s", err)})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
|
||||||
l.Tracef("validating form %+v", form)
|
|
||||||
if err := validateCreateMedia(form); err != nil {
|
if err := validateCreateMedia(form); err != nil {
|
||||||
l.Debugf("error validating form: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Debug("calling processor media create func")
|
apiAttachment, errWithCode := m.processor.MediaCreate(c.Request.Context(), authed, form)
|
||||||
apiAttachment, err := m.processor.MediaCreate(c.Request.Context(), authed, form)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error creating attachment: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,6 +132,7 @@ func validateCreateMedia(form *model.AttachmentRequest) error {
|
||||||
if maxImageSize > maxSize {
|
if maxImageSize > maxSize {
|
||||||
maxSize = maxImageSize
|
maxSize = maxImageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
if form.File.Size > int64(maxSize) {
|
if form.File.Size > int64(maxSize) {
|
||||||
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
|
return fmt.Errorf("file size limit exceeded: limit is %d bytes but attachment was %d bytes", maxSize, form.File.Size)
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,15 +247,14 @@ func (suite *MediaCreateTestSuite) TestMediaCreateLongDescription() {
|
||||||
suite.mediaModule.MediaCreatePOSTHandler(ctx)
|
suite.mediaModule.MediaCreatePOSTHandler(ctx)
|
||||||
|
|
||||||
// check response
|
// check response
|
||||||
suite.EqualValues(http.StatusUnprocessableEntity, recorder.Code)
|
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
||||||
|
|
||||||
result := recorder.Result()
|
result := recorder.Result()
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
expectedErr := fmt.Sprintf(`{"error":"image description length must be between 0 and 500 characters (inclusive), but provided image description was %d chars"}`, len(description))
|
suite.Equal(`{"error":"Bad Request: image description length must be between 0 and 500 characters (inclusive), but provided image description was 6667 chars"}`, string(b))
|
||||||
suite.Equal(expectedErr, string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {
|
func (suite *MediaCreateTestSuite) TestMediaCreateTooShortDescription() {
|
||||||
|
|
|
@ -19,12 +19,12 @@
|
||||||
package media
|
package media
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -59,33 +59,34 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
// '404':
|
||||||
// description: forbidden
|
// description: not found
|
||||||
// '422':
|
// '406':
|
||||||
// description: unprocessable
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) MediaGETHandler(c *gin.Context) {
|
func (m *Module) MediaGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "MediaGETHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentID := c.Param(IDKey)
|
attachmentID := c.Param(IDKey)
|
||||||
if attachmentID == "" {
|
if attachmentID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
|
err := errors.New("no attachment id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID)
|
attachment, errWithCode := m.processor.MediaGet(c.Request.Context(), authed, attachmentID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,11 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,50 +88,45 @@
|
||||||
// description: bad request
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
// '404':
|
||||||
// description: forbidden
|
// description: not found
|
||||||
// '422':
|
// '406':
|
||||||
// description: unprocessable
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) MediaPUTHandler(c *gin.Context) {
|
func (m *Module) MediaPUTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "MediaGETHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
attachmentID := c.Param(IDKey)
|
attachmentID := c.Param(IDKey)
|
||||||
if attachmentID == "" {
|
if attachmentID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"})
|
err := errors.New("no attachment id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// extract the media update form from the request context
|
form := &model.AttachmentUpdateRequest{}
|
||||||
l.Tracef("parsing request form: %s", c.Request.Form)
|
if err := c.ShouldBind(form); err != nil {
|
||||||
var form model.AttachmentUpdateRequest
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
if err := c.ShouldBind(&form); err != nil {
|
|
||||||
l.Debugf("could not parse form from request: %s", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
if err := validateUpdateMedia(form); err != nil {
|
||||||
l.Tracef("validating form %+v", form)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
if err := validateUpdateMedia(&form); err != nil {
|
|
||||||
l.Debugf("error validating form: %s", err)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, &form)
|
attachment, errWithCode := m.processor.MediaUpdate(c.Request.Context(), authed, attachmentID, form)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -232,7 +232,7 @@ func (suite *MediaUpdateTestSuite) TestUpdateImageShortDescription() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
|
|
||||||
// reply should be an error message
|
// reply should be an error message
|
||||||
suite.Equal(`{"error":"image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: image description length must be between 50 and 500 characters (inclusive), but provided image description was 16 chars"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMediaUpdateTestSuite(t *testing.T) {
|
func TestMediaUpdateTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -19,34 +19,26 @@
|
||||||
package notification
|
package notification
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
|
// NotificationsGETHandler serves a list of notifications to the caller, with the desired query parameters
|
||||||
func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
"func": "NotificationsGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error authing status faved by request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,8 +47,8 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -74,12 +66,14 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) {
|
||||||
sinceID = sinceIDString
|
sinceID = sinceIDString
|
||||||
}
|
}
|
||||||
|
|
||||||
notifs, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
|
resp, errWithCode := m.processor.NotificationsGet(c.Request.Context(), authed, limit, maxID, sinceID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing notifications get: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, notifs)
|
if resp.LinkHeader != "" {
|
||||||
|
c.Header("Link", resp.LinkHeader)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,14 +19,15 @@
|
||||||
package search
|
package search
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -52,50 +53,44 @@
|
||||||
// type: array
|
// type: array
|
||||||
// items:
|
// items:
|
||||||
// "$ref": "#/definitions/searchResult"
|
// "$ref": "#/definitions/searchResult"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '404':
|
||||||
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) SearchGETHandler(c *gin.Context) {
|
func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
"func": "SearchGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error authing search request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accountID := c.Query(AccountIDKey)
|
|
||||||
maxID := c.Query(MaxIDKey)
|
|
||||||
minID := c.Query(MinIDKey)
|
|
||||||
searchType := c.Query(TypeKey)
|
|
||||||
|
|
||||||
excludeUnreviewed := false
|
excludeUnreviewed := false
|
||||||
excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
|
excludeUnreviewedString := c.Query(ExcludeUnreviewedKey)
|
||||||
if excludeUnreviewedString != "" {
|
if excludeUnreviewedString != "" {
|
||||||
var err error
|
var err error
|
||||||
excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
|
excludeUnreviewed, err = strconv.ParseBool(excludeUnreviewedString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", excludeUnreviewedString, err)})
|
err := fmt.Errorf("error parsing %s: %s", ExcludeUnreviewedKey, err)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
query := c.Query(QueryKey)
|
query := c.Query(QueryKey)
|
||||||
if query == "" {
|
if query == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "query parameter q was empty"})
|
err := errors.New("query parameter q was empty")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,18 +100,19 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||||
var err error
|
var err error
|
||||||
resolve, err = strconv.ParseBool(resolveString)
|
resolve, err = strconv.ParseBool(resolveString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", resolveString, err)})
|
err := fmt.Errorf("error parsing %s: %s", ResolveKey, err)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
limit := 20
|
limit := 2
|
||||||
limitString := c.Query(LimitKey)
|
limitString := c.Query(LimitKey)
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -133,18 +129,12 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||||
if offsetString != "" {
|
if offsetString != "" {
|
||||||
i, err := strconv.ParseInt(offsetString, 10, 64)
|
i, err := strconv.ParseInt(offsetString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing offset string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", OffsetKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse offset query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
offset = int(i)
|
offset = int(i)
|
||||||
}
|
}
|
||||||
if limit > 40 {
|
|
||||||
limit = 40
|
|
||||||
}
|
|
||||||
if limit < 1 {
|
|
||||||
limit = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
following := false
|
following := false
|
||||||
followingString := c.Query(FollowingKey)
|
followingString := c.Query(FollowingKey)
|
||||||
|
@ -152,16 +142,17 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||||
var err error
|
var err error
|
||||||
following, err = strconv.ParseBool(followingString)
|
following, err = strconv.ParseBool(followingString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("couldn't parse param %s: %s", followingString, err)})
|
err := fmt.Errorf("error parsing %s: %s", FollowingKey, err)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
searchQuery := &model.SearchQuery{
|
searchQuery := &model.SearchQuery{
|
||||||
AccountID: accountID,
|
AccountID: c.Query(AccountIDKey),
|
||||||
MaxID: maxID,
|
MaxID: c.Query(MaxIDKey),
|
||||||
MinID: minID,
|
MinID: c.Query(MinIDKey),
|
||||||
Type: searchType,
|
Type: c.Query(TypeKey),
|
||||||
ExcludeUnreviewed: excludeUnreviewed,
|
ExcludeUnreviewed: excludeUnreviewed,
|
||||||
Query: query,
|
Query: query,
|
||||||
Resolve: resolve,
|
Resolve: resolve,
|
||||||
|
@ -172,8 +163,7 @@ func (m *Module) SearchGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
|
results, errWithCode := m.processor.SearchGet(c.Request.Context(), authed, searchQuery)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error searching: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -66,37 +67,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
|
func (m *Module) StatusBoostPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusBoostPOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("not authed so can't boost status")
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusBoost(c.Request.Context(), authed, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status boost: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -134,13 +134,13 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() {
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||||
|
|
||||||
// check response
|
// check response
|
||||||
suite.EqualValues(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
|
suite.Equal(http.StatusForbidden, recorder.Code) // we 403 unboostable statuses
|
||||||
|
|
||||||
result := recorder.Result()
|
result := recorder.Result()
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), `{"error":"forbidden"}`, string(b))
|
assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
// try to boost a status that's not visible to the user
|
// try to boost a status that's not visible to the user
|
||||||
|
@ -177,13 +177,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() {
|
||||||
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
suite.statusModule.StatusBoostPOSTHandler(ctx)
|
||||||
|
|
||||||
// check response
|
// check response
|
||||||
suite.EqualValues(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
suite.Equal(http.StatusNotFound, recorder.Code) // we 404 statuses that aren't visible
|
||||||
|
|
||||||
result := recorder.Result()
|
|
||||||
defer result.Body.Close()
|
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
|
||||||
assert.NoError(suite.T(), err)
|
|
||||||
assert.Equal(suite.T(), `{"error":"404 not found"}`, string(b))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusBoostTestSuite(t *testing.T) {
|
func TestStatusBoostTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -84,10 +85,9 @@ func (m *Module) StatusBoostedByGETHandler(c *gin.Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiAccounts, err := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
|
apiAccounts, errWithCode := m.processor.StatusBoostedBy(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status boosted by request: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,37 +66,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusContextGETHandler(c *gin.Context) {
|
func (m *Module) StatusContextGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusContextGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error authing status context request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID)
|
statusContext, errWithCode := m.processor.StatusGetContext(c.Request.Context(), authed, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error getting status context: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -23,12 +23,11 @@
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
"github.com/superseriousbusiness/gotosocial/internal/validate"
|
||||||
)
|
)
|
||||||
|
@ -61,58 +60,44 @@
|
||||||
// description: "The newly created status."
|
// description: "The newly created status."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/status"
|
// "$ref": "#/definitions/status"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal error
|
// description: internal server error
|
||||||
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "statusCreatePOSTHandler")
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("couldn't auth: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// First check this user/account is permitted to post new statuses.
|
|
||||||
// There's no point continuing otherwise.
|
|
||||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
|
||||||
l.Debugf("couldn't auth: %s", err)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// extract the status create form from the request context
|
|
||||||
l.Debugf("parsing request form: %s", c.Request.Form)
|
|
||||||
form := &model.AdvancedStatusCreateForm{}
|
form := &model.AdvancedStatusCreateForm{}
|
||||||
if err := c.ShouldBind(form); err != nil || form == nil {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
l.Debugf("could not parse form from request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Debugf("handling status request form: %+v", form)
|
|
||||||
|
|
||||||
// Give the fields on the request form a first pass to make sure the request is superficially valid.
|
|
||||||
l.Tracef("validating form %+v", form)
|
|
||||||
if err := validateCreateStatus(form); err != nil {
|
if err := validateCreateStatus(form); err != nil {
|
||||||
l.Debugf("error validating form: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := m.processor.StatusCreate(c.Request.Context(), authed, form)
|
apiStatus, errWithCode := m.processor.StatusCreate(c.Request.Context(), authed, form)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status create: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -120,7 +105,6 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
||||||
// validate that, structurally, we have a valid status/post
|
|
||||||
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
|
if form.Status == "" && form.MediaIDs == nil && form.Poll == nil {
|
||||||
return errors.New("no status, media, or poll provided")
|
return errors.New("no status, media, or poll provided")
|
||||||
}
|
}
|
||||||
|
@ -135,19 +119,16 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
||||||
maxPollChars := config.GetStatusesPollOptionMaxChars()
|
maxPollChars := config.GetStatusesPollOptionMaxChars()
|
||||||
maxCwChars := config.GetStatusesCWMaxChars()
|
maxCwChars := config.GetStatusesCWMaxChars()
|
||||||
|
|
||||||
// validate status
|
|
||||||
if form.Status != "" {
|
if form.Status != "" {
|
||||||
if len(form.Status) > maxChars {
|
if len(form.Status) > maxChars {
|
||||||
return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars)
|
return fmt.Errorf("status too long, %d characters provided but limit is %d", len(form.Status), maxChars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate media attachments
|
|
||||||
if len(form.MediaIDs) > maxMediaFiles {
|
if len(form.MediaIDs) > maxMediaFiles {
|
||||||
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
|
return fmt.Errorf("too many media files attached to status, %d attached but limit is %d", len(form.MediaIDs), maxMediaFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate poll
|
|
||||||
if form.Poll != nil {
|
if form.Poll != nil {
|
||||||
if form.Poll.Options == nil {
|
if form.Poll.Options == nil {
|
||||||
return errors.New("poll with no options")
|
return errors.New("poll with no options")
|
||||||
|
@ -162,14 +143,12 @@ func validateCreateStatus(form *model.AdvancedStatusCreateForm) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate spoiler text/cw
|
|
||||||
if form.SpoilerText != "" {
|
if form.SpoilerText != "" {
|
||||||
if len(form.SpoilerText) > maxCwChars {
|
if len(form.SpoilerText) > maxCwChars {
|
||||||
return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars)
|
return fmt.Errorf("content-warning/spoilertext too long, %d characters provided but limit is %d", len(form.SpoilerText), maxCwChars)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate post language
|
|
||||||
if form.Language != "" {
|
if form.Language != "" {
|
||||||
if err := validate.Language(form.Language); err != nil {
|
if err := validate.Language(form.Language); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -256,7 +256,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(`{"error":"bad request"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: status with id 3759e7ef-8ee1-4c0c-86f6-8b70b9ad3d50 not replyable because it doesn't exist"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post a reply to the status of a local user that allows replies.
|
// Post a reply to the status of a local user that allows replies.
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,43 +66,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusDELETEHandler(c *gin.Context) {
|
func (m *Module) StatusDELETEHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusDELETEHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("not authed so can't delete status")
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusDelete(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status delete: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// the status was already gone/never existed
|
|
||||||
if apiStatus == nil {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "Record not found"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,37 +63,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
|
func (m *Module) StatusFavePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusFavePOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("not authed so can't fave status")
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusFave(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status fave: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -118,13 +118,13 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() {
|
||||||
suite.statusModule.StatusFavePOSTHandler(ctx)
|
suite.statusModule.StatusFavePOSTHandler(ctx)
|
||||||
|
|
||||||
// check response
|
// check response
|
||||||
suite.EqualValues(http.StatusBadRequest, recorder.Code)
|
suite.EqualValues(http.StatusForbidden, recorder.Code)
|
||||||
|
|
||||||
result := recorder.Result()
|
result := recorder.Result()
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
assert.NoError(suite.T(), err)
|
assert.NoError(suite.T(), err)
|
||||||
assert.Equal(suite.T(), `{"error":"bad request"}`, string(b))
|
assert.Equal(suite.T(), `{"error":"Forbidden"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestStatusFaveTestSuite(t *testing.T) {
|
func TestStatusFaveTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,37 +64,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
|
func (m *Module) StatusFavedByGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
"func": "statusGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error authing status faved by request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiAccounts, err := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
|
apiAccounts, errWithCode := m.processor.StatusFavedBy(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status faved by request: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,45 +55,40 @@
|
||||||
// description: "The requested created status."
|
// description: "The requested created status."
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/status"
|
// "$ref": "#/definitions/status"
|
||||||
// '401':
|
|
||||||
// description: unauthorized
|
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
|
// '401':
|
||||||
|
// description: unauthorized
|
||||||
|
// '403':
|
||||||
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: internal error
|
// description: internal server error
|
||||||
func (m *Module) StatusGETHandler(c *gin.Context) {
|
func (m *Module) StatusGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
"func": "statusGETHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, false, false, false, false)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorf("error authing status faved by request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusGet(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status get: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -63,37 +64,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
|
func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusUnboostPOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("not authed so can't unboost status")
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusUnboost(c.Request.Context(), authed, targetStatusID)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status unboost: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,11 +19,12 @@
|
||||||
package status
|
package status
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -62,37 +63,32 @@
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '404':
|
// '404':
|
||||||
// description: not found
|
// description: not found
|
||||||
|
// '406':
|
||||||
|
// description: not acceptable
|
||||||
|
// '500':
|
||||||
|
// description: internal server error
|
||||||
func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
|
func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "StatusUnfavePOSTHandler",
|
|
||||||
"request_uri": c.Request.RequestURI,
|
|
||||||
"user_agent": c.Request.UserAgent(),
|
|
||||||
"origin_ip": c.ClientIP(),
|
|
||||||
})
|
|
||||||
l.Debugf("entering function")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debug("not authed so can't unfave status")
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
targetStatusID := c.Param(IDKey)
|
targetStatusID := c.Param(IDKey)
|
||||||
if targetStatusID == "" {
|
if targetStatusID == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"})
|
err := errors.New("no status id specified")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
apiStatus, err := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
|
apiStatus, errWithCode := m.processor.StatusUnfave(c.Request.Context(), authed, targetStatusID)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error processing status unfave: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,14 +2,24 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var wsUpgrader = websocket.Upgrader{
|
||||||
|
ReadBufferSize: 1024,
|
||||||
|
WriteBufferSize: 1024,
|
||||||
|
// we expect cors requests (via eg., pinafore.social) so be lenient
|
||||||
|
CheckOrigin: func(r *http.Request) bool { return true },
|
||||||
|
}
|
||||||
|
|
||||||
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
|
// StreamGETHandler swagger:operation GET /api/v1/streaming streamGet
|
||||||
//
|
//
|
||||||
// Initiate a websocket connection for live streaming of statuses and notifications.
|
// Initiate a websocket connection for live streaming of statuses and notifications.
|
||||||
|
@ -108,79 +118,78 @@
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
func (m *Module) StreamGETHandler(c *gin.Context) {
|
func (m *Module) StreamGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "StreamGETHandler")
|
|
||||||
|
|
||||||
streamType := c.Query(StreamQueryKey)
|
streamType := c.Query(StreamQueryKey)
|
||||||
if streamType == "" {
|
if streamType == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("no stream type provided under query key %s", StreamQueryKey)})
|
err := fmt.Errorf("no stream type provided under query key %s", StreamQueryKey)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
accessToken := c.Query(AccessTokenQueryKey)
|
accessToken := c.Query(AccessTokenQueryKey)
|
||||||
if accessToken == "" {
|
if accessToken == "" {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("no access token provided under query key %s", AccessTokenQueryKey)})
|
err := fmt.Errorf("no access token provided under query key %s", AccessTokenQueryKey)
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure a valid token has been provided and obtain the associated account
|
account, errWithCode := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
|
||||||
account, err := m.processor.AuthorizeStreamingRequest(c.Request.Context(), accessToken)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "could not authorize with given token"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare to upgrade the connection to a websocket connection
|
|
||||||
upgrader := websocket.Upgrader{
|
|
||||||
ReadBufferSize: 1024,
|
|
||||||
WriteBufferSize: 1024,
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
|
||||||
// we fully expect cors requests (via something like pinafore.social) so we should be lenient here
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// do the actual upgrade here
|
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
|
||||||
if err != nil {
|
|
||||||
l.Infof("error upgrading websocket connection: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer conn.Close() // whatever happens, when we leave this function we want to close the websocket connection
|
|
||||||
|
|
||||||
// inform the processor that we have a new connection and want a s for it
|
|
||||||
s, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
|
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
c.JSON(errWithCode.Code(), errWithCode.Safe())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer close(s.Hangup) // closing stream.Hangup indicates that we've finished with the connection (the client has gone), so we want to do this on exiting this handler
|
|
||||||
|
|
||||||
// spawn a new ticker for pinging the connection periodically
|
stream, errWithCode := m.processor.OpenStreamForAccount(c.Request.Context(), account, streamType)
|
||||||
t := time.NewTicker(30 * time.Second)
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// we want to stay in the sendloop as long as possible while the client is connected -- the only thing that should break the loop is if the client leaves or something else goes wrong
|
l := logrus.WithFields(logrus.Fields{
|
||||||
sendLoop:
|
"account": account.Username,
|
||||||
|
"path": BasePath,
|
||||||
|
"streamID": stream.ID,
|
||||||
|
"streamType": streamType,
|
||||||
|
})
|
||||||
|
|
||||||
|
wsConn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||||
|
if err != nil {
|
||||||
|
// If the upgrade fails, then Upgrade replies to the client with an HTTP error response.
|
||||||
|
// Because websocket issues are a pretty common source of headaches, we should also log
|
||||||
|
// this at Error to make this plenty visible and help admins out a bit.
|
||||||
|
l.Errorf("error upgrading websocket connection: %s", err)
|
||||||
|
close(stream.Hangup)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// cleanup
|
||||||
|
wsConn.Close()
|
||||||
|
close(stream.Hangup)
|
||||||
|
}()
|
||||||
|
|
||||||
|
streamTicker := time.NewTicker(30 * time.Second)
|
||||||
|
|
||||||
|
// We want to stay in the loop as long as possible while the client is connected.
|
||||||
|
// The only thing that should break the loop is if the client leaves or the connection becomes unhealthy.
|
||||||
|
//
|
||||||
|
// If the loop does break, we expect the client to reattempt connection, so it's cheap to leave + try again
|
||||||
|
wsLoop:
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case m := <-s.Messages:
|
case m := <-stream.Messages:
|
||||||
// we've got a streaming message!!
|
|
||||||
l.Trace("received message from stream")
|
l.Trace("received message from stream")
|
||||||
if err := conn.WriteJSON(m); err != nil {
|
if err := wsConn.WriteJSON(m); err != nil {
|
||||||
l.Debugf("error writing json to websocket connection: %s", err)
|
l.Debugf("error writing json to websocket connection; breaking off: %s", err)
|
||||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
break wsLoop
|
||||||
break sendLoop
|
|
||||||
}
|
}
|
||||||
l.Trace("wrote message into websocket connection")
|
l.Trace("wrote message into websocket connection")
|
||||||
case <-t.C:
|
case <-streamTicker.C:
|
||||||
l.Trace("received TICK from ticker")
|
l.Trace("received TICK from ticker")
|
||||||
if err := conn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
if err := wsConn.WriteMessage(websocket.PingMessage, []byte(": ping")); err != nil {
|
||||||
l.Debugf("error writing ping to websocket connection: %s", err)
|
l.Debugf("error writing ping to websocket connection; breaking off: %s", err)
|
||||||
// if something is wrong we want to bail and drop the connection -- the client will create a new one
|
break wsLoop
|
||||||
break sendLoop
|
|
||||||
}
|
}
|
||||||
l.Trace("wrote ping message into websocket connection")
|
l.Trace("wrote ping message into websocket connection")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l.Trace("leaving StreamGETHandler")
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,17 +105,14 @@
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "HomeTimelineGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +139,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -154,8 +151,8 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||||
if localString != "" {
|
if localString != "" {
|
||||||
i, err := strconv.ParseBool(localString)
|
i, err := strconv.ParseBool(localString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing local string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
local = i
|
local = i
|
||||||
|
@ -163,13 +160,12 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
resp, errWithCode := m.processor.HomeTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor HomeTimelineGet: %s", errWithCode)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.LinkHeader != "" {
|
if resp.LinkHeader != "" {
|
||||||
c.Header("Link", resp.LinkHeader)
|
c.Header("Link", resp.LinkHeader)
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, resp.Statuses)
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,13 +19,13 @@
|
||||||
package timeline
|
package timeline
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -105,17 +105,14 @@
|
||||||
// '400':
|
// '400':
|
||||||
// description: bad request
|
// description: bad request
|
||||||
func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "PublicTimelineGETHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -142,8 +139,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
||||||
if limitString != "" {
|
if limitString != "" {
|
||||||
i, err := strconv.ParseInt(limitString, 10, 64)
|
i, err := strconv.ParseInt(limitString, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing limit string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LimitKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse limit query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
limit = int(i)
|
limit = int(i)
|
||||||
|
@ -154,8 +151,8 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
||||||
if localString != "" {
|
if localString != "" {
|
||||||
i, err := strconv.ParseBool(localString)
|
i, err := strconv.ParseBool(localString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error parsing local string: %s", err)
|
err := fmt.Errorf("error parsing %s: %s", LocalKey, err)
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse local query param"})
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
local = i
|
local = i
|
||||||
|
@ -163,13 +160,12 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) {
|
||||||
|
|
||||||
resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
resp, errWithCode := m.processor.PublicTimelineGet(c.Request.Context(), authed, maxID, sinceID, minID, limit, local)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error from processor PublicTimelineGet: %s", errWithCode)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if resp.LinkHeader != "" {
|
if resp.LinkHeader != "" {
|
||||||
c.Header("Link", resp.LinkHeader)
|
c.Header("Link", resp.LinkHeader)
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, resp.Statuses)
|
c.JSON(http.StatusOK, resp.Items)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,12 +19,13 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
"github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -54,48 +55,48 @@
|
||||||
// responses:
|
// responses:
|
||||||
// '200':
|
// '200':
|
||||||
// description: Change successful
|
// description: Change successful
|
||||||
|
// '400':
|
||||||
|
// description: bad request
|
||||||
// '401':
|
// '401':
|
||||||
// description: unauthorized
|
// description: unauthorized
|
||||||
// '403':
|
// '403':
|
||||||
// description: forbidden
|
// description: forbidden
|
||||||
// '400':
|
// '406':
|
||||||
// description: bad request
|
// description: not acceptable
|
||||||
// '500':
|
// '500':
|
||||||
// description: "internal error"
|
// description: internal error
|
||||||
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
|
func (m *Module) PasswordChangePOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithField("func", "PasswordChangePOSTHandler")
|
|
||||||
|
|
||||||
authed, err := oauth.Authed(c, true, true, true, true)
|
authed, err := oauth.Authed(c, true, true, true, true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error authing: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGet)
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// First check this user/account is active.
|
|
||||||
if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() {
|
|
||||||
l.Debugf("couldn't auth: %s", err)
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
form := &model.PasswordChangeRequest{}
|
form := &model.PasswordChangeRequest{}
|
||||||
if err := c.ShouldBind(form); err != nil || form == nil || form.NewPassword == "" || form.OldPassword == "" {
|
if err := c.ShouldBind(form); err != nil {
|
||||||
if err != nil {
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
l.Debugf("could not parse form from request: %s", err)
|
return
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "missing one or more required form values"})
|
|
||||||
|
if form.OldPassword == "" {
|
||||||
|
err := errors.New("password change request missing field old_password")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if form.NewPassword == "" {
|
||||||
|
err := errors.New("password change request missing field new_password")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
|
if errWithCode := m.processor.UserChangePassword(c.Request.Context(), authed, form); errWithCode != nil {
|
||||||
l.Debugf("error changing user password: %s", errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,7 +49,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() {
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
ctx.Request.Form = url.Values{
|
ctx.Request.Form = url.Values{
|
||||||
"old_password": {"password"},
|
"old_password": {"password"},
|
||||||
|
@ -83,7 +83,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
ctx.Request.Form = url.Values{
|
ctx.Request.Form = url.Values{
|
||||||
"new_password": {"peepeepoopoopassword"},
|
"new_password": {"peepeepoopoopassword"},
|
||||||
|
@ -97,7 +97,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(`{"error":"missing one or more required form values"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: password change request missing field old_password"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||||
|
@ -110,7 +110,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
ctx.Request.Form = url.Values{
|
ctx.Request.Form = url.Values{
|
||||||
"old_password": {"notright"},
|
"old_password": {"notright"},
|
||||||
|
@ -125,7 +125,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(`{"error":"bad request: old password did not match"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: old password did not match"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||||
|
@ -138,7 +138,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||||
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
ctx.Set(oauth.SessionAuthorizedToken, oauthToken)
|
||||||
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"])
|
||||||
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"])
|
||||||
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil)
|
ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", user.PasswordChangePath), nil)
|
||||||
ctx.Request.Header.Set("accept", "application/json")
|
ctx.Request.Header.Set("accept", "application/json")
|
||||||
ctx.Request.Form = url.Values{
|
ctx.Request.Form = url.Values{
|
||||||
"old_password": {"password"},
|
"old_password": {"password"},
|
||||||
|
@ -153,7 +153,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() {
|
||||||
defer result.Body.Close()
|
defer result.Body.Close()
|
||||||
b, err := ioutil.ReadAll(result.Body)
|
b, err := ioutil.ReadAll(result.Body)
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.Equal(`{"error":"bad request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
suite.Equal(`{"error":"Bad Request: password is 94% strength, try including more special characters, using uppercase letters, using numbers or using a longer password"}`, string(b))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPasswordChangeTestSuite(t *testing.T) {
|
func TestPasswordChangeTestSuite(t *testing.T) {
|
||||||
|
|
|
@ -56,8 +56,8 @@ type UserStandardTestSuite struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *UserStandardTestSuite) SetupTest() {
|
func (suite *UserStandardTestSuite) SetupTest() {
|
||||||
testrig.InitTestLog()
|
|
||||||
testrig.InitTestConfig()
|
testrig.InitTestConfig()
|
||||||
|
testrig.InitTestLog()
|
||||||
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1)
|
||||||
suite.testTokens = testrig.NewTestTokens()
|
suite.testTokens = testrig.NewTestTokens()
|
||||||
|
|
154
internal/api/errorhandling.go
Normal file
154
internal/api/errorhandling.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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 api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: add more templated html pages here for different error types
|
||||||
|
|
||||||
|
// NotFoundHandler serves a 404 html page through the provided gin context,
|
||||||
|
// if accept is 'text/html', or just returns a json error if 'accept' is empty
|
||||||
|
// or application/json.
|
||||||
|
//
|
||||||
|
// When serving html, NotFoundHandler calls the provided InstanceGet function
|
||||||
|
// to fetch the apimodel representation of the instance, for serving in the
|
||||||
|
// 404 header and footer.
|
||||||
|
//
|
||||||
|
// If an error is returned by InstanceGet, the function will panic.
|
||||||
|
func NotFoundHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string) {
|
||||||
|
switch accept {
|
||||||
|
case string(TextHTML):
|
||||||
|
host := config.GetHost()
|
||||||
|
instance, err := instanceGet(c.Request.Context(), host)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(http.StatusNotFound, "404.tmpl", gin.H{
|
||||||
|
"instance": instance,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": http.StatusText(http.StatusNotFound)})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// genericErrorHandler is a more general version of the NotFoundHandler, which can
|
||||||
|
// be used for serving either generic error pages with some rendered help text,
|
||||||
|
// or just some error json if the caller prefers (or has no preference).
|
||||||
|
func genericErrorHandler(c *gin.Context, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode), accept string, errWithCode gtserror.WithCode) {
|
||||||
|
switch accept {
|
||||||
|
case string(TextHTML):
|
||||||
|
host := config.GetHost()
|
||||||
|
instance, err := instanceGet(c.Request.Context(), host)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.HTML(errWithCode.Code(), "error.tmpl", gin.H{
|
||||||
|
"instance": instance,
|
||||||
|
"code": errWithCode.Code(),
|
||||||
|
"error": errWithCode.Safe(),
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrorHandler takes the provided gin context and errWithCode and tries to serve
|
||||||
|
// a helpful error to the caller. It will do content negotiation to figure out if
|
||||||
|
// the caller prefers to see an html page with the error rendered there. If not, or
|
||||||
|
// if something goes wrong during the function, it will recover and just try to serve
|
||||||
|
// an appropriate application/json content-type error.
|
||||||
|
func ErrorHandler(c *gin.Context, errWithCode gtserror.WithCode, instanceGet func(ctx context.Context, domain string) (*apimodel.Instance, gtserror.WithCode)) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if raw := c.Request.URL.RawQuery; raw != "" {
|
||||||
|
path = path + "?" + raw
|
||||||
|
}
|
||||||
|
|
||||||
|
l := logrus.WithFields(logrus.Fields{
|
||||||
|
"path": path,
|
||||||
|
"error": errWithCode.Error(),
|
||||||
|
})
|
||||||
|
|
||||||
|
statusCode := errWithCode.Code()
|
||||||
|
|
||||||
|
if statusCode == http.StatusInternalServerError {
|
||||||
|
l.Error("Internal Server Error")
|
||||||
|
} else {
|
||||||
|
l.Debug("handling error")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if we panic for any reason during error handling,
|
||||||
|
// we should still try to return a basic code
|
||||||
|
defer func() {
|
||||||
|
if p := recover(); p != nil {
|
||||||
|
l.Warnf("recovered from panic: %s", p)
|
||||||
|
c.JSON(statusCode, gin.H{"error": errWithCode.Safe()})
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// discover if we're allowed to serve a nice html error page,
|
||||||
|
// or if we should just use a json. Normally we would want to
|
||||||
|
// check for a returned error, but if an error occurs here we
|
||||||
|
// can just fall back to default behavior (serve json error).
|
||||||
|
accept, _ := NegotiateAccept(c, HTMLOrJSONAcceptHeaders...)
|
||||||
|
|
||||||
|
if statusCode == http.StatusNotFound {
|
||||||
|
// use our special not found handler with useful status text
|
||||||
|
NotFoundHandler(c, instanceGet, accept)
|
||||||
|
} else {
|
||||||
|
genericErrorHandler(c, instanceGet, accept, errWithCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OAuthErrorHandler is a lot like ErrorHandler, but it specifically returns errors
|
||||||
|
// that are compatible with https://datatracker.ietf.org/doc/html/rfc6749#section-5.2,
|
||||||
|
// but serializing errWithCode.Error() in the 'error' field, and putting any help text
|
||||||
|
// from the error in the 'error_description' field. This means you should be careful not
|
||||||
|
// to pass any detailed errors (that might contain sensitive information) into the
|
||||||
|
// errWithCode.Error() field, since the client will see this. Use your noggin!
|
||||||
|
func OAuthErrorHandler(c *gin.Context, errWithCode gtserror.WithCode) {
|
||||||
|
l := logrus.WithFields(logrus.Fields{
|
||||||
|
"path": c.Request.URL.Path,
|
||||||
|
"error": errWithCode.Error(),
|
||||||
|
"help": errWithCode.Safe(),
|
||||||
|
})
|
||||||
|
|
||||||
|
statusCode := errWithCode.Code()
|
||||||
|
|
||||||
|
if statusCode == http.StatusInternalServerError {
|
||||||
|
l.Error("Internal Server Error")
|
||||||
|
} else {
|
||||||
|
l.Debug("handling OAuth error")
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(statusCode, gin.H{
|
||||||
|
"error": errWithCode.Error(),
|
||||||
|
"error_description": errWithCode.Safe(),
|
||||||
|
})
|
||||||
|
}
|
34
internal/api/mime.go
Normal file
34
internal/api/mime.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
/*
|
||||||
|
GoToSocial
|
||||||
|
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
|
||||||
|
|
||||||
|
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 api
|
||||||
|
|
||||||
|
// MIME represents a mime-type.
|
||||||
|
type MIME string
|
||||||
|
|
||||||
|
// MIME type
|
||||||
|
const (
|
||||||
|
AppJSON MIME = `application/json`
|
||||||
|
AppXML MIME = `application/xml`
|
||||||
|
AppActivityJSON MIME = `application/activity+json`
|
||||||
|
AppActivityLDJSON MIME = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`
|
||||||
|
AppForm MIME = `application/x-www-form-urlencoded`
|
||||||
|
MultipartForm MIME = `multipart/form-data`
|
||||||
|
TextXML MIME = `text/xml`
|
||||||
|
TextHTML MIME = `text/html`
|
||||||
|
)
|
|
@ -43,3 +43,24 @@ type Notification struct {
|
||||||
// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.
|
// Status that was the object of the notification, e.g. in mentions, reblogs, favourites, or polls.
|
||||||
Status *Status `json:"status,omitempty"`
|
Status *Status `json:"status,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The below functions are added onto the apimodel notification so that it satisfies
|
||||||
|
the Timelineable interface in internal/timeline.
|
||||||
|
*/
|
||||||
|
|
||||||
|
func (n *Notification) GetID() string {
|
||||||
|
return n.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notification) GetAccountID() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notification) GetBoostOfID() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *Notification) GetBoostOfAccountID() string {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
|
@ -18,9 +18,11 @@
|
||||||
|
|
||||||
package model
|
package model
|
||||||
|
|
||||||
// StatusTimelineResponse wraps a slice of statuses, ready to be serialized, along with the Link
|
import "github.com/superseriousbusiness/gotosocial/internal/timeline"
|
||||||
|
|
||||||
|
// TimelineResponse wraps a slice of timelineables, ready to be serialized, along with the Link
|
||||||
// header for the previous and next queries, to be returned to the client.
|
// header for the previous and next queries, to be returned to the client.
|
||||||
type StatusTimelineResponse struct {
|
type TimelineResponse struct {
|
||||||
Statuses []*Status
|
Items []timeline.Timelineable
|
||||||
LinkHeader string
|
LinkHeader string
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,33 +25,40 @@
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Offer represents an offered mime-type.
|
|
||||||
type Offer string
|
|
||||||
|
|
||||||
const (
|
|
||||||
AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'.
|
|
||||||
AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'.
|
|
||||||
AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
|
||||||
TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'.
|
|
||||||
)
|
|
||||||
|
|
||||||
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
|
||||||
// https://www.w3.org/TR/activitypub/#retrieving-objects
|
//
|
||||||
var ActivityPubAcceptHeaders = []Offer{
|
var ActivityPubAcceptHeaders = []MIME{
|
||||||
AppActivityJSON,
|
AppActivityJSON,
|
||||||
AppActivityLDJSON,
|
AppActivityLDJSON,
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONAcceptHeaders is a slice of offers that just contains application/json types.
|
// JSONAcceptHeaders is a slice of offers that just contains application/json types.
|
||||||
var JSONAcceptHeaders = []Offer{
|
var JSONAcceptHeaders = []MIME{
|
||||||
|
AppJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
// HTMLOrJSONAcceptHeaders is a slice of offers that prefers TextHTML and will
|
||||||
|
// fall back to JSON if necessary. This is useful for error handling, since it can
|
||||||
|
// be used to serve a nice HTML page if the caller accepts that, or just JSON if not.
|
||||||
|
var HTMLOrJSONAcceptHeaders = []MIME{
|
||||||
|
TextHTML,
|
||||||
AppJSON,
|
AppJSON,
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTMLAcceptHeaders is a slice of offers that just contains text/html types.
|
// HTMLAcceptHeaders is a slice of offers that just contains text/html types.
|
||||||
var HTMLAcceptHeaders = []Offer{
|
var HTMLAcceptHeaders = []MIME{
|
||||||
TextHTML,
|
TextHTML,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTMLOrActivityPubHeaders matches text/html first, then activitypub types.
|
||||||
|
// This is useful for user URLs that a user might go to in their browser.
|
||||||
|
// https://www.w3.org/TR/activitypub/#retrieving-objects
|
||||||
|
var HTMLOrActivityPubHeaders = []MIME{
|
||||||
|
TextHTML,
|
||||||
|
AppActivityJSON,
|
||||||
|
AppActivityLDJSON,
|
||||||
|
}
|
||||||
|
|
||||||
// NegotiateAccept takes the *gin.Context from an incoming request, and a
|
// NegotiateAccept takes the *gin.Context from an incoming request, and a
|
||||||
// slice of Offers, and performs content negotiation for the given request
|
// slice of Offers, and performs content negotiation for the given request
|
||||||
// with the given content-type offers. It will return a string representation
|
// with the given content-type offers. It will return a string representation
|
||||||
|
@ -73,7 +80,7 @@
|
||||||
// often-used Accept types.
|
// often-used Accept types.
|
||||||
//
|
//
|
||||||
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation
|
// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation
|
||||||
func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) {
|
func NegotiateAccept(c *gin.Context, offers ...MIME) (string, error) {
|
||||||
if len(offers) == 0 {
|
if len(offers) == 0 {
|
||||||
return "", errors.New("no format offered")
|
return "", errors.New("no format offered")
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,8 +23,8 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet
|
// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet
|
||||||
|
@ -45,27 +45,22 @@
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/nodeinfo"
|
// "$ref": "#/definitions/nodeinfo"
|
||||||
func (m *Module) NodeInfoGETHandler(c *gin.Context) {
|
func (m *Module) NodeInfoGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "NodeInfoGETHandler",
|
|
||||||
"user-agent": c.Request.UserAgent(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
|
ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request)
|
||||||
|
if errWithCode != nil {
|
||||||
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := json.Marshal(ni)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Debugf("error with get node info request: %s", err)
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||||
c.JSON(err.Code(), err.Safe())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, jsonErr := json.Marshal(ni)
|
|
||||||
if jsonErr != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()})
|
|
||||||
}
|
|
||||||
|
|
||||||
c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b)
|
c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b)
|
||||||
}
|
}
|
||||||
|
|
|
@ -22,8 +22,8 @@
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet
|
// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet
|
||||||
|
@ -45,19 +45,14 @@
|
||||||
// schema:
|
// schema:
|
||||||
// "$ref": "#/definitions/wellKnownResponse"
|
// "$ref": "#/definitions/wellKnownResponse"
|
||||||
func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) {
|
func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
|
||||||
"func": "NodeInfoWellKnownGETHandler",
|
|
||||||
})
|
|
||||||
|
|
||||||
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
|
niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request)
|
||||||
if err != nil {
|
if errWithCode != nil {
|
||||||
l.Debugf("error with get node info rel request: %s", err)
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(err.Code(), err.Safe())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,48 +20,45 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it.
|
||||||
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
func (m *Module) FollowersGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
// usernames on our instance are always lowercase
|
||||||
"func": "FollowersGETHandler",
|
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||||
"url": c.Request.RequestURI,
|
|
||||||
})
|
|
||||||
|
|
||||||
requestedUsername := c.Param(UsernameKey)
|
|
||||||
if requestedUsername == "" {
|
if requestedUsername == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
err := errors.New("no username specified in request")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
|
format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Tracef("negotiated format: %s", format)
|
|
||||||
|
|
||||||
ctx := transferContext(c)
|
if format == string(api.TextHTML) {
|
||||||
|
// redirect to the user's profile
|
||||||
|
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||||
|
}
|
||||||
|
|
||||||
followers, errWithCode := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL)
|
resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Info(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, mErr := json.Marshal(followers)
|
b, err := json.Marshal(resp)
|
||||||
if mErr != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("could not marshal json: %s", mErr)
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||||
l.Error(err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,48 +20,45 @@
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/api"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it.
|
||||||
func (m *Module) FollowingGETHandler(c *gin.Context) {
|
func (m *Module) FollowingGETHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
// usernames on our instance are always lowercase
|
||||||
"func": "FollowingGETHandler",
|
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||||
"url": c.Request.RequestURI,
|
|
||||||
})
|
|
||||||
|
|
||||||
requestedUsername := c.Param(UsernameKey)
|
|
||||||
if requestedUsername == "" {
|
if requestedUsername == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
err := errors.New("no username specified in request")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...)
|
format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
|
api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
l.Tracef("negotiated format: %s", format)
|
|
||||||
|
|
||||||
ctx := transferContext(c)
|
if format == string(api.TextHTML) {
|
||||||
|
// redirect to the user's profile
|
||||||
|
c.Redirect(http.StatusSeeOther, "/@"+requestedUsername)
|
||||||
|
}
|
||||||
|
|
||||||
following, errWithCode := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL)
|
resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL)
|
||||||
if errWithCode != nil {
|
if errWithCode != nil {
|
||||||
l.Info(errWithCode.Error())
|
api.ErrorHandler(c, errWithCode, m.processor.InstanceGet)
|
||||||
c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
b, mErr := json.Marshal(following)
|
b, err := json.Marshal(resp)
|
||||||
if mErr != nil {
|
if err != nil {
|
||||||
err := fmt.Errorf("could not marshal json: %s", mErr)
|
api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet)
|
||||||
l.Error(err)
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -19,43 +19,33 @@
|
||||||
package user
|
package user
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/superseriousbusiness/gotosocial/internal/api"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck
|
||||||
)
|
)
|
||||||
|
|
||||||
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
// InboxPOSTHandler deals with incoming POST requests to an actor's inbox.
|
||||||
// Eg., POST to https://example.org/users/whatever/inbox.
|
// Eg., POST to https://example.org/users/whatever/inbox.
|
||||||
func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
func (m *Module) InboxPOSTHandler(c *gin.Context) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
// usernames on our instance are always lowercase
|
||||||
"func": "InboxPOSTHandler",
|
requestedUsername := strings.ToLower(c.Param(UsernameKey))
|
||||||
"url": c.Request.RequestURI,
|
|
||||||
})
|
|
||||||
|
|
||||||
requestedUsername := c.Param(UsernameKey)
|
|
||||||
if requestedUsername == "" {
|
if requestedUsername == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"})
|
err := errors.New("no username specified in request")
|
||||||
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := transferContext(c)
|
if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil {
|
||||||
|
|
||||||
posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request)
|
|
||||||
if err != nil {
|
|
||||||
if withCode, ok := err.(gtserror.WithCode); ok {
|
if withCode, ok := err.(gtserror.WithCode); ok {
|
||||||
l.Debugf("InboxPOSTHandler: %s", withCode.Error())
|
api.ErrorHandler(c, withCode, m.processor.InstanceGet)
|
||||||
c.JSON(withCode.Code(), withCode.Safe())
|
} else {
|
||||||
return
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
}
|
}
|
||||||
l.Debugf("InboxPOSTHandler: error processing request: %s", err)
|
} else if !posted {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
err := errors.New("unable to process request")
|
||||||
return
|
api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet)
|
||||||
}
|
|
||||||
|
|
||||||
if !posted {
|
|
||||||
l.Debugf("InboxPOSTHandler: request could not be handled as an AP request; headers were: %+v", c.Request.Header)
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue