Merge pull request 'develop' (#6) from AkkomaGang/akkoma:develop into develop

Reviewed-on: #6
This commit is contained in:
qbism 2023-08-03 08:08:43 +00:00
commit b366b9cd6b
108 changed files with 14730 additions and 3598 deletions

View file

@ -1,3 +1,8 @@
platform: linux/amd64
depends_on:
- test
variables: variables:
- &scw-secrets - &scw-secrets
- SCW_ACCESS_KEY - SCW_ACCESS_KEY
@ -12,8 +17,6 @@ variables:
branch: branch:
- develop - develop
- stable - stable
- refs/tags/v*
- refs/tags/stable-*
- &on-stable - &on-stable
when: when:
event: event:
@ -21,14 +24,6 @@ variables:
- tag - tag
branch: branch:
- stable - stable
- refs/tags/stable-*
- &on-point-release
when:
event:
- push
branch:
- develop
- stable
- &on-pr-open - &on-pr-open
when: when:
event: event:
@ -39,60 +34,7 @@ variables:
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)" - &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
- &mix-clean "mix deps.clean --all && mix clean" - &mix-clean "mix deps.clean --all && mix clean"
services:
postgres:
image: postgres:15
when:
event:
- pull_request
environment:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
pipeline: pipeline:
lint:
<<: *on-pr-open
image: akkoma/ci-base:1.14
commands:
- mix local.hex --force
- mix local.rebar --force
- mix format --check-formatted
build:
image: akkoma/ci-base:1.14
<<: *on-pr-open
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
commands:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get
- mix compile
test:
image: akkoma/ci-base:1.14
<<: *on-pr-open
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
commands:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get
- mix compile
- mix ecto.drop -f -q
- mix ecto.create
- mix ecto.migrate
- mix test --preload-modules --exclude erratic --exclude federated --max-cases 4
# Canonical amd64 # Canonical amd64
ubuntu22: ubuntu22:
image: hexpm/elixir:1.14.3-erlang-25.2.2-ubuntu-jammy-20221130 image: hexpm/elixir:1.14.3-erlang-25.2.2-ubuntu-jammy-20221130
@ -151,8 +93,8 @@ pipeline:
# Canonical amd64-musl # Canonical amd64-musl
musl: musl:
image: hexpm/elixir:1.14.3-erlang-25.2.2-alpine-3.15.6 image: hexpm/elixir:1.14.3-erlang-25.2.2-alpine-3.18.0
<<: *on-release <<: *on-stable
environment: environment:
MIX_ENV: prod MIX_ENV: prod
commands: commands:
@ -167,31 +109,9 @@ pipeline:
release-musl: release-musl:
image: akkoma/releaser image: akkoma/releaser
<<: *on-release <<: *on-stable
secrets: *scw-secrets secrets: *scw-secrets
commands: commands:
- export SOURCE=akkoma-amd64-musl.zip - export SOURCE=akkoma-amd64-musl.zip
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-amd64-musl.zip - export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-amd64-musl.zip
- /bin/sh /entrypoint.sh - /bin/sh /entrypoint.sh
docs:
<<: *on-point-release
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
environment:
CI: "true"
image: python:3.10-slim
commands:
- apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- cd docs
- pip install -r requirements.txt
- mkdocs build
- zip -r docs.zip site/*
- cd site
- rclone copy . scaleway:akkoma-docs/$CI_COMMIT_BRANCH/

115
.woodpecker/build-arm64.yml Normal file
View file

@ -0,0 +1,115 @@
platform: linux/arm64
depends_on:
- test
variables:
- &scw-secrets
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
- &setup-hex "mix local.hex --force && mix local.rebar --force"
- &on-release
when:
event:
- push
- tag
branch:
- stable
- develop
- &on-stable
when:
event:
- push
- tag
branch:
- stable
- &on-pr-open
when:
event:
- pull_request
- &tag-build "export BUILD_TAG=$${CI_COMMIT_TAG:-\"$CI_COMMIT_BRANCH\"} && export PLEROMA_BUILD_BRANCH=$BUILD_TAG"
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
- &mix-clean "mix deps.clean --all && mix clean"
pipeline:
# Canonical arm64
ubuntu22:
image: hexpm/elixir:1.14.3-erlang-25.2.2-ubuntu-jammy-20221130
<<: *on-release
environment:
MIX_ENV: prod
DEBIAN_FRONTEND: noninteractive
commands:
- apt-get update && apt-get install -y cmake libmagic-dev rclone zip imagemagick libmagic-dev git build-essential g++ wget
- *clean
- echo "import Config" > config/prod.secret.exs
- *setup-hex
- *tag-build
- mix deps.get --only prod
- mix release --path release
- zip akkoma-ubuntu-jammy.zip -r release
release-ubuntu22:
image: akkoma/releaser:arm64
<<: *on-release
secrets: *scw-secrets
commands:
- export SOURCE=akkoma-ubuntu-jammy.zip
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-arm64-ubuntu-jammy.zip
- /bin/sh /entrypoint.sh
debian-bullseye:
image: hexpm/elixir:1.14.3-erlang-25.2.2-debian-bullseye-20230109
<<: *on-stable
environment:
MIX_ENV: prod
DEBIAN_FRONTEND: noninteractive
commands:
- apt-get update && apt-get install -y cmake libmagic-dev rclone zip imagemagick libmagic-dev git build-essential gcc make g++ wget
- *clean
- echo "import Config" > config/prod.secret.exs
- *setup-hex
- *tag-build
- *mix-clean
- mix deps.get --only prod
- mix release --path release
- zip akkoma-arm64.zip -r release
release-debian:
image: akkoma/releaser:arm64
<<: *on-stable
secrets: *scw-secrets
commands:
- export SOURCE=akkoma-arm64.zip
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-arm64.zip
- /bin/sh /entrypoint.sh
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-debian-stable.zip
- /bin/sh /entrypoint.sh
# Canonical arm64-musl
musl:
image: hexpm/elixir:1.14.3-erlang-25.2.2-alpine-3.18.0
<<: *on-stable
environment:
MIX_ENV: prod
commands:
- apk add git gcc g++ musl-dev make cmake file-dev rclone wget zip imagemagick
- *clean
- *setup-hex
- *mix-clean
- *tag-build
- mix deps.get --only prod
- mix release --path release
- zip akkoma-arm64-musl.zip -r release
release-musl:
image: akkoma/releaser:arm64
<<: *on-stable
secrets: *scw-secrets
commands:
- export SOURCE=akkoma-arm64-musl.zip
- export DEST=scaleway:akkoma-updates/$${CI_COMMIT_TAG:-"$CI_COMMIT_BRANCH"}/akkoma-arm64-musl.zip
- /bin/sh /entrypoint.sh

69
.woodpecker/docs.yml Normal file
View file

@ -0,0 +1,69 @@
platform: linux/amd64
depends_on:
- test
- build-amd64
variables:
- &scw-secrets
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
- &setup-hex "mix local.hex --force && mix local.rebar --force"
- &on-release
when:
event:
- push
- tag
branch:
- develop
- stable
- refs/tags/v*
- refs/tags/stable-*
- &on-stable
when:
event:
- push
- tag
branch:
- stable
- refs/tags/stable-*
- &on-point-release
when:
event:
- push
branch:
- develop
- stable
- &on-pr-open
when:
event:
- pull_request
- &tag-build "export BUILD_TAG=$${CI_COMMIT_TAG:-\"$CI_COMMIT_BRANCH\"} && export PLEROMA_BUILD_BRANCH=$BUILD_TAG"
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
- &mix-clean "mix deps.clean --all && mix clean"
pipeline:
docs:
<<: *on-point-release
secrets:
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
environment:
CI: "true"
image: python:3.10-slim
commands:
- apt-get update && apt-get install -y rclone wget git zip
- wget https://github.com/scaleway/scaleway-cli/releases/download/v2.5.1/scaleway-cli_2.5.1_linux_amd64
- mv scaleway-cli_2.5.1_linux_amd64 scaleway-cli
- chmod +x scaleway-cli
- ./scaleway-cli object config install type=rclone
- cd docs
- pip install -r requirements.txt
- mkdocs build
- zip -r docs.zip site/*
- cd site
- rclone copy . scaleway:akkoma-docs/$CI_COMMIT_BRANCH/

96
.woodpecker/test.yml Normal file
View file

@ -0,0 +1,96 @@
platform: linux/amd64
variables:
- &scw-secrets
- SCW_ACCESS_KEY
- SCW_SECRET_KEY
- SCW_DEFAULT_ORGANIZATION_ID
- &setup-hex "mix local.hex --force && mix local.rebar --force"
- &on-release
when:
event:
- push
- tag
branch:
- develop
- stable
- refs/tags/v*
- refs/tags/stable-*
- &on-stable
when:
event:
- push
- tag
branch:
- stable
- refs/tags/stable-*
- &on-point-release
when:
event:
- push
branch:
- develop
- stable
- &on-pr-open
when:
event:
- pull_request
- &tag-build "export BUILD_TAG=$${CI_COMMIT_TAG:-\"$CI_COMMIT_BRANCH\"} && export PLEROMA_BUILD_BRANCH=$BUILD_TAG"
- &clean "(rm -rf release || true) && (rm -rf _build || true) && (rm -rf /root/.mix)"
- &mix-clean "mix deps.clean --all && mix clean"
services:
postgres:
image: postgres:15
when:
event:
- pull_request
environment:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
pipeline:
lint:
<<: *on-pr-open
image: akkoma/ci-base:1.14
commands:
- mix local.hex --force
- mix local.rebar --force
- mix format --check-formatted
build:
image: akkoma/ci-base:1.14
<<: *on-pr-open
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
commands:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get
- mix compile
test:
image: akkoma/ci-base:1.14
<<: *on-pr-open
environment:
MIX_ENV: test
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
DB_HOST: postgres
commands:
- mix local.hex --force
- mix local.rebar --force
- mix deps.get
- mix compile
- mix ecto.drop -f -q
- mix ecto.create
- mix ecto.migrate
- mix test --preload-modules --exclude erratic --exclude federated --max-cases 4

View file

@ -6,6 +6,63 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## Unreleased ## Unreleased
## Added
- Added a new configuration option to the MediaProxy feature that allows the blocking of specific domains from using the media proxy or being explicitly allowed by the Content-Security-Policy.
- Please make sure instances you wanted to block media from are not in the MediaProxy `whitelist`, and instead use `blocklist`.
- `OnlyMedia` Upload Filter to simplify restricting uploads to audio, image, and video types
- ARM64 OTP builds
- Ubuntu22 builds are available for develop and stable
- other distributions are stable only
## Changed
- Alpine OTP builds are now from alpine 3.18, which is SSLv3 compatible.
If you use alpine OTP builds you will have to update your local system.
## Fixed
- Deactivated users can no longer show up in the emoji reaction list
- Embedded posts can no longer bypass `:restrict\_unauthenticated`
- GET/HEAD requests will now work when requesting AWS-based instances.
## Security
- Add `no_new_privs` hardening to OpenRC and systemd service files
## 2023.05
## Added
- Custom options for users to accept/reject private messages
- options: everybody, nobody, people\_i\_follow
- MRF to reject notes from accounts newer than a given age
- this will have the side-effect of rejecting legitimate messages if your
post gets boosted outside of your local bubble and people your instance
does not know about reply to it.
## Fixed
- Support for `streams` public key URIs
- Bookmarks are cleaned up on DB prune now
## Security
- Fixed mediaproxy being a bit of a silly billy
## 2023.04
## Added
- Nodeinfo keys for unauthenticated timeline visibility
- Option to disable federated timeline
- Option to make the bubble timeline publicly accessible
- Ability to swap between installed standard frontends
- *mastodon frontends are still not counted as standard frontends due to the complexity in serving them correctly*.
### Upgrade Notes
- Elixir 1.14 is now required. If your distribution does not package this, you can
use [asdf](https://asdf-vm.com/). At time of writing, elixir 1.14.3 / erlang 25.3
is confirmed to work.
## 2023.03
## Fixed ## Fixed
- Support for `streams` public key URIs - Support for `streams` public key URIs
@ -74,7 +131,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Rich media will now hard-exit after 5 seconds, to prevent timeline hangs - Rich media will now hard-exit after 5 seconds, to prevent timeline hangs
- HTTP Content Security Policy is now far more strict to prevent any potential XSS/CSS leakages - HTTP Content Security Policy is now far more strict to prevent any potential XSS/CSS leakages
- Follow requests are now paginated, matches mastodon API spec, so use the Link header to paginate. - Follow requests are now paginated, matches mastodon API spec, so use the Link header to paginate.
- `internal.fetch` and `relay` actors are now represented with the actor type `Application`
### Fixed ### Fixed
- /api/v1/accounts/lookup will now respect restrict\_unauthenticated - /api/v1/accounts/lookup will now respect restrict\_unauthenticated

View file

@ -418,6 +418,8 @@
config :pleroma, :mrf_follow_bot, follower_nickname: nil config :pleroma, :mrf_follow_bot, follower_nickname: nil
config :pleroma, :mrf_reject_newly_created_account_notes, age: 86_400
config :pleroma, :rich_media, config :pleroma, :rich_media,
enabled: true, enabled: true,
ignore_hosts: [], ignore_hosts: [],
@ -441,7 +443,8 @@
# Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1 # Note: max_read_duration defaults to Pleroma.ReverseProxy.max_read_duration_default/1
max_read_duration: 30_000 max_read_duration: 30_000
], ],
whitelist: [] whitelist: [],
blocklist: []
config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http, config :pleroma, Pleroma.Web.MediaProxy.Invalidation.Http,
method: :purge, method: :purge,

View file

@ -1558,7 +1558,21 @@
%{ %{
key: :whitelist, key: :whitelist,
type: {:list, :string}, type: {:list, :string},
description: "List of hosts with scheme to bypass the MediaProxy", description: """
List of hosts with scheme to bypass the MediaProxy.\n
The media will be fetched by the client, directly from the remote server.\n
To allow this, it will Content-Security-Policy exceptions for each instance listed.\n
This is to be used for instances you trust and do not want to cache media for.
""",
suggestions: ["http://example.com"]
},
%{
key: :blocklist,
type: {:list, :string},
description: """
List of hosts with scheme which will not go through the MediaProxy, and will not be explicitly allowed by the Content-Security-Policy.
This is to be used for instances where you do not want their media to go through your server or to be accessed by clients.
""",
suggestions: ["http://example.com"] suggestions: ["http://example.com"]
} }
] ]

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
docker-compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) akkoma docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) akkoma
docker-compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) db docker compose build --build-arg UID=$(id -u) --build-arg GID=$(id -g) db

View file

@ -1,3 +1,3 @@
#!/bin/sh #!/bin/sh
docker-compose run --rm akkoma $@ docker compose run --rm akkoma $@

View file

@ -42,7 +42,7 @@ For a frontend configured under the `available` key, it's enough to install it b
This will download the latest build for the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`). This will download the latest build for the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`).
You can override any of the details. To install a Pleroma-FE build from a different URL, you could do this: You can override any of the details. To install an Akkoma-FE build from a different URL, you could do this:
=== "OTP" === "OTP"

View file

@ -62,6 +62,6 @@ mix ecto.migrate
# Start akkoma (replace with your system service manager's equivalent if different) # Start akkoma (replace with your system service manager's equivalent if different)
sudo systemctl start akkoma sudo systemctl start akkoma
# Update Pleroma-FE frontend to latest stable. For other Frontends see Frontend Configuration doc for more information. # Update Akkoma-FE frontend to latest stable. For other Frontends see Frontend Configuration doc for more information.
mix pleroma.frontend install pleroma-fe --ref stable mix pleroma.frontend install pleroma-fe --ref stable
``` ```

View file

@ -2,6 +2,14 @@
Note: Additional clients may work, but these are known to work with Akkoma. Note: Additional clients may work, but these are known to work with Akkoma.
Apps listed here might not support all of Akkoma's features. Apps listed here might not support all of Akkoma's features.
## Multiplatform
### Kaiteki
- Homepage: <https://kaiteki.app/>
- Source Code: <https://github.com/Kaiteki-Fedi/Kaiteki>
- Contact: [@kaiteki@fedi.software](https://fedi.software/@Kaiteki)
- Platforms: Web, Windows, Linux, Android
- Features: MastoAPI, Supports multiple backends
## Desktop ## Desktop
### Whalebird ### Whalebird
- Homepage: <https://whalebird.social/> - Homepage: <https://whalebird.social/>

View file

@ -246,11 +246,11 @@ Notes:
### :frontend_configurations ### :frontend_configurations
This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Pleroma-FE configuration and customization for instance administrators](https://docs-fe.akkoma.dev/stable/CONFIGURATION/#options). This can be used to configure a keyword list that keeps the configuration data for any kind of frontend. By default, settings for `pleroma_fe` and `masto_fe` are configured. You can find the documentation for `pleroma_fe` configuration into [Akkoma-FE configuration and customization for instance administrators](https://docs-fe.akkoma.dev/stable/CONFIGURATION/#options).
Frontends can access these settings at `/api/v1/pleroma/frontend_configurations` Frontends can access these settings at `/api/v1/pleroma/frontend_configurations`
To add your own configuration for Pleroma-FE, use it like this: To add your own configuration for Akkoma-FE, use it like this:
```elixir ```elixir
config :pleroma, :frontend_configurations, config :pleroma, :frontend_configurations,
@ -615,6 +615,12 @@ This filter only strips the GPS and location metadata with Exiftool leaving colo
No specific configuration. No specific configuration.
#### Pleroma.Upload.Filter.OnlyMedia
This filter rejects uploads that are not identified with Content-Type matching audio/\*, image/\*, or video/\*
No specific configuration.
#### Pleroma.Upload.Filter.Mogrify #### Pleroma.Upload.Filter.Mogrify
* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`. * `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`.

View file

@ -6,7 +6,7 @@ To add a custom theme to your instance, you'll first need to get a custom theme,
### Create your own theme ### Create your own theme
* You can create your own theme using the Pleroma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme. * You can create your own theme using the Akkoma FE by going to settings (gear on the top right) and choose the Theme tab. Here you have the options to create a personal theme.
* To download your theme, you can do Save preset * To download your theme, you can do Save preset
* If you want to upload a theme to customise it further, you can upload it using Load preset * If you want to upload a theme to customise it further, you can upload it using Load preset

View file

@ -25,6 +25,7 @@ Home, public, hashtag & list timelines accept these parameters:
## Statuses ## Statuses
- `visibility`: has additional possible values `list` and `local` (for local-only statuses) - `visibility`: has additional possible values `list` and `local` (for local-only statuses)
- `emoji_reactions`: additional field since Akkoma 3.2.0; identical to `pleroma/emoji_reactions`
Has these additional fields under the `pleroma` object: Has these additional fields under the `pleroma` object:
@ -36,7 +37,9 @@ Has these additional fields under the `pleroma` object:
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain` - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being its mimetype. Currently, the only alternate representation supported is `text/plain`
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
- `thread_muted`: true if the thread the post belongs to is muted - `thread_muted`: true if the thread the post belongs to is muted
- `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 1, me: true}`. Contains no information about the reacting users, for that use the `/statuses/:id/reactions` endpoint. - `emoji_reactions`: A list with emoji / reaction maps. The format is `{name: "☕", count: 2, me: true, account_ids: ["UserID1", "UserID2"]}`.
The `account_ids` property was added in Akkoma 3.2.0.
Further info about all reacting users at once, can be found using the `/statuses/:id/reactions` endpoint.
- `parent_visible`: If the parent of this post is visible to the user or not. - `parent_visible`: If the parent of this post is visible to the user or not.
- `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise. - `pinned_at`: a datetime (iso8601) when status was pinned, `null` otherwise.
@ -214,6 +217,11 @@ Returns: array of Status.
The maximum number of statuses is limited to 100 per request. The maximum number of statuses is limited to 100 per request.
## PUT `/api/v1/statuses/:id/emoji_reactions/:emoji`
This endpoint is an extension of the Fedibird Mastodon fork.
It behaves identical to PUT `/api/v1/pleroma/statuses/:id/reactions/:emoji`.
## PATCH `/api/v1/accounts/update_credentials` ## PATCH `/api/v1/accounts/update_credentials`
Additional parameters can be added to the JSON body/Form data: Additional parameters can be added to the JSON body/Form data:

View file

@ -3,7 +3,7 @@
# Introduction to Akkoma # Introduction to Akkoma
## What is Akkoma? ## What is Akkoma?
Akkoma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3. Akkoma is a federated social networking platform, compatible with Mastodon and other ActivityPub implementations. It is free software licensed under the AGPLv3.
It actually consists of two components: a backend, named simply Akkoma, and a user-facing frontend, named Pleroma-FE. It also includes the Mastodon frontend, if that's your thing. It actually consists of two components: a backend, named simply Akkoma, and a user-facing frontend, named Akkoma-FE. It also includes the Mastodon frontend, if that's your thing.
It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other. It's part of what we call the fediverse, a federated network of instances which speak common protocols and can communicate with each other.
One account on an instance is enough to talk to the entire fediverse! One account on an instance is enough to talk to the entire fediverse!
@ -31,11 +31,11 @@ Installation instructions can be found in the installation section of these docs
## I got an account, now what? ## I got an account, now what?
Great! Now you can explore the fediverse! Open the login page for your Akkoma instance (e.g. <https://otp.akkoma.dev>) and login with your username and password. (If you don't have an account yet, click on Register) Great! Now you can explore the fediverse! Open the login page for your Akkoma instance (e.g. <https://otp.akkoma.dev>) and login with your username and password. (If you don't have an account yet, click on Register)
### Pleroma-FE ### Akkoma-FE
The default front-end used by Akkoma is Pleroma-FE. You can find more information on what it is and how to use it in the [Introduction to Pleroma-FE](https://docs-fe.akkoma.dev/stable/). The default front-end used by Akkoma is Akkoma-FE. You can find more information on what it is and how to use it in the [Introduction to Akkoma-FE](https://docs-fe.akkoma.dev/stable/).
### Mastodon interface ### Mastodon interface
If the Pleroma-FE interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too! If the Akkoma-FE interface isn't your thing, or you're just trying something new but you want to keep using the familiar Mastodon interface, we got that too!
Just add a "/web" after your instance url (e.g. <https://otp.akkoma.dev/web>) and you'll end on the Mastodon web interface, but with a Akkoma backend! MAGIC! Just add a "/web" after your instance url (e.g. <https://otp.akkoma.dev/web>) and you'll end on the Mastodon web interface, but with a Akkoma backend! MAGIC!
The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation. The Mastodon interface is from the Glitch-soc fork. For more information on the Mastodon interface you can check the [Mastodon](https://docs.joinmastodon.org/) and [Glitch-soc](https://glitch-soc.github.io/docs/) documentation.

View file

@ -23,23 +23,7 @@ sudo apt full-upgrade
sudo apt install git build-essential postgresql postgresql-contrib cmake libmagic-dev sudo apt install git build-essential postgresql postgresql-contrib cmake libmagic-dev
``` ```
### Install Elixir and Erlang ### Create the akkoma user
* Install Elixir and Erlang (you might need to use backports or [asdf](https://github.com/asdf-vm/asdf) on old systems):
```shell
sudo apt update
sudo apt install elixir erlang-dev erlang-nox
```
### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
```shell
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
```
### Install AkkomaBE
* Add a new system user for the Akkoma service: * Add a new system user for the Akkoma service:
@ -49,7 +33,67 @@ sudo useradd -r -s /bin/false -m -d /var/lib/akkoma -U akkoma
**Note**: To execute a single command as the Akkoma system user, use `sudo -Hu akkoma command`. You can also switch to a shell by using `sudo -Hu akkoma $SHELL`. If you dont have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l akkoma -s $SHELL -c 'command'` and `su -l akkoma -s $SHELL` for starting a shell. **Note**: To execute a single command as the Akkoma system user, use `sudo -Hu akkoma command`. You can also switch to a shell by using `sudo -Hu akkoma $SHELL`. If you dont have and want `sudo` on your system, you can use `su` as root user (UID 0) for a single command by using `su -l akkoma -s $SHELL -c 'command'` and `su -l akkoma -s $SHELL` for starting a shell.
* Git clone the AkkomaBE repository from stable-branch and make the Akkoma user the owner of the directory: ### Install Elixir and Erlang
If your distribution packages a recent enough version of Elixir, you can install it directly from the distro repositories and skip to the next section of the guide:
```shell
sudo apt install elixir erlang-dev erlang-nox
```
Otherwise use [asdf](https://github.com/asdf-vm/asdf) to install the latest versions of Elixir and Erlang.
First, install some dependencies needed to build Elixir and Erlang:
```shell
sudo apt install curl unzip build-essential autoconf m4 libncurses5-dev libssh-dev unixodbc-dev xsltproc libxml2-utils libncurses-dev
```
Then login to the `akkoma` user and install asdf:
```shell
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.11.3
```
Add the following lines to `~/.bashrc`:
```shell
. "$HOME/.asdf/asdf.sh"
# asdf completions
. "$HOME/.asdf/completions/asdf.bash"
```
Restart the shell:
```shell
exec $SHELL
```
Next install Erlang:
```shell
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
export KERL_CONFIGURE_OPTIONS="--disable-debug --without-javac"
asdf install erlang 25.3.2.1
asdf global erlang 25.3.2.1
```
Now install Elixir:
```shell
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf install elixir 1.14.5-otp-25
asdf global elixir 1.14.5-otp-25
```
Confirm that Elixir is installed correctly by checking the version:
```shell
elixir --version
```
### Optional packages: [`docs/installation/optional/media_graphics_packages.md`](../installation/optional/media_graphics_packages.md)
```shell
sudo apt install imagemagick ffmpeg libimage-exiftool-perl
```
### Install AkkomaBE
* Log into the `akkoma` user and clone the AkkomaBE repository from the stable branch and make the Akkoma user the owner of the directory:
```shell ```shell
sudo mkdir -p /opt/akkoma sudo mkdir -p /opt/akkoma

View file

@ -10,7 +10,7 @@ If you want to migrate from or OTP to docker, check out [the migration guide](./
### Prepare the system ### Prepare the system
* Install docker and docker-compose * Install docker and docker compose
* [Docker](https://docs.docker.com/engine/install/) * [Docker](https://docs.docker.com/engine/install/)
* [Docker-compose](https://docs.docker.com/compose/install/) * [Docker-compose](https://docs.docker.com/compose/install/)
* This will usually just be a repository installation and a package manager invocation. * This will usually just be a repository installation and a package manager invocation.
@ -26,7 +26,7 @@ echo "DOCKER_USER=$(id -u):$(id -g)" >> .env
``` ```
This probably won't need to be changed, it's only there to set basic environment This probably won't need to be changed, it's only there to set basic environment
variables for the docker-compose file. variables for the docker compose file.
### Building the container ### Building the container
@ -65,9 +65,9 @@ cp config/generated_config.exs config/prod.secret.exs
We need to run a few commands on the database container, this isn't too bad We need to run a few commands on the database container, this isn't too bad
```bash ```bash
docker-compose run --rm --user akkoma -d db docker compose run --rm --user akkoma -d db
# Note down the name it gives here, it will be something like akkoma_db_run # Note down the name it gives here, it will be something like akkoma_db_run
docker-compose run --rm akkoma psql -h db -U akkoma -f config/setup_db.psql docker compose run --rm akkoma psql -h db -U akkoma -f config/setup_db.psql
docker stop akkoma_db_run # Replace with the name you noted down docker stop akkoma_db_run # Replace with the name you noted down
``` ```
@ -84,17 +84,17 @@ We're going to run it in the foreground on the first run, just to make sure
everything start up. everything start up.
```bash ```bash
docker-compose up docker compose up
``` ```
If everything went well, you should be able to access your instance at http://localhost:4000 If everything went well, you should be able to access your instance at http://localhost:4000
You can `ctrl-c` out of the docker-compose now to shutdown the server. You can `ctrl-c` out of the docker compose now to shutdown the server.
### Running in the background ### Running in the background
```bash ```bash
docker-compose up -d docker compose up -d
``` ```
### Create your first user ### Create your first user
@ -125,8 +125,8 @@ cp docker-resources/Caddyfile.example docker-resources/Caddyfile
Then edit the TLD in your caddyfile to the domain you're serving on. Then edit the TLD in your caddyfile to the domain you're serving on.
Uncomment the `caddy` section in the docker-compose file, Uncomment the `caddy` section in the docker compose file,
then run `docker-compose up -d` again. then run `docker compose up -d` again.
#### Running a reverse proxy on the host #### Running a reverse proxy on the host
@ -152,7 +152,7 @@ git pull
./docker-resources/manage.sh mix deps.get ./docker-resources/manage.sh mix deps.get
./docker-resources/manage.sh mix compile ./docker-resources/manage.sh mix compile
./docker-resources/manage.sh mix ecto.migrate ./docker-resources/manage.sh mix ecto.migrate
docker-compose restart akkoma db docker compose restart akkoma db
``` ```
#### Further reading #### Further reading

View file

@ -118,3 +118,15 @@ To fix this, run:
``` ```
which will remove the config from the database. Things should work now. which will remove the config from the database. Things should work now.
## Migrating back to Pleroma
Akkoma is a hard fork of Pleroma. As such, migrating back is not guaranteed to always work. But if you want to migrate back to Pleroma, you can always try. Just note that you may run into unexpected issues and you're basically on your own. The following are some tips that may help, but note that these are barely tested, so proceed at your own risk.
First you will need to roll back the database migrations. The latest migration both Akkoma and Pleroma still have in common should be 20210416051708, so roll back to that. If you run from source, that should be
```sh
MIX_ENV=prod mix ecto.rollback --to 20210416051708
```
Then switch back to Pleroma for updates (similar to how was done to migrate to Akkoma), and remove the front-ends. The front-ends are installed in the `frontends` folder in the [static directory](../configuration/static_dir.md). Once you are back to Pleroma, you will need to run the database migrations again. See the Pleroma documentation for this.

View file

@ -10,7 +10,7 @@ You probably should, in the first instance.
### Prepare the system ### Prepare the system
* Install docker and docker-compose * Install docker and docker compose
* [Docker](https://docs.docker.com/engine/install/) * [Docker](https://docs.docker.com/engine/install/)
* [Docker-compose](https://docs.docker.com/compose/install/) * [Docker-compose](https://docs.docker.com/compose/install/)
* This will usually just be a repository installation and a package manager invocation. * This will usually just be a repository installation and a package manager invocation.
@ -46,7 +46,7 @@ For *most* from-source installs it'll already be there.
And the same with `uploads`, make sure your uploads (if you have them on disk) are And the same with `uploads`, make sure your uploads (if you have them on disk) are
located at `uploads/` in the akkoma source directory. located at `uploads/` in the akkoma source directory.
If you have them on a different disk, you will need to mount that disk into the docker-compose file, If you have them on a different disk, you will need to mount that disk into the docker compose file,
with an entry that looks like this: with an entry that looks like this:
```yaml ```yaml
@ -66,7 +66,7 @@ echo "DOCKER_USER=$(id -u):$(id -g)" >> .env
``` ```
This probably won't need to be changed, it's only there to set basic environment This probably won't need to be changed, it's only there to set basic environment
variables for the docker-compose file. variables for the docker compose file.
=== "From source" === "From source"
@ -126,21 +126,21 @@ mkdir pgdata
Now we can import our database to the container. Now we can import our database to the container.
```bash ```bash
docker-compose run --rm --user akkoma -d db docker compose run --rm --user akkoma -d db
docker-compose run --rm akkoma pg_restore -v -U akkoma -j $(grep -c ^processor /proc/cpuinfo) -d akkoma -h db akkoma_backup.sql docker compose run --rm akkoma pg_restore -v -U akkoma -j $(grep -c ^processor /proc/cpuinfo) -d akkoma -h db akkoma_backup.sql
``` ```
### Reverse proxies ### Reverse proxies
If you're just reusing your old proxy, you may have to uncomment the line in If you're just reusing your old proxy, you may have to uncomment the line in
the docker-compose file under `ports`. You'll find it. the docker compose file under `ports`. You'll find it.
Otherwise, you can use the same setup as the [docker installation guide](./docker_en.md#reverse-proxies). Otherwise, you can use the same setup as the [docker installation guide](./docker_en.md#reverse-proxies).
### Let's go ### Let's go
```bash ```bash
docker-compose up -d docker compose up -d
``` ```
You should now be at the same point as you were before, but with a docker install. You should now be at the same point as you were before, but with a docker install.

View file

@ -1,19 +1,19 @@
# Optional software packages needed for specific functionality # Optional software packages needed for specific functionality
For specific Pleroma functionality (which is disabled by default) some or all of the below packages are required: For specific Akkoma functionality (which is disabled by default) some or all of the below packages are required:
* `ImageMagic` * `ImageMagick`
* `ffmpeg` * `ffmpeg`
* `exiftool` * `exiftool`
Please refer to documentation in `docs/installation` on how to install them on specific OS. Please refer to documentation in `docs/installation` on how to install them on specific OS.
Note: the packages are not required with the current default settings of Pleroma. Note: the packages are not required with the current default settings of Akkoma.
## `ImageMagick` ## `ImageMagick`
`ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images. `ImageMagick` is a set of tools to create, edit, compose, or convert bitmap images.
It is required for the following Pleroma features: It is required for the following Akkoma features:
* `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`) * `Pleroma.Upload.Filters.Mogrify`, `Pleroma.Upload.Filters.Mogrifun` upload filters (related config: `Plaroma.Upload/filters` in `config/config.exs`)
* Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`) * Media preview proxy for still images (related config: `media_preview_proxy/enabled` in `config/config.exs`)
@ -21,12 +21,12 @@ It is required for the following Pleroma features:
`ffmpeg` is software to record, convert and stream audio and video. `ffmpeg` is software to record, convert and stream audio and video.
It is required for the following Pleroma features: It is required for the following Akkoma features:
* Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`) * Media preview proxy for videos (related config: `media_preview_proxy/enabled` in `config/config.exs`)
## `exiftool` ## `exiftool`
`exiftool` is media files metadata reader/writer. `exiftool` is media files metadata reader/writer.
It is required for the following Pleroma features: It is required for the following Akkoma features:
* `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`) * `Pleroma.Upload.Filters.Exiftool` upload filter (related config: `Plaroma.Upload/filters` in `config/config.exs`)

View file

@ -19,12 +19,16 @@ This is a little more complex than it used to be (thanks ubuntu)
Use the following mapping to figure out your flavour: Use the following mapping to figure out your flavour:
| distribution | flavour | available branches | | distribution | architecture | flavour | available branches |
| ------------- | ------------------ | ------------------- | | --------------- | ------------------ | ------------------- | ------------------- |
| debian stable | amd64 | develop, stable | | debian bullseye | amd64 | amd64 | develop, stable |
| ubuntu focal | amd64 | develop, stable | | debian bullseye | arm64 | arm64 | stable |
| ubuntu jammy | amd64-ubuntu-jammy | develop, stable | | ubuntu focal | amd64 | amd64 | develop, stable |
| alpine | amd64-musl | stable | | ubuntu focal | arm64 | arm64 | stable |
| ubuntu jammy | amd64 | amd64-ubuntu-jammy | develop, stable |
| ubuntu jammy | arm64 | arm64-ubuntu-jammy | develop, stable |
| alpine | amd64 | amd64-musl | stable |
| alpine | arm64 | arm64-musl | stable |
Other similar distributions will _probably_ work, but if it is not listed above, there is no official Other similar distributions will _probably_ work, but if it is not listed above, there is no official
support. support.
@ -118,8 +122,12 @@ Restart PostgreSQL to apply configuration changes:
adduser --system --shell /bin/false --home /opt/akkoma akkoma adduser --system --shell /bin/false --home /opt/akkoma akkoma
# Set the flavour environment variable to the string you got in Detecting flavour section. # Set the flavour environment variable to the string you got in Detecting flavour section.
# For example if the flavour is `amd64` the command will be # For example if the flavour is `amd64-musl` the command will be
export FLAVOUR="amd64" # export FLAVOUR="amd64-musl"
export FLAVOUR="<replace-this-with-the-correct-flavour-string>"
# Make sure the SHELL variable is set
export SHELL="${SHELL:-/bin/sh}"
# Clone the release build into a temporary directory and unpack it # Clone the release build into a temporary directory and unpack it
su akkoma -s $SHELL -lc " su akkoma -s $SHELL -lc "

View file

@ -38,11 +38,11 @@
{% endif %} {% endif %}
{% if page and page.url.startswith('backend') %} {% if page and page.url.startswith('backend') %}
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma" %} {% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma" %}
{% set repo_name = "pleroma/pleroma" %} {% set repo_name = "AkkomaGang/akkoma" %}
{% elif page and page.url.startswith('frontend') %} {% elif page and page.url.startswith('frontend') %}
{% set repo_url = "https://git.pleroma.social/pleroma/pleroma-fe" %} {% set repo_url = "https://akkoma.dev/AkkomaGang/akkoma-fe" %}
{% set repo_name = "pleroma/pleroma-fe" %} {% set repo_name = "AkkomaGang/akkoma-fe" %}
{% else %} {% else %}
{% set repo_url = config.repo_url %} {% set repo_url = config.repo_url %}
{% set repo_name = config.repo_name %} {% set repo_name = config.repo_name %}

View file

@ -4,6 +4,9 @@
# 1. Replace 'example.tld' with your instance's domain wherever it appears. # 1. Replace 'example.tld' with your instance's domain wherever it appears.
# 2. Copy this section into your Caddyfile and restart Caddy. # 2. Copy this section into your Caddyfile and restart Caddy.
# If you are able to, it's highly recommended to have your media served via a separate subdomain for improved security.
# Uncomment the relevant sectons here and modify the base_url setting for Pleroma.Upload and :media_proxy accordingly.
example.tld { example.tld {
log { log {
output file /var/log/caddy/akkoma.log output file /var/log/caddy/akkoma.log
@ -14,4 +17,21 @@ example.tld {
# this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only # this is explicitly IPv4 since Pleroma.Web.Endpoint binds on IPv4 only
# and `localhost.` resolves to [::0] on some systems: see issue #930 # and `localhost.` resolves to [::0] on some systems: see issue #930
reverse_proxy 127.0.0.1:4000 reverse_proxy 127.0.0.1:4000
# Uncomment if using a separate media subdomain
#@mediaproxy path /media/* /proxy/*
#handle @mediaproxy {
# redir https://media.example.tld{uri} permanent
#}
} }
# Uncomment if using a separate media subdomain
#media.example.tld {
# @mediaproxy path /media/* /proxy/*
# reverse_proxy @mediaproxy 127.0.0.1:4000 {
# transport http {
# response_header_timeout 10s
# read_timeout 15s
# }
# }
#}

View file

@ -8,6 +8,7 @@ pidfile="/var/run/akkoma.pid"
directory=/opt/akkoma directory=/opt/akkoma
healthcheck_delay=60 healthcheck_delay=60
healthcheck_timer=30 healthcheck_timer=30
no_new_privs="yes"
: ${akkoma_port:-4000} : ${akkoma_port:-4000}

View file

@ -171,6 +171,21 @@ def run(["prune_objects" | args]) do
end end
|> Repo.delete_all(timeout: :infinity) |> Repo.delete_all(timeout: :infinity)
if !Keyword.get(options, :keep_threads) do
# Without the --keep-threads option, it's possible that bookmarked
# objects have been deleted. We remove the corresponding bookmarks.
"""
delete from public.bookmarks
where id in (
select b.id from public.bookmarks b
left join public.activities a on b.activity_id = a.id
left join public.objects o on a."data" ->> 'object' = o.data ->> 'id'
where o.id is null
)
"""
|> Repo.query([], timeout: :infinity)
end
if Keyword.get(options, :prune_orphaned_activities) do if Keyword.get(options, :prune_orphaned_activities) do
# Prune activities who link to a single object # Prune activities who link to a single object
""" """

View file

@ -21,7 +21,7 @@ defp generate_topics(object, activity) do
["user", "list"] ++ visibility_tags(object, activity) ["user", "list"] ++ visibility_tags(object, activity)
end end
defp visibility_tags(object, activity) do defp visibility_tags(object, %{data: %{"type" => type}} = activity) when type != "Announce" do
case Visibility.get_visibility(activity) do case Visibility.get_visibility(activity) do
"public" -> "public" ->
if activity.local do if activity.local do
@ -31,6 +31,10 @@ defp visibility_tags(object, activity) do
end end
|> item_creation_tags(object, activity) |> item_creation_tags(object, activity)
"local" ->
["public:local"]
|> item_creation_tags(object, activity)
"direct" -> "direct" ->
["direct"] ["direct"]
@ -39,6 +43,10 @@ defp visibility_tags(object, activity) do
end end
end end
defp visibility_tags(_object, _activity) do
[]
end
defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do defp item_creation_tags(tags, object, %{data: %{"type" => "Create"}} = activity) do
tags ++ tags ++
remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity) remote_topics(activity) ++ hashtags_to_topics(object) ++ attachment_topics(object, activity)
@ -63,7 +71,18 @@ defp remote_topics(_), do: []
defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: [] defp attachment_topics(%{data: %{"attachment" => []}}, _act), do: []
defp attachment_topics(_object, %{local: true}), do: ["public:media", "public:local:media"] defp attachment_topics(_object, %{local: true} = activity) do
case Visibility.get_visibility(activity) do
"public" ->
["public:media", "public:local:media"]
"local" ->
["public:local:media"]
_ ->
[]
end
end
defp attachment_topics(_object, %{actor: actor}) when is_binary(actor), defp attachment_topics(_object, %{actor: actor}) when is_binary(actor),
do: ["public:media", "public:remote:media:" <> URI.parse(actor).host] do: ["public:media", "public:remote:media:" <> URI.parse(actor).host]

View file

@ -40,7 +40,7 @@ def translate(string, from_language, to_language) do
if Map.has_key?(body, "detectedLanguage") do if Map.has_key?(body, "detectedLanguage") do
get_in(body, ["detectedLanguage", "language"]) get_in(body, ["detectedLanguage", "language"])
else else
from_language from_language || ""
end end
{:ok, detected, translated} {:ok, detected, translated}

View file

@ -262,11 +262,14 @@ defp http_children do
proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url) proxy = Pleroma.HTTP.AdapterHelper.format_proxy(proxy_url)
pool_size = Config.get([:http, :pool_size]) pool_size = Config.get([:http, :pool_size])
:public_key.cacerts_load()
config = config =
[:http, :adapter] [:http, :adapter]
|> Config.get([]) |> Config.get([])
|> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size) |> Pleroma.HTTP.AdapterHelper.add_pool_size(pool_size)
|> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy) |> Pleroma.HTTP.AdapterHelper.maybe_add_proxy_pool(proxy)
|> Pleroma.HTTP.AdapterHelper.maybe_add_cacerts(:public_key.cacerts_get())
|> Keyword.put(:name, MyFinch) |> Keyword.put(:name, MyFinch)
[{Finch, config}] [{Finch, config}]

View file

@ -25,7 +25,7 @@ defmodule Pleroma.Constants do
const(static_only_files, const(static_only_files,
do: do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance embed sw.js sw-pleroma.js favicon.png schemas doc)
) )
const(status_updatable_fields, const(status_updatable_fields,

View file

@ -27,10 +27,10 @@ defmodule Pleroma.HTTP do
nil | {:ok, Env.t()} | {:error, any()} nil | {:ok, Env.t()} | {:error, any()}
def get(url, headers \\ [], options \\ []) def get(url, headers \\ [], options \\ [])
def get(nil, _, _), do: nil def get(nil, _, _), do: nil
def get(url, headers, options), do: request(:get, url, "", headers, options) def get(url, headers, options), do: request(:get, url, nil, headers, options)
@spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()} @spec head(Request.url(), Request.headers(), keyword()) :: {:ok, Env.t()} | {:error, any()}
def head(url, headers \\ [], options \\ []), do: request(:head, url, "", headers, options) def head(url, headers \\ [], options \\ []), do: request(:head, url, nil, headers, options)
@doc """ @doc """
Performs POST request. Performs POST request.

View file

@ -47,6 +47,17 @@ def maybe_add_proxy_pool(opts, proxy) do
|> put_in([:pools, :default, :conn_opts, :proxy], proxy) |> put_in([:pools, :default, :conn_opts, :proxy], proxy)
end end
def maybe_add_cacerts(opts, nil), do: opts
def maybe_add_cacerts(opts, cacerts) do
opts
|> maybe_add_pools()
|> maybe_add_default_pool()
|> maybe_add_conn_opts()
|> maybe_add_transport_opts()
|> put_in([:pools, :default, :conn_opts, :transport_opts, :cacerts], cacerts)
end
def add_pool_size(opts, pool_size) do def add_pool_size(opts, pool_size) do
opts opts
|> maybe_add_pools() |> maybe_add_pools()
@ -82,6 +93,16 @@ defp maybe_add_conn_opts(opts) do
end end
end end
defp maybe_add_transport_opts(opts) do
transport_opts = get_in(opts, [:pools, :default, :conn_opts, :transport_opts])
unless is_nil(transport_opts) do
opts
else
put_in(opts, [:pools, :default, :conn_opts, :transport_opts], [])
end
end
@doc """ @doc """
Merge default connection & adapter options with received ones. Merge default connection & adapter options with received ones.
""" """

View file

@ -195,6 +195,7 @@ defp exclude_filtered(query, user) do
from([_n, a, o] in query, from([_n, a, o] in query,
where: where:
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
fragment("?->>'content' is null", o.data) or
fragment("?->>'actor' = ?", o.data, ^user.ap_id) fragment("?->>'actor' = ?", o.data, ^user.ap_id)
) )
end end
@ -695,7 +696,7 @@ def skip?(
cond do cond do
opts[:type] == "poll" -> false opts[:type] == "poll" -> false
user.ap_id == actor -> false user.ap_id == actor -> false
!User.following?(follower, user) -> true !User.following?(user, follower) -> true
true -> false true -> false
end end
end end

View file

@ -251,6 +251,7 @@ defp build_resp_headers(headers, opts) do
|> Enum.filter(fn {k, _} -> k in @keep_resp_headers end) |> Enum.filter(fn {k, _} -> k in @keep_resp_headers end)
|> build_resp_cache_headers(opts) |> build_resp_cache_headers(opts)
|> build_resp_content_disposition_header(opts) |> build_resp_content_disposition_header(opts)
|> build_csp_headers()
|> Keyword.merge(Keyword.get(opts, :resp_headers, [])) |> Keyword.merge(Keyword.get(opts, :resp_headers, []))
end end
@ -316,6 +317,10 @@ defp build_resp_content_disposition_header(headers, opts) do
end end
end end
defp build_csp_headers(headers) do
List.keystore(headers, "content-security-policy", 0, {"content-security-policy", "sandbox"})
end
defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do defp header_length_constraint(headers, limit) when is_integer(limit) and limit > 0 do
with {_, size} <- List.keyfind(headers, "content-length", 0), with {_, size} <- List.keyfind(headers, "content-length", 0),
{size, _} <- Integer.parse(size), {size, _} <- Integer.parse(size),

View file

@ -38,9 +38,9 @@ def filter([filter | rest], upload) do
{:ok, :noop} -> {:ok, :noop} ->
filter(rest, upload) filter(rest, upload)
error -> {:error, e} ->
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}") Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(e)}")
error {:error, e}
end end
end end
end end

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.OnlyMedia do
@behaviour Pleroma.Upload.Filter
alias Pleroma.Upload
def filter(%Upload{content_type: content_type}) do
[type, _subtype] = String.split(content_type, "/")
if type in ["image", "video", "audio"] do
{:ok, :noop}
else
{:error, "Disallowed content-type: #{content_type}"}
end
end
def filter(_), do: {:ok, :noop}
end

View file

@ -159,6 +159,11 @@ defmodule Pleroma.User do
field(:language, :string) field(:language, :string)
field(:status_ttl_days, :integer, default: nil) field(:status_ttl_days, :integer, default: nil)
field(:accepts_direct_messages_from, Ecto.Enum,
values: [:everybody, :people_i_follow, :nobody],
default: :everybody
)
embeds_one( embeds_one(
:notification_settings, :notification_settings,
Pleroma.User.NotificationSetting, Pleroma.User.NotificationSetting,
@ -536,7 +541,8 @@ def update_changeset(struct, params \\ %{}) do
:is_discoverable, :is_discoverable,
:actor_type, :actor_type,
:disclose_client, :disclose_client,
:status_ttl_days :status_ttl_days,
:accepts_direct_messages_from
] ]
) )
|> unique_constraint(:nickname) |> unique_constraint(:nickname)
@ -870,7 +876,7 @@ def post_register_action(%User{is_approved: true, is_confirmed: true} = user) do
end end
end end
defp send_user_approval_email(user) do defp send_user_approval_email(%User{email: email} = user) when is_binary(email) do
user user
|> Pleroma.Emails.UserEmail.approval_pending_email() |> Pleroma.Emails.UserEmail.approval_pending_email()
|> Pleroma.Emails.Mailer.deliver_async() |> Pleroma.Emails.Mailer.deliver_async()
@ -878,6 +884,10 @@ defp send_user_approval_email(user) do
{:ok, :enqueued} {:ok, :enqueued}
end end
defp send_user_approval_email(_user) do
{:ok, :skipped}
end
defp send_admin_approval_emails(user) do defp send_admin_approval_emails(user) do
all_superusers() all_superusers()
|> Enum.filter(fn user -> not is_nil(user.email) end) |> Enum.filter(fn user -> not is_nil(user.email) end)
@ -2000,7 +2010,6 @@ defp create_service_actor(uri, nickname) do
%User{ %User{
invisible: true, invisible: true,
local: true, local: true,
actor_type: "Application",
ap_id: uri, ap_id: uri,
nickname: nickname, nickname: nickname,
follower_address: uri <> "/followers" follower_address: uri <> "/followers"
@ -2722,4 +2731,16 @@ def unfollow_hashtag(%User{} = user, %Hashtag{} = hashtag) do
def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do def following_hashtag?(%User{} = user, %Hashtag{} = hashtag) do
not is_nil(HashtagFollow.get(user, hashtag)) not is_nil(HashtagFollow.get(user, hashtag))
end end
def accepts_direct_messages?(
%User{accepts_direct_messages_from: :people_i_follow} = receiver,
%User{} = sender
) do
User.following?(receiver, sender)
end
def accepts_direct_messages?(%User{accepts_direct_messages_from: :everybody}, _), do: true
def accepts_direct_messages?(%User{accepts_direct_messages_from: :nobody}, _),
do: false
end end

View file

@ -31,7 +31,7 @@ def show(%User{} = source, %User{} = target) do
UserNote UserNote
|> where(source_id: ^source.id, target_id: ^target.id) |> where(source_id: ^source.id, target_id: ^target.id)
|> Repo.one() do |> Repo.one() do
note.comment note.comment || ""
else else
_ -> "" _ -> ""
end end

View file

@ -147,7 +147,8 @@ def get_policies do
|> Enum.concat([ |> Enum.concat([
Pleroma.Web.ActivityPub.MRF.HashtagPolicy, Pleroma.Web.ActivityPub.MRF.HashtagPolicy,
Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy, Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy,
Pleroma.Web.ActivityPub.MRF.NormalizeMarkup Pleroma.Web.ActivityPub.MRF.NormalizeMarkup,
Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy
]) ])
|> Enum.uniq() |> Enum.uniq()
end end

View file

@ -0,0 +1,65 @@
defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
require Pleroma.Constants
@moduledoc """
Removes entries from the "To" field from direct messages if the user has requested to not
allow direct messages
"""
@impl true
def filter(
%{
"type" => "Create",
"actor" => actor,
"object" => %{
"type" => "Note"
}
} = activity
) do
with recipients <- Map.get(activity, "to", []),
cc <- Map.get(activity, "cc", []),
true <- is_direct?(recipients, cc),
sender <- User.get_cached_by_ap_id(actor) do
new_to =
Enum.filter(recipients, fn recv ->
should_include?(sender, recv)
end)
{:ok,
activity
|> Map.put("to", new_to)
|> maybe_replace_object_to(new_to)}
else
_ ->
{:ok, activity}
end
end
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
defp should_include?(sender, receiver_ap_id) do
with %User{local: true} = receiver <- User.get_cached_by_ap_id(receiver_ap_id) do
User.accepts_direct_messages?(receiver, sender)
else
_ -> true
end
end
defp maybe_replace_object_to(%{"object" => %{"to" => _}} = activity, to) do
Kernel.put_in(activity, ["object", "to"], to)
end
defp maybe_replace_object_to(other, _), do: other
defp is_direct?(to, cc) do
!(Enum.member?(to, Pleroma.Constants.as_public()) ||
Enum.member?(cc, Pleroma.Constants.as_public()))
end
end

View file

@ -0,0 +1,50 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy do
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
alias Pleroma.User
@moduledoc """
Rejects notes from accounts that were created below a certain threshold of time ago
"""
@impl true
def filter(
%{
"type" => type,
"actor" => actor
} = activity
)
when type in ["Note", "Create"] do
min_age = Pleroma.Config.get([:mrf_reject_newly_created_account_notes, :age])
with %User{local: false} = user <- Pleroma.User.get_cached_by_ap_id(actor),
true <- Timex.diff(Timex.now(), user.inserted_at, :seconds) < min_age do
{:reject, "[RejectNewlyCreatedAccountNotesPolicy] Account created too recently"}
else
_ -> {:ok, activity}
end
end
@impl true
def filter(object), do: {:ok, object}
@impl true
def describe, do: {:ok, %{}}
@impl true
def config_description do
%{
key: :mrf_reject_newly_created_account_notes,
related_policy: "Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy",
label: "MRF Reject New Accounts",
description: "Reject notes from accounts created too recently",
children: [
%{
key: :age,
type: :integer,
description: "Time below which to reject (in seconds)",
suggestions: [86_400]
}
]
}
end
end

View file

@ -22,7 +22,10 @@ def cast_and_filter_recipients(message, field, follower_collection, field_fallba
end end
def fix_object_defaults(data) do def fix_object_defaults(data) do
context = Utils.maybe_create_context(data["context"] || data["conversation"]) context =
Utils.maybe_create_context(
data["context"] || data["conversation"] || data["inReplyTo"] || data["id"]
)
%User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"]) %User{follower_address: follower_collection} = User.get_cached_by_ap_id(data["attributedTo"])

View file

@ -920,8 +920,13 @@ def add_attributed_to(object) do
def prepare_attachments(object) do def prepare_attachments(object) do
attachments = attachments =
object case Map.get(object, "attachment", []) do
|> Map.get("attachment", []) [_ | _] = list -> list
_ -> []
end
attachments =
attachments
|> Enum.map(fn data -> |> Enum.map(fn data ->
[%{"mediaType" => media_type, "href" => href} = url | _] = data["url"] [%{"mediaType" => media_type, "href" => href} = url | _] = data["url"]

View file

@ -410,7 +410,7 @@ def blocks_operation do
operationId: "AccountController.blocks", operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}", description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}], security: [%{"oAuth" => ["read:blocks"]}],
parameters: pagination_params(), parameters: [with_relationships_param() | pagination_params()],
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_accounts())
} }
@ -708,6 +708,16 @@ defp update_credentials_request do
nullable: true, nullable: true,
description: description:
"Number of days after which statuses will be deleted. Set to -1 to disable." "Number of days after which statuses will be deleted. Set to -1 to disable."
},
accepts_direct_messages_from: %Schema{
type: :string,
enum: [
"everybody",
"nobody",
"people_i_follow"
],
nullable: true,
description: "Who to accept DMs from"
} }
}, },
example: %{ example: %{
@ -729,7 +739,8 @@ defp update_credentials_request do
also_known_as: ["https://foo.bar/users/foo"], also_known_as: ["https://foo.bar/users/foo"],
discoverable: false, discoverable: false,
actor_type: "Person", actor_type: "Person",
status_ttl_days: 30 status_ttl_days: 30,
accepts_direct_messages_from: "everybody"
} }
} }
end end
@ -756,7 +767,7 @@ defp array_of_relationships do
"showing_reblogs" => true, "showing_reblogs" => true,
"followed_by" => true, "followed_by" => true,
"blocking" => false, "blocking" => false,
"blocked_by" => true, "blocked_by" => false,
"muting" => false, "muting" => false,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "", "note" => "",
@ -772,7 +783,7 @@ defp array_of_relationships do
"showing_reblogs" => true, "showing_reblogs" => true,
"followed_by" => true, "followed_by" => true,
"blocking" => false, "blocking" => false,
"blocked_by" => true, "blocked_by" => false,
"muting" => true, "muting" => true,
"muting_notifications" => false, "muting_notifications" => false,
"note" => "", "note" => "",

View file

@ -143,7 +143,7 @@ def admin_account do
} }
}, },
tags: %Schema{type: :string}, tags: %Schema{type: :string},
is_confirmed: %Schema{type: :string} is_confirmed: %Schema{type: :boolean}
} }
} }
end end

View file

@ -13,7 +13,10 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
description: "Relationship between current account and requested account", description: "Relationship between current account and requested account",
type: :object, type: :object,
properties: %{ properties: %{
blocked_by: %Schema{type: :boolean}, blocked_by: %Schema{
type: :boolean,
description: "Represents being blocked by this user. Always false."
},
blocking: %Schema{type: :boolean}, blocking: %Schema{type: :boolean},
domain_blocking: %Schema{type: :boolean}, domain_blocking: %Schema{type: :boolean},
endorsed: %Schema{type: :boolean}, endorsed: %Schema{type: :boolean},

View file

@ -88,7 +88,7 @@ def reject_follow_request(follower, followed) do
def delete(activity_id, user) do def delete(activity_id, user) do
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <- with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(activity_id)}, {:find_activity, Activity.get_by_id(activity_id, filter: [])},
{_, %Object{} = object, _} <- {_, %Object{} = object, _} <-
{:find_object, Object.normalize(activity, fetch: false), activity}, {:find_object, Object.normalize(activity, fetch: false), activity},
true <- User.superuser?(user) || user.ap_id == object.data["actor"], true <- User.superuser?(user) || user.ap_id == object.data["actor"],

View file

@ -144,6 +144,8 @@ def make_poll_data(%{poll: %{options: options, expires_in: expires_in}} = data)
when is_list(options) do when is_list(options) do
limits = Config.get([:instance, :poll_limits]) limits = Config.get([:instance, :poll_limits])
options = options |> Enum.uniq()
with :ok <- validate_poll_expiration(expires_in, limits), with :ok <- validate_poll_expiration(expires_in, limits),
:ok <- validate_poll_options_amount(options, limits), :ok <- validate_poll_options_amount(options, limits),
:ok <- validate_poll_options_length(options, limits) do :ok <- validate_poll_options_length(options, limits) do
@ -179,9 +181,14 @@ def make_poll_data(_data) do
end end
defp validate_poll_options_amount(options, %{max_options: max_options}) do defp validate_poll_options_amount(options, %{max_options: max_options}) do
if Enum.count(options) > max_options do cond do
Enum.count(options) < 2 ->
{:error, "Poll must contain at least 2 options"}
Enum.count(options) > max_options ->
{:error, "Poll can't contain more than #{max_options} options"} {:error, "Poll can't contain more than #{max_options} options"}
else
true ->
:ok :ok
end end
end end

View file

@ -11,22 +11,31 @@ defmodule Pleroma.Web.EmbedController do
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
plug(:put_layout, :embed)
def show(conn, %{"id" => id}) do def show(conn, %{"id" => id}) do
with %Activity{local: true} = activity <- with {:activity, %Activity{} = activity} <-
Activity.get_by_id_with_object(id), {:activity, Activity.get_by_id_with_object(id)},
true <- Visibility.is_public?(activity.object) do {:local, true} <- {:local, activity.local},
{:visible, true} <- {:visible, Visibility.visible_for_user?(activity, nil)} do
{:ok, author} = User.get_or_fetch(activity.object.data["actor"]) {:ok, author} = User.get_or_fetch(activity.object.data["actor"])
conn conn
|> delete_resp_header("x-frame-options") |> delete_resp_header("x-frame-options")
|> delete_resp_header("content-security-policy") |> delete_resp_header("content-security-policy")
|> put_view(Pleroma.Web.EmbedView)
|> render("show.html", |> render("show.html",
activity: activity, activity: activity,
author: User.sanitize_html(author), author: User.sanitize_html(author),
counts: get_counts(activity) counts: get_counts(activity)
) )
else
{:activity, _} ->
render_error(conn, :not_found, "Post not found")
{:local, false} ->
render_error(conn, :unauthorized, "Federated posts cannot be embedded")
{:visible, false} ->
render_error(conn, :unauthorized, "Not authorized to view this post")
end end
end end

View file

@ -221,6 +221,7 @@ def update_credentials(%{assigns: %{user: user}, body_params: params} = conn, _p
|> Maps.put_if_present(:is_discoverable, params[:discoverable]) |> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language])) |> Maps.put_if_present(:language, Pleroma.Web.Gettext.normalize_locale(params[:language]))
|> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value) |> Maps.put_if_present(:status_ttl_days, params[:status_ttl_days], status_ttl_days_value)
|> Maps.put_if_present(:accepts_direct_messages_from, params[:accepts_direct_messages_from])
# What happens here: # What happens here:
# #
@ -517,7 +518,12 @@ def blocks(%{assigns: %{user: user}} = conn, params) do
conn conn
|> add_link_headers(users) |> add_link_headers(users)
|> render("index.json", users: users, for: user, as: :user) |> render("index.json",
users: users,
for: user,
as: :user,
embed_relationships: embed_relationships?(params)
)
end end
@doc "GET /api/v1/accounts/lookup" @doc "GET /api/v1/accounts/lookup"

View file

@ -124,14 +124,7 @@ def render(
target, target,
&User.blocks_user?(&1, &2) &User.blocks_user?(&1, &2)
), ),
blocked_by: blocked_by: false,
UserRelationship.exists?(
user_relationships,
:block,
target,
reading_user,
&User.blocks_user?(&1, &2)
),
muting: muting:
UserRelationship.exists?( UserRelationship.exists?(
user_relationships, user_relationships,
@ -354,6 +347,7 @@ defp maybe_put_settings(
|> Kernel.put_in([:source, :privacy], user.default_scope) |> Kernel.put_in([:source, :privacy], user.default_scope)
|> Kernel.put_in([:source, :pleroma, :show_role], user.show_role) |> Kernel.put_in([:source, :pleroma, :show_role], user.show_role)
|> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text) |> Kernel.put_in([:source, :pleroma, :no_rich_text], user.no_rich_text)
|> Kernel.put_in([:accepts_direct_messages_from], user.accepts_direct_messages_from)
end end
defp maybe_put_settings(data, _, _, _), do: data defp maybe_put_settings(data, _, _, _), do: data

View file

@ -52,7 +52,7 @@ def url(url) do
@spec url_proxiable?(String.t()) :: boolean() @spec url_proxiable?(String.t()) :: boolean()
def url_proxiable?(url) do def url_proxiable?(url) do
not local?(url) and not whitelisted?(url) not local?(url) and not whitelisted?(url) and not blocked?(url)
end end
def preview_url(url, preview_params \\ []) do def preview_url(url, preview_params \\ []) do
@ -83,6 +83,16 @@ def whitelisted?(url) do
domain in mediaproxy_whitelist_domains domain in mediaproxy_whitelist_domains
end end
def blocked?(url) do
%{scheme: scheme, host: domain} = URI.parse(url)
# Block either the bare domain or the scheme-domain combo
scheme_domain = "#{scheme}://#{domain}"
blocklist = Config.get([:media_proxy, :blocklist])
Enum.member?(blocklist, domain) ||
Enum.member?(blocklist, scheme_domain)
end
defp maybe_get_domain_from_url("http" <> _ = url) do defp maybe_get_domain_from_url("http" <> _ = url) do
URI.parse(url).host URI.parse(url).host
end end

View file

@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
@impl Provider @impl Provider
def build_tags(%{user: user}) do def build_tags(%{user: user}) do
bio_tree = Floki.parse_fragment!(user.bio) profile_tree =
user.bio
|> append_fields_tag(user.fields)
|> Floki.parse_fragment!()
(Floki.attribute(bio_tree, "link[rel~=me]", "href") ++ (Floki.attribute(profile_tree, "link[rel~=me]", "href") ++
Floki.attribute(bio_tree, "a[rel~=me]", "href")) Floki.attribute(profile_tree, "a[rel~=me]", "href"))
|> Enum.map(fn link -> |> Enum.map(fn link ->
{:link, [rel: "me", href: link], []} {:link, [rel: "me", href: link], []}
end) end)
end end
defp append_fields_tag(bio, fields) do
fields
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
end
end end

View file

@ -20,12 +20,12 @@ def build_tags(%{activity_id: id, object: object, user: user}) do
[ [
title_tag(user), title_tag(user),
{:meta, [property: "twitter:description", content: scrubbed_content], []} {:meta, [name: "twitter:description", content: scrubbed_content], []}
] ++ ] ++
if attachments == [] or Metadata.activity_nsfw?(object) do if attachments == [] or Metadata.activity_nsfw?(object) do
[ [
image_tag(user), image_tag(user),
{:meta, [property: "twitter:card", content: "summary"], []} {:meta, [name: "twitter:card", content: "summary"], []}
] ]
else else
attachments attachments
@ -37,20 +37,19 @@ def build_tags(%{user: user}) do
with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio) do
[ [
title_tag(user), title_tag(user),
{:meta, [property: "twitter:description", content: truncated_bio], []}, {:meta, [name: "twitter:description", content: truncated_bio], []},
image_tag(user), image_tag(user),
{:meta, [property: "twitter:card", content: "summary"], []} {:meta, [name: "twitter:card", content: "summary"], []}
] ]
end end
end end
defp title_tag(user) do defp title_tag(user) do
{:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} {:meta, [name: "twitter:title", content: Utils.user_name_string(user)], []}
end end
def image_tag(user) do def image_tag(user) do
{:meta, [property: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], {:meta, [name: "twitter:image", content: MediaProxy.preview_url(User.avatar_url(user))], []}
[]}
end end
defp build_attachments(id, %{data: %{"attachment" => attachments}}) do defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
@ -60,10 +59,10 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
case Utils.fetch_media_type(@media_types, url["mediaType"]) do case Utils.fetch_media_type(@media_types, url["mediaType"]) do
"audio" -> "audio" ->
[ [
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [name: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player:width", content: "480"], []}, {:meta, [name: "twitter:player:width", content: "480"], []},
{:meta, [property: "twitter:player:height", content: "80"], []}, {:meta, [name: "twitter:player:height", content: "80"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []} {:meta, [name: "twitter:player", content: player_url(id)], []}
| acc | acc
] ]
@ -74,10 +73,10 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
# workaround. # workaround.
"image" -> "image" ->
[ [
{:meta, [property: "twitter:card", content: "summary_large_image"], []}, {:meta, [name: "twitter:card", content: "summary_large_image"], []},
{:meta, {:meta,
[ [
property: "twitter:player", name: "twitter:player",
content: MediaProxy.url(url["href"]) content: MediaProxy.url(url["href"])
], []} ], []}
| acc | acc
@ -90,14 +89,14 @@ defp build_attachments(id, %{data: %{"attachment" => attachments}}) do
width = url["width"] || 480 width = url["width"] || 480
[ [
{:meta, [property: "twitter:card", content: "player"], []}, {:meta, [name: "twitter:card", content: "player"], []},
{:meta, [property: "twitter:player", content: player_url(id)], []}, {:meta, [name: "twitter:player", content: player_url(id)], []},
{:meta, [property: "twitter:player:width", content: "#{width}"], []}, {:meta, [name: "twitter:player:width", content: "#{width}"], []},
{:meta, [property: "twitter:player:height", content: "#{height}"], []}, {:meta, [name: "twitter:player:height", content: "#{height}"], []},
{:meta, [property: "twitter:player:stream", content: MediaProxy.url(url["href"])], {:meta, [name: "twitter:player:stream", content: MediaProxy.url(url["href"])],
[]}, []},
{:meta, {:meta, [name: "twitter:player:stream:content_type", content: url["mediaType"]],
[property: "twitter:player:stream:content_type", content: url["mediaType"]], []} []}
| acc | acc
] ]
@ -123,8 +122,8 @@ defp maybe_add_dimensions(metadata, url) do
!is_nil(url["height"]) && !is_nil(url["width"]) -> !is_nil(url["height"]) && !is_nil(url["width"]) ->
metadata ++ metadata ++
[ [
{:meta, [property: "twitter:player:width", content: "#{url["width"]}"], []}, {:meta, [name: "twitter:player:width", content: "#{url["width"]}"], []},
{:meta, [property: "twitter:player:height", content: "#{url["height"]}"], []} {:meta, [name: "twitter:player:height", content: "#{url["height"]}"], []}
] ]
true -> true ->

View file

@ -55,7 +55,7 @@ def scrub_html(content), do: content
def user_name_string(user) do def user_name_string(user) do
"#{user.name} " <> "#{user.name} " <>
if user.local do if user.local do
"(@#{user.nickname}@#{Pleroma.Web.Endpoint.host()})" "(@#{user.nickname}@#{Pleroma.Web.WebFinger.domain()})"
else else
"(@#{user.nickname})" "(@#{user.nickname})"
end end

View file

@ -41,6 +41,17 @@ def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
end end
end end
defp filter_allowed_user_by_ap_id(ap_ids, excluded_ap_ids) do
Enum.reject(ap_ids, fn ap_id ->
with false <- ap_id in excluded_ap_ids,
%{is_active: true} <- User.get_cached_by_ap_id(ap_id) do
false
else
_ -> true
end
end)
end
def filter_allowed_users(reactions, user, with_muted) do def filter_allowed_users(reactions, user, with_muted) do
exclude_ap_ids = exclude_ap_ids =
if is_nil(user) do if is_nil(user) do
@ -51,7 +62,7 @@ def filter_allowed_users(reactions, user, with_muted) do
end end
filter_emoji = fn emoji, users, url -> filter_emoji = fn emoji, users, url ->
case Enum.reject(users, &(&1 in exclude_ap_ids)) do case filter_allowed_user_by_ap_id(users, exclude_ap_ids) do
[] -> nil [] -> nil
users -> {emoji, users, url} users -> {emoji, users, url}
end end

View file

@ -37,7 +37,7 @@ def call(%{request_path: <<"/", @path, "/", file::binary>>} = conn, opts) do
%{query_params: %{"name" => name}} = conn -> %{query_params: %{"name" => name}} = conn ->
name = escape_header_value(name) name = escape_header_value(name)
put_resp_header(conn, "content-disposition", "filename=\"#{name}\"") put_resp_header(conn, "content-disposition", ~s[inline; filename="#{name}"])
conn -> conn ->
conn conn

View file

@ -6,8 +6,8 @@ defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do
def parse(html, _data) do def parse(html, _data) do
with elements = [_ | _] <- get_discovery_data(html), with elements = [_ | _] <- get_discovery_data(html),
oembed_url when is_binary(oembed_url) <- get_oembed_url(elements), oembed_url when is_binary(oembed_url) <- get_oembed_url(elements),
{:ok, oembed_data} <- get_oembed_data(oembed_url) do {:ok, oembed_data = %{"html" => html}} <- get_oembed_data(oembed_url) do
oembed_data %{oembed_data | "html" => Pleroma.HTML.filter_tags(html)}
else else
_e -> %{} _e -> %{}
end end

View file

@ -25,6 +25,7 @@ defmodule Pleroma.Web.Streamer do
def registry, do: @registry def registry, do: @registry
@public_streams ["public", "public:local", "public:media", "public:local:media"] @public_streams ["public", "public:local", "public:media", "public:local:media"]
@local_streams ["public:local", "public:local:media"]
@user_streams ["user", "user:notification", "direct"] @user_streams ["user", "user:notification", "direct"]
@doc "Expands and authorizes a stream, and registers the process for streaming." @doc "Expands and authorizes a stream, and registers the process for streaming."
@ -41,14 +42,37 @@ def get_topic_and_add_socket(stream, user, oauth_token, params \\ %{}) do
end end
end end
defp can_access_stream(user, oauth_token, kind) do
with {_, true} <- {:restrict?, Config.restrict_unauthenticated_access?(:timelines, kind)},
{_, %User{id: user_id}, %Token{user_id: user_id}} <- {:user, user, oauth_token},
{_, true} <-
{:scopes,
OAuthScopesPlug.filter_descendants(["read:statuses"], oauth_token.scopes) != []} do
true
else
{:restrict?, _} ->
true
_ ->
false
end
end
@doc "Expand and authorizes a stream" @doc "Expand and authorizes a stream"
@spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) :: @spec get_topic(stream :: String.t(), User.t() | nil, Token.t() | nil, Map.t()) ::
{:ok, topic :: String.t()} | {:error, :bad_topic} {:ok, topic :: String.t()} | {:error, :bad_topic}
def get_topic(stream, user, oauth_token, params \\ %{}) def get_topic(stream, user, oauth_token, params \\ %{})
# Allow all public steams. # Allow all public steams if the instance allows unauthenticated access.
def get_topic(stream, _user, _oauth_token, _params) when stream in @public_streams do # Otherwise, only allow users with valid oauth tokens.
def get_topic(stream, user, oauth_token, _params) when stream in @public_streams do
kind = if stream in @local_streams, do: :local, else: :federated
if can_access_stream(user, oauth_token, kind) do
{:ok, stream} {:ok, stream}
else
{:error, :unauthorized}
end
end end
# Allow all hashtags streams. # Allow all hashtags streams.
@ -57,12 +81,20 @@ def get_topic("hashtag", _user, _oauth_token, %{"tag" => tag} = _params) do
end end
# Allow remote instance streams. # Allow remote instance streams.
def get_topic("public:remote", _user, _oauth_token, %{"instance" => instance} = _params) do def get_topic("public:remote", user, oauth_token, %{"instance" => instance} = _params) do
if can_access_stream(user, oauth_token, :federated) do
{:ok, "public:remote:" <> instance} {:ok, "public:remote:" <> instance}
else
{:error, :unauthorized}
end
end end
def get_topic("public:remote:media", _user, _oauth_token, %{"instance" => instance} = _params) do def get_topic("public:remote:media", user, oauth_token, %{"instance" => instance} = _params) do
if can_access_stream(user, oauth_token, :federated) do
{:ok, "public:remote:media:" <> instance} {:ok, "public:remote:media:" <> instance}
else
{:error, :unauthorized}
end
end end
# Expand user streams. # Expand user streams.

View file

@ -6,10 +6,10 @@
<title><%= Pleroma.Config.get([:instance, :name]) %></title> <title><%= Pleroma.Config.get([:instance, :name]) %></title>
<meta content='noindex' name='robots'> <meta content='noindex' name='robots'>
<%= Phoenix.HTML.raw(assigns[:meta] || "") %> <%= Phoenix.HTML.raw(assigns[:meta] || "") %>
<link rel="stylesheet" href="/embed.css"> <link rel="stylesheet" href="/embed/embed.css">
<base target="_parent"> <base target="_parent">
</head> </head>
<body> <body>
<%= render @view_module, @view_template, assigns %> <%= render view_module(@conn), view_template(@conn), assigns %>
</body> </body>
</html> </html>

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.EmbedView do
alias Pleroma.Web.Metadata.Utils alias Pleroma.Web.Metadata.Utils
alias Pleroma.Web.Router.Helpers alias Pleroma.Web.Router.Helpers
use Phoenix.HTML import Phoenix.HTML
defdelegate full_nickname(user), to: User defdelegate full_nickname(user), to: User
@ -55,10 +55,13 @@ defp activity_url(%User{local: false}, %Activity{object: %Object{data: data}}) d
data["url"] || data["external_url"] || data["id"] data["url"] || data["external_url"] || data["id"]
end end
defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}}) do defp attachments(%Activity{object: %Object{data: %{"attachment" => attachments}}})
when is_list(attachments) do
attachments attachments
end end
defp attachments(_), do: []
defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do defp sensitive?(%Activity{object: %Object{data: %{"sensitive" => sensitive}}}) do
sensitive sensitive
end end

View file

@ -96,7 +96,7 @@ def represent_user(user, "XML") do
|> XmlBuilder.to_doc() |> XmlBuilder.to_doc()
end end
defp domain do def domain do
Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host() Pleroma.Config.get([__MODULE__, :domain]) || Pleroma.Web.Endpoint.host()
end end

View file

@ -134,7 +134,7 @@ defp deps do
{:tesla, "~> 1.4.4"}, {:tesla, "~> 1.4.4"},
{:castore, "~> 0.1"}, {:castore, "~> 0.1"},
{:cowlib, "~> 2.9"}, {:cowlib, "~> 2.9"},
{:finch, "~> 0.14.0"}, {:finch, "~> 0.16.0"},
{:jason, "~> 1.2"}, {:jason, "~> 1.2"},
{:trailing_format_plug, "~> 0.0.7"}, {:trailing_format_plug, "~> 0.0.7"},
{:mogrify, "~> 0.9.1"}, {:mogrify, "~> 0.9.1"},

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -3,16 +3,16 @@ msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-09-20 13:18+0000\n" "POT-Creation-Date: 2020-09-20 13:18+0000\n"
"PO-Revision-Date: 2020-12-14 06:00+0000\n" "PO-Revision-Date: 2023-02-26 08:57+0000\n"
"Last-Translator: shironeko <shironeko@tesaguri.club>\n" "Last-Translator: SevicheCC <sevicheee@outlook.com>\n"
"Language-Team: Chinese (Simplified) <https://translate.pleroma.social/" "Language-Team: Chinese (Simplified) <http://translate.akkoma.dev/projects/"
"projects/pleroma/pleroma/zh_Hans/>\n" "akkoma/akkoma-backend-errors/zh_Hans/>\n"
"Language: zh_Hans\n" "Language: zh_Hans\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n" "Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.0.4\n" "X-Generator: Weblate 4.14\n"
## This file is a PO Template file. ## This file is a PO Template file.
## ##
@ -146,7 +146,7 @@ msgid "Cannot post an empty status without attachments"
msgstr "无法发送空白且不包含附件的状态" msgstr "无法发送空白且不包含附件的状态"
#: lib/pleroma/web/common_api/utils.ex:511 #: lib/pleroma/web/common_api/utils.ex:511
#, elixir-format, fuzzy #, elixir-format
msgid "Comment must be up to %{max_size} characters" msgid "Comment must be up to %{max_size} characters"
msgstr "评论最多可使用 %{max_size} 字符" msgstr "评论最多可使用 %{max_size} 字符"
@ -250,7 +250,7 @@ msgstr "没有该对话"
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:388
#: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456 #: lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:414 lib/pleroma/web/admin_api/controllers/admin_api_controller.ex:456
#, elixir-format, fuzzy #, elixir-format
msgid "No such permission_group" msgid "No such permission_group"
msgstr "没有该权限组" msgstr "没有该权限组"
@ -297,7 +297,7 @@ msgid "This resource requires authentication."
msgstr "该资源需要认证。" msgstr "该资源需要认证。"
#: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206 #: lib/pleroma/plugs/rate_limiter/rate_limiter.ex:206
#, elixir-format, fuzzy #, elixir-format
msgid "Throttled" msgid "Throttled"
msgstr "节流了" msgstr "节流了"
@ -380,7 +380,7 @@ msgid "Failed"
msgstr "失败" msgstr "失败"
#: lib/pleroma/web/oauth/oauth_controller.ex:410 #: lib/pleroma/web/oauth/oauth_controller.ex:410
#, elixir-format, fuzzy #, elixir-format
msgid "Failed to authenticate: %{message}." msgid "Failed to authenticate: %{message}."
msgstr "认证失败:%{message}。" msgstr "认证失败:%{message}。"
@ -406,7 +406,7 @@ msgid "Invalid Username/Password"
msgstr "无效的用户名/密码" msgstr "无效的用户名/密码"
#: lib/pleroma/web/twitter_api/twitter_api.ex:118 #: lib/pleroma/web/twitter_api/twitter_api.ex:118
#, elixir-format, fuzzy #, elixir-format
msgid "Invalid answer data" msgid "Invalid answer data"
msgstr "无效的回答数据" msgstr "无效的回答数据"
@ -437,7 +437,7 @@ msgid "Unsupported OAuth provider: %{provider}."
msgstr "不支持的 OAuth 提供者:%{provider}。" msgstr "不支持的 OAuth 提供者:%{provider}。"
#: lib/pleroma/uploaders/uploader.ex:72 #: lib/pleroma/uploaders/uploader.ex:72
#, elixir-format, fuzzy #, elixir-format
msgid "Uploader callback timeout" msgid "Uploader callback timeout"
msgstr "上传回复超时" msgstr "上传回复超时"
@ -452,7 +452,7 @@ msgid "CAPTCHA Error"
msgstr "验证码错误" msgstr "验证码错误"
#: lib/pleroma/web/common_api/common_api.ex:290 #: lib/pleroma/web/common_api/common_api.ex:290
#, elixir-format, fuzzy #, elixir-format
msgid "Could not add reaction emoji" msgid "Could not add reaction emoji"
msgstr "无法添加表情反应" msgstr "无法添加表情反应"

View file

@ -0,0 +1,163 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-23 12:40+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Automatically generated\n"
"Language-Team: none\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Translate Toolkit 3.7.2\n"
## This file is a PO Template file.
##
## `msgid`s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run `mix gettext.extract` to bring this file up to
## date. Leave `msgstr`s empty as changing them here as no
## effect: edit them in PO (`.po`) files instead.
msgid "eperm"
msgstr ""
msgid "eacces"
msgstr ""
msgid "eagain"
msgstr ""
msgid "ebadf"
msgstr ""
msgid "ebadmsg"
msgstr ""
msgid "ebusy"
msgstr ""
msgid "edeadlk"
msgstr ""
msgid "edeadlock"
msgstr ""
msgid "edquot"
msgstr ""
msgid "eexist"
msgstr ""
msgid "efault"
msgstr ""
msgid "efbig"
msgstr ""
msgid "eftype"
msgstr ""
msgid "eintr"
msgstr ""
msgid "einval"
msgstr ""
msgid "eio"
msgstr ""
msgid "eisdir"
msgstr ""
msgid "eloop"
msgstr ""
msgid "emfile"
msgstr ""
msgid "emlink"
msgstr ""
msgid "emultihop"
msgstr ""
msgid "enametoolong"
msgstr ""
msgid "enfile"
msgstr ""
msgid "enobufs"
msgstr ""
msgid "enodev"
msgstr ""
msgid "enolck"
msgstr ""
msgid "enolink"
msgstr ""
msgid "enoent"
msgstr ""
msgid "enomem"
msgstr ""
msgid "enospc"
msgstr ""
msgid "enosr"
msgstr ""
msgid "enostr"
msgstr ""
msgid "enosys"
msgstr ""
msgid "enotblk"
msgstr ""
msgid "enotdir"
msgstr ""
msgid "enotsup"
msgstr ""
msgid "enxio"
msgstr ""
msgid "eopnotsupp"
msgstr ""
msgid "eoverflow"
msgstr ""
msgid "epipe"
msgstr ""
msgid "erange"
msgstr ""
msgid "erofs"
msgstr ""
msgid "espipe"
msgstr ""
msgid "esrch"
msgstr ""
msgid "estale"
msgstr ""
msgid "etxtbsy"
msgstr ""
msgid "exdev"
msgstr ""

View file

@ -0,0 +1,601 @@
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-02-23 12:41+0000\n"
"PO-Revision-Date: 2023-02-24 13:57+0000\n"
"Last-Translator: SevicheCC <sevicheee@outlook.com>\n"
"Language-Team: Chinese (Simplified) <http://translate.akkoma.dev/projects/"
"akkoma/akkoma-backend-static-pages/zh_Hans/>\n"
"Language: zh_Hans\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.14\n"
## This file is a PO Template file.
##
## "msgid"s here are often extracted from source code.
## Add new translations manually only if they're dynamic
## translations that can't be statically extracted.
##
## Run "mix gettext.extract" to bring this file up to
## date. Leave "msgstr"s empty as changing them here as no
## effect: edit them in PO (.po) files instead.
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:9
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button"
msgid "Authorize"
msgstr "批准"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error fetching user"
msgstr "无法获取用户信息"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header"
msgid "Remote follow"
msgstr "跨站关注"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "placeholder text for auth code entry"
msgid "Authentication code"
msgstr "验证码"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:10
#, elixir-autogen, elixir-format
msgctxt "placeholder text for password entry"
msgid "Password"
msgstr "密码"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "placeholder text for username entry"
msgid "Username"
msgstr "用户名"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:13
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for login"
msgid "Authorize"
msgstr "授权"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for mfa"
msgid "Authorize"
msgstr "授权"
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error following account"
msgstr "无法关注账户"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_login.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header, need login"
msgid "Log in to follow"
msgstr "登录以关注"
#: lib/pleroma/web/templates/twitter_api/remote_follow/follow_mfa.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow mfa header"
msgid "Two-factor authentication"
msgstr "双因素身份验证"
#: lib/pleroma/web/templates/twitter_api/remote_follow/followed.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow success"
msgid "Account followed!"
msgstr "已关注该账号!"
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:7
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:7
#, elixir-autogen, elixir-format
msgctxt "placeholder text for account id"
msgid "Your account ID, e.g. lain@quitter.se"
msgstr "你的账号ID比如lain@quitter.se"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "remote follow authorization button for following with a remote account"
msgid "Follow"
msgstr "关注"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "remote follow error"
msgid "Error: %{error}"
msgstr "错误:%{error}"
#: lib/pleroma/web/templates/twitter_api/util/subscribe.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "remote follow header"
msgid "Remotely follow %{nickname}"
msgstr "远程关注 %{nickname}"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:12
#, elixir-autogen, elixir-format
msgctxt "password reset button"
msgid "Reset"
msgstr "重制"
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "password reset failed homepage link"
msgid "Homepage"
msgstr "主页"
#: lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset failed message"
msgid "Password reset failed"
msgstr "密码重置失败"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "password reset form confirm password prompt"
msgid "Confirmation"
msgstr "确认"
#: lib/pleroma/web/templates/twitter_api/password/reset.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "password reset form password prompt"
msgid "Password"
msgstr "密码"
#: lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset invalid token message"
msgid "Invalid Token"
msgstr "无效 Token"
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "password reset successful homepage link"
msgid "Homepage"
msgstr "主页"
#: lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "password reset successful message"
msgid "Password changed!"
msgstr "密码已修改!"
#: lib/pleroma/web/templates/feed/feed/tag.atom.eex:15
#: lib/pleroma/web/templates/feed/feed/tag.rss.eex:7
#, elixir-autogen, elixir-format
msgctxt "tag feed description"
msgid "These are public toots tagged with #%{tag}. You can interact with them if you have an account anywhere in the fediverse."
msgstr "这些是带有 #%{tag} "
"标签的公开帖文。如果你在联邦宇宙的任何地方有账户,你可以与它们进行交互。"
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:3
#, elixir-autogen, elixir-format
msgctxt "oauth authorization exists page title"
msgid "Authorization exists"
msgstr "已存在授权"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:37
#, elixir-autogen, elixir-format
msgctxt "oauth authorize approve button"
msgid "Approve"
msgstr "允许"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:35
#, elixir-autogen, elixir-format
msgctxt "oauth authorize cancel button"
msgid "Cancel"
msgstr "取消"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:26
#, elixir-autogen, elixir-format
msgctxt "oauth authorize message"
msgid "Application <strong>%{client_name}</strong> is requesting access to your account."
msgstr "应用程序 <strong>%{client_name}</strong> 正在请求访问您的帐户。"
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:3
#, elixir-autogen, elixir-format
msgctxt "oauth authorized page title"
msgid "Successfully authorized"
msgstr "授权成功"
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "oauth external provider page title"
msgid "Sign in with external provider"
msgstr "使用外部服务进行登录"
#: lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex:13
#, elixir-autogen, elixir-format
msgctxt "oauth external provider sign in button"
msgid "Sign in with %{strategy}"
msgstr "用 %{strategy} 登录"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:59
#, elixir-autogen, elixir-format
msgctxt "oauth login button"
msgid "Log In"
msgstr "登录"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:56
#, elixir-autogen, elixir-format
msgctxt "oauth login password prompt"
msgid "Password"
msgstr "密码"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:52
#, elixir-autogen, elixir-format
msgctxt "oauth login username prompt"
msgid "Username"
msgstr "用户名"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:44
#, elixir-autogen, elixir-format
msgctxt "oauth register nickname prompt"
msgid "Pleroma Handle"
msgstr "Pleroma账号"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:42
#, elixir-autogen, elixir-format
msgctxt "oauth register nickname unchangeable warning"
msgid "Choose carefully! You won't be able to change this later. You will be able to change your display name, though."
msgstr "选择时要慎重!您以后将无法更改此项。不过您可以更改您的显示名称。"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:18
#, elixir-autogen, elixir-format
msgctxt "oauth register page email prompt"
msgid "Email"
msgstr "邮箱"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:10
#, elixir-autogen, elixir-format
msgctxt "oauth register page fill form prompt"
msgid "If you'd like to register a new account, please provide the details below."
msgstr "如果您想注册一个新账户,请提供以下细节。"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:35
#, elixir-autogen, elixir-format
msgctxt "oauth register page login button"
msgid "Proceed as existing user"
msgstr "以现有用户身份进行"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:31
#, elixir-autogen, elixir-format
msgctxt "oauth register page login password prompt"
msgid "Password"
msgstr "密码"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:24
#, elixir-autogen, elixir-format
msgctxt "oauth register page login prompt"
msgid "Alternatively, sign in to connect to existing account."
msgstr "或者登录后连接到现有账户。"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:27
#, elixir-autogen, elixir-format
msgctxt "oauth register page login username prompt"
msgid "Name or email"
msgstr "名字或者邮箱"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:14
#, elixir-autogen, elixir-format
msgctxt "oauth register page nickname prompt"
msgid "Nickname"
msgstr "昵称"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:22
#, elixir-autogen, elixir-format
msgctxt "oauth register page register button"
msgid "Proceed as new user"
msgstr "以新用户身份进行"
#: lib/pleroma/web/templates/o_auth/o_auth/register.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "oauth register page title"
msgid "Registration Details"
msgstr "注册细节"
#: lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "oauth scopes message"
msgid "The following permissions will be granted"
msgstr "将授予以下权限"
#: lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex:6
#: lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex:6
#, elixir-autogen, elixir-format
msgctxt "oauth token code message"
msgid "Token code is <br>%{token}"
msgstr "Token 码是<br>%{token}"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:14
#, elixir-autogen, elixir-format
msgctxt "mfa auth code prompt"
msgid "Authentication code"
msgstr "授权码"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:9
#, elixir-autogen, elixir-format
msgctxt "mfa auth page title"
msgid "Two-factor authentication"
msgstr "双因素身份验证"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:25
#, elixir-autogen, elixir-format
msgctxt "mfa auth page use recovery code link"
msgid "Enter a two-factor recovery code"
msgstr "输入一个双因素恢复的恢复代码"
#: lib/pleroma/web/templates/o_auth/mfa/totp.html.eex:22
#, elixir-autogen, elixir-format
msgctxt "mfa auth verify code button"
msgid "Verify"
msgstr "认证"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:9
#, elixir-autogen, elixir-format
msgctxt "mfa recover page title"
msgid "Two-factor recovery"
msgstr "双因素恢复"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:14
#, elixir-autogen, elixir-format
msgctxt "mfa recover recovery code prompt"
msgid "Recovery code"
msgstr "恢复码"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:25
#, elixir-autogen, elixir-format
msgctxt "mfa recover use 2fa code link"
msgid "Enter a two-factor code"
msgstr "输入一个双重因素验证码"
#: lib/pleroma/web/templates/o_auth/mfa/recovery.html.eex:22
#, elixir-autogen, elixir-format
msgctxt "mfa recover verify recovery code button"
msgid "Verify"
msgstr "验证"
#: lib/pleroma/web/templates/static_fe/static_fe/profile.html.eex:42
#, elixir-autogen, elixir-format
msgctxt "static fe profile page remote follow button"
msgid "Remote follow"
msgstr "跨站关注"
#: lib/pleroma/web/templates/email/digest.html.eex:163
#, elixir-autogen, elixir-format
msgctxt "digest email header line"
msgid "Hey %{nickname}, here is what you've missed!"
msgstr "嗨 %{nickname},这是你错过的一些东西!"
#: lib/pleroma/web/templates/email/digest.html.eex:544
#, elixir-autogen, elixir-format
msgctxt "digest email receiver address"
msgid "The email address you are subscribed as is <a href='mailto:%{@user.email}' style='color: %{color};text-decoration: none;'>%{email}</a>. "
msgstr ""
"您订阅的电子邮件地址是 <a href='mailto:%{@user.email}' style='color: %{color"
"};text-decoration: none;'>%{email}</a>。 "
#: lib/pleroma/web/templates/email/digest.html.eex:538
#, elixir-autogen, elixir-format
msgctxt "digest email sending reason"
msgid "You have received this email because you have signed up to receive digest emails from <b>%{instance}</b> Pleroma instance."
msgstr "您之所以会收到来自<b>%{instance}</b> Pleroma "
"实例的邮件摘要,是因为您已经注册了该服务实例。"
#: lib/pleroma/web/templates/email/digest.html.eex:547
#, elixir-autogen, elixir-format
msgctxt "digest email unsubscribe action"
msgid "To unsubscribe, please go %{here}."
msgstr "取消订阅,请点击 %{here}."
#: lib/pleroma/web/templates/email/digest.html.eex:547
#, elixir-autogen, elixir-format
msgctxt "digest email unsubscribe action link text"
msgid "here"
msgstr "这里"
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "mailer unsubscribe failed message"
msgid "UNSUBSCRIBE FAILURE"
msgstr "取消订阅失败"
#: lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex:1
#, elixir-autogen, elixir-format
msgctxt "mailer unsubscribe successful message"
msgid "UNSUBSCRIBE SUCCESSFUL"
msgstr "成功取消订阅"
#: lib/pleroma/web/templates/email/digest.html.eex:385
#, elixir-format
msgctxt "new followers count header"
msgid "%{count} New Follower"
msgid_plural "%{count} New Followers"
msgstr[0] "%{count} 个新关注者"
#: lib/pleroma/emails/user_email.ex:384
#, elixir-autogen, elixir-format
msgctxt "account archive email subject"
msgid "Your account archive is ready"
msgstr "您的账户档案已经准备好了"
#: lib/pleroma/emails/user_email.ex:188
#, elixir-autogen, elixir-format
msgctxt "approval pending email body"
msgid "<h3>Awaiting Approval</h3>\n<p>Your account at %{instance_name} is being reviewed by staff. You will receive another email once your account is approved.</p>\n"
msgstr ""
"<h3>正在等待批准</h3>\n"
"<p> 您在%{instance_name}的账户正在被工作人员审查。一旦您的账户被批准通过,您"
"将收到另一封电子邮件。</p>\n"
#: lib/pleroma/emails/user_email.ex:202
#, elixir-autogen, elixir-format
msgctxt "approval pending email subject"
msgid "Your account is awaiting approval"
msgstr "您的账户正在等待审批"
#: lib/pleroma/emails/user_email.ex:158
#, elixir-autogen, elixir-format
msgctxt "confirmation email body"
msgid "<h3>Thank you for registering on %{instance_name}</h3>\n<p>Email confirmation is required to activate the account.</p>\n<p>Please click the following link to <a href=\"%{confirmation_url}\">activate your account</a>.</p>\n"
msgstr ""
"<h3>感谢注册%{instance_name}</h3>\n"
"<p>需要电子邮件确认才能激活该账户</p>\n"
"<p>请点击以下链接以 <a href=\"%{confirmation_url}\">确认您的账户</a></p>\n"
#: lib/pleroma/emails/user_email.ex:174
#, elixir-autogen, elixir-format
msgctxt "confirmation email subject"
msgid "%{instance_name} account confirmation"
msgstr "%{instance_name} 账户确认"
#: lib/pleroma/emails/user_email.ex:310
#, elixir-autogen, elixir-format
msgctxt "digest email subject"
msgid "Your digest from %{instance_name}"
msgstr "您来自 %{instance_name} 的摘要邮件"
#: lib/pleroma/emails/user_email.ex:81
#, elixir-autogen, elixir-format
msgctxt "password reset email body"
msgid "<h3>Reset your password at %{instance_name}</h3>\n<p>Someone has requested password change for your account at %{instance_name}.</p>\n<p>If it was you, visit the following link to proceed: <a href=\"%{password_reset_url}\">reset password</a>.</p>\n<p>If it was someone else, nothing to worry about: your data is secure and your password has not been changed.</p>\n"
msgstr ""
"<h3>在 %{instance_name} 重置您的密码</h3>\n"
"<p>有人请求更改您在 %{instance_name} 的账户密码。</p>\n"
"<p>如果这是您的操作,请点击以下链接继续重置密码:<a href=\""
"%{password_reset_url}\">重置密码</a>。</p>\n"
"<p>如果这不是您的操作,请不用担心:您的数据是安全的,您的密码未被更改。</p>\n"
#: lib/pleroma/emails/user_email.ex:98
#, elixir-autogen, elixir-format
msgctxt "password reset email subject"
msgid "Password reset"
msgstr "重置密码"
#: lib/pleroma/emails/user_email.ex:215
#, elixir-autogen, elixir-format
msgctxt "successful registration email body"
msgid "<h3>Hello @%{nickname},</h3>\n<p>Your account at %{instance_name} has been registered successfully.</p>\n<p>No further action is required to activate your account.</p>\n"
msgstr ""
"<h3>你好,@%{nickname}</h3>\n"
"<p>你在 %{instance_name} 的账户已经成功注册。</p>\n"
"<p>无需进行其他操作即可激活你的账户。</p>\n"
#: lib/pleroma/emails/user_email.ex:231
#, elixir-autogen, elixir-format
msgctxt "successful registration email subject"
msgid "Account registered on %{instance_name}"
msgstr "账号注册在 %{instance_name}"
#: lib/pleroma/emails/user_email.ex:136
#, elixir-autogen, elixir-format
msgctxt "user invitation email subject"
msgid "Invitation to %{instance_name}"
msgstr "邀请加入 %{instance_name}"
#: lib/pleroma/emails/user_email.ex:53
#, elixir-autogen, elixir-format
msgctxt "welcome email html body"
msgid "Welcome to %{instance_name}!"
msgstr "欢迎来到%{instance_name}"
#: lib/pleroma/emails/user_email.ex:41
#, elixir-autogen, elixir-format
msgctxt "welcome email subject"
msgid "Welcome to %{instance_name}!"
msgstr "欢迎来到%{instance_name}"
#: lib/pleroma/emails/user_email.ex:65
#, elixir-autogen, elixir-format
msgctxt "welcome email text body"
msgid "Welcome to %{instance_name}!"
msgstr "欢迎来到 %{instance_name}"
#: lib/pleroma/emails/user_email.ex:368
#, elixir-autogen, elixir-format
msgctxt "account archive email body - admin requested"
msgid "<p>Admin @%{admin_nickname} requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
"<p>管理员 @%{admin_nickname} 请求对你的 Akkoma "
"账户进行完整备份,备份已准备好可供下载:</p>\n"
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
#: lib/pleroma/emails/user_email.ex:356
#, elixir-autogen, elixir-format
msgctxt "account archive email body - self-requested"
msgid "<p>You requested a full backup of your Akkoma account. It's ready for download:</p>\n<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
msgstr ""
"<p>您请求了 Akkoma 帐户的完整备份。备份已准备就绪,可以下载:</p>\n"
"<p><a href=\"%{download_url}\">%{download_url}</a></p>\n"
#: lib/pleroma/web/templates/o_auth/o_auth/show.html.eex:41
#, elixir-autogen, elixir-format
msgctxt "oauth register page title"
msgid "This is your first visit! Please enter your Akkoma handle."
msgstr "这是您的第一次访问请填写您的Akkoma账号。"
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:123
#, elixir-autogen, elixir-format
msgctxt "remote follow error message - unknown error"
msgid "Something went wrong."
msgstr "发生了一些错误。"
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:67
#, elixir-autogen, elixir-format
msgctxt "remote follow error message - user not found"
msgid "Could not find user"
msgstr "无法找到相应用户"
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:8
#, elixir-autogen, elixir-format
msgctxt "status interact authorization button"
msgid "Interact"
msgstr "互动"
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:2
#, elixir-autogen, elixir-format
msgctxt "status interact error"
msgid "Error: %{error}"
msgstr "错误:%{error}"
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:95
#, elixir-autogen, elixir-format
msgctxt "status interact error message - status not found"
msgid "Could not find status"
msgstr "无法找到帖文"
#: lib/pleroma/web/twitter_api/controllers/util_controller.ex:144
#, elixir-autogen, elixir-format
msgctxt "status interact error message - unknown error"
msgid "Something went wrong."
msgstr "发生了一些错误。"
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "status interact header"
msgid "Interacting with %{nickname}'s %{status_link}"
msgstr "与 %{nickname} 的 %{status_link} 进行交互"
#: lib/pleroma/web/templates/twitter_api/util/status_interact.html.eex:4
#, elixir-autogen, elixir-format
msgctxt "status interact header - status link text"
msgid "status"
msgstr "帖文"
#: lib/pleroma/emails/user_email.ex:119
#, elixir-autogen, elixir-format
msgctxt "user invitation email body"
msgid "<h3>You are invited to %{instance_name}</h3>\n<p>%{inviter_name} invites you to join %{instance_name}, an instance of Akkoma federated social networking platform.</p>\n<p>Click the following link to register: <a href=\"%{registration_url}\">accept invitation</a>.</p>\n"
msgstr ""
"<h3>您被邀请加入 %{instance_name}</h3>\n"
"<p>%{inviter_name} 邀请您加入 %{instance_name},这是一个使用 Akkoma "
"联邦社交网络平台的实例。</p>\n"
"<p>点击以下链接注册: <a href=\"%{registration_url}\">接受邀请</a>.</p>\n"

View file

@ -1,11 +1,19 @@
defmodule Pleroma.Repo.Migrations.ForcePinnedObjectsToExist do defmodule Pleroma.Repo.Migrations.ForcePinnedObjectsToExist do
use Ecto.Migration use Ecto.Migration
def change do def up do
execute("UPDATE users SET pinned_objects = '{}' WHERE pinned_objects IS NULL") execute("UPDATE users SET pinned_objects = '{}' WHERE pinned_objects IS NULL")
alter table("users") do alter table("users") do
modify(:pinned_objects, :map, null: false, default: %{}) modify(:pinned_objects, :map, null: false, default: %{})
end end
end end
def down do
alter table("users") do
modify(:pinned_objects, :map, null: true, default: nil)
end
execute("UPDATE users SET pinned_objects = NULL WHERE pinned_objects = '{}'")
end
end end

View file

@ -1,9 +1,15 @@
defmodule Pleroma.Repo.Migrations.AddMastofeSettings do defmodule Pleroma.Repo.Migrations.AddMastofeSettings do
use Ecto.Migration use Ecto.Migration
def change do def up do
alter table(:users) do alter table(:users) do
add_if_not_exists(:mastofe_settings, :map) add_if_not_exists(:mastofe_settings, :map)
end end
end end
def down do
alter table(:users) do
remove_if_exists(:mastofe_settings, :map)
end
end
end end

View file

@ -1,9 +1,15 @@
defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do defmodule Pleroma.Repo.Migrations.AddLanguageToUsers do
use Ecto.Migration use Ecto.Migration
def change do def up do
alter table(:users) do alter table(:users) do
add_if_not_exists(:language, :string) add_if_not_exists(:language, :string)
end end
end end
def down do
alter table(:users) do
remove_if_exists(:language, :string)
end
end
end end

View file

@ -1,21 +0,0 @@
defmodule Pleroma.Repo.Migrations.InstanceActorsToActorTypeApplication do
use Ecto.Migration
def up do
execute("""
update users
set actor_type = 'Application'
where local
and (ap_id like '%/relay' or ap_id like '%/internal/fetch')
""")
end
def down do
execute("""
update users
set actor_type = 'Person'
where local
and (ap_id like '%/relay' or ap_id like '%/internal/fetch')
""")
end
end

View file

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.AddUnfollowedDmRestrictions do
use Ecto.Migration
def change do
alter table(:users) do
add(:accepts_direct_messages_from, :string, default: "everybody")
end
end
end

115
priv/static/embed/embed.css Normal file
View file

@ -0,0 +1,115 @@
body {
background-color: #282c37;
font-family: sans-serif;
color: white;
margin: 0;
padding: 1em;
padding-bottom: 0;
}
.avatar {
cursor: pointer;
}
.avatar img {
float: left;
border-radius: 4px;
margin-right: 4px;
}
.activity-content {
padding-top: 1em;
}
.attachment {
margin-top: 1em;
}
.attachment img {
max-width: 100%;
}
.date a {
text-decoration: none;
}
.date a:hover {
text-decoration: underline;
}
.date a,
.counts {
color: #666;
font-size: 0.9em;
}
.counts dt,
.counts dd {
float: left;
margin-left: 1em;
}
a {
color: white;
}
.h-card {
min-height: 48px;
margin-bottom: 8px;
}
.h-card a {
text-decoration: none;
}
.h-card a:hover {
text-decoration: underline;
}
.display-name {
padding-top: 4px;
display: block;
text-overflow: ellipsis;
overflow: hidden;
color: white;
}
/* keep emoji from being hilariously huge */
.display-name img {
max-height: 1em;
}
.display-name .nickname {
padding-top: 4px;
display: block;
}
.nickname:hover {
text-decoration: none;
}
.pull-right {
float: right;
}
.collapse {
margin: 0;
width: auto;
}
a.button {
box-sizing: border-box;
display: inline-block;
color: white;
background-color: #419bdd;
border-radius: 4px;
border: none;
padding: 10px;
font-weight: 500;
font-size: 0.9em;
}
a.button:hover {
text-decoration: none;
background-color: #61a6d9;
}

View file

@ -0,0 +1,43 @@
(function () {
'use strict'
var ready = function (loaded) {
if (['interactive', 'complete'].indexOf(document.readyState) !== -1) {
loaded()
} else {
document.addEventListener('DOMContentLoaded', loaded)
}
}
ready(function () {
var iframes = []
window.addEventListener('message', function (e) {
var data = e.data || {}
if (data.type !== 'setHeightPleromaEmbed' || !iframes[data.id]) {
return
}
iframes[data.id].height = data.height
});
[].forEach.call(document.querySelectorAll('iframe.pleroma-embed'), function (iframe) {
iframe.scrolling = 'no'
iframe.style.overflow = 'hidden'
iframes.push(iframe)
var id = iframes.length - 1
iframe.onload = function () {
iframe.contentWindow.postMessage({
type: 'setHeightPleromaEmbed',
id: id
}, '*')
}
iframe.onload()
})
})
})()

View file

@ -14,13 +14,13 @@ input {
padding: 10px; padding: 10px;
margin-top: 5px; margin-top: 5px;
margin-bottom: 10px; margin-bottom: 10px;
background-color: var(--background-color); background-color: transparent;
color: var(--primary-text-color); color: inherit;
border: 0; border: 0;
transition-property: border-bottom; transition-property: border-bottom;
transition-duration: 0.35s; transition-duration: 0.35s;
border-bottom: 2px solid #2a384a; border-bottom: 2px solid var(--faint);
font-size: 14px; font: inherit;
width: inherit; width: inherit;
box-sizing: border-box; box-sizing: border-box;
} }
@ -91,26 +91,22 @@ [type="checkbox"]:checked+label:before {
a.button, a.button,
button { button {
width: 100%; width: 100%;
background-color: #1c2a3a; background-color: var(--btn);
color: var(--primary-text-color); color: var(--btnText);
border-radius: 4px; border-radius: 4px;
border: none; border: none;
padding: 10px 16px; padding: 10px 16px;
margin-top: 20px; margin-top: 20px;
margin-bottom: 20px; margin-bottom: 20px;
text-transform: uppercase; text-transform: uppercase;
font-size: 16px; box-shadow: var(--btnShadow);
box-shadow: 0px 0px 2px 0px black, font: inherit;
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
} }
a.button:hover, a.button:hover,
button:hover { button:hover {
cursor: pointer; cursor: pointer;
box-shadow: 0px 0px 0px 1px var(--brand-color), box-shadow: var(--btnHoverShadow);
0px 1px 0px 0px rgba(255, 255, 255, 0.2) inset,
0px -1px 0px 0px rgba(0, 0, 0, 0.2) inset;
} }
.actions { .actions {
@ -156,3 +152,20 @@ .account-header__nickname {
font-size: 14px; font-size: 14px;
color: var(--muted-text-color); color: var(--muted-text-color);
} }
.oauth {
/* Remote interaction /main/ostatus has such hierarchy, and its header and
* content do not pad themselves:
* (.panel.oauth (h2)
* (form (input)
* (button))) */
padding: 1px 1em;
}
.oauth .container__content {
/* Frontend selection /oauth/authorize needs an inverse because its heading
* and content have their own background and padding:
* (.panel.oauth (form (.container__content (.panel-heading)
* (.panel-content)))) */
margin: -1px -1em;
}

View file

@ -9,6 +9,7 @@ command=/opt/akkoma/bin/pleroma
command_args="start" command_args="start"
command_user=akkoma command_user=akkoma
command_background=1 command_background=1
no_new_privs="yes"
# Ask process to terminate within 30 seconds, otherwise kill it # Ask process to terminate within 30 seconds, otherwise kill it
retry="SIGTERM/30/SIGKILL/5" retry="SIGTERM/30/SIGKILL/5"

View file

@ -0,0 +1 @@
{"@context":["https://www.w3.org/ns/activitystreams","https://w3id.org/security/v1",{"manuallyApprovesFollowers":"as:manuallyApprovesFollowers","sensitive":"as:sensitive","Hashtag":"as:Hashtag","quoteUrl":"as:quoteUrl","toot":"http://joinmastodon.org/ns#","Emoji":"toot:Emoji","featured":"toot:featured","discoverable":"toot:discoverable","schema":"http://schema.org#","PropertyValue":"schema:PropertyValue","value":"schema:value","misskey":"https://misskey-hub.net/ns#","_misskey_content":"misskey:_misskey_content","_misskey_quote":"misskey:_misskey_quote","_misskey_reaction":"misskey:_misskey_reaction","_misskey_votes":"misskey:_misskey_votes","_misskey_talk":"misskey:_misskey_talk","isCat":"misskey:isCat","vcard":"http://www.w3.org/2006/vcard/ns#"}],"id":"https://mk.absturztau.be/notes/93e7nm8wqg/activity","actor":"https://mk.absturztau.be/users/8ozbzjs3o8","type":"Create","published":"2022-08-01T11:06:49.568Z","object":{"id":"https://mk.absturztau.be/notes/93e7nm8wqg","type":"Note","attributedTo":"https://mk.absturztau.be/users/8ozbzjs3o8","summary":null,"content":"<p><span>meow</span></p>","_misskey_content":"meow","published":"2022-08-01T11:06:49.568Z","to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"],"inReplyTo":null,"attachment":[],"sensitive":false,"tag":[]},"to":["https://www.w3.org/ns/activitystreams#Public"],"cc":["https://mk.absturztau.be/users/8ozbzjs3o8/followers"]}

View file

@ -7,6 +7,7 @@ defmodule Mix.Tasks.Pleroma.DatabaseTest do
use Oban.Testing, repo: Pleroma.Repo use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Bookmark
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
@ -45,21 +46,25 @@ test "it replaces objects with references" do
end end
describe "prune_objects" do describe "prune_objects" do
test "it prunes old objects from the database" do setup do
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
date = old_insert_date =
Timex.now() Timex.now()
|> Timex.shift(days: -deadline) |> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime() |> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second) |> NaiveDateTime.truncate(:second)
%{old_insert_date: old_insert_date}
end
test "it prunes old objects from the database", %{old_insert_date: old_insert_date} do
insert(:note) insert(:note)
%{id: note_remote_public_id} = %{id: note_remote_public_id} =
:note :note
|> insert() |> insert()
|> Ecto.Changeset.change(%{updated_at: date}) |> Ecto.Changeset.change(%{updated_at: old_insert_date})
|> Repo.update!() |> Repo.update!()
note_remote_non_public = note_remote_non_public =
@ -69,7 +74,7 @@ test "it prunes old objects from the database" do
note_remote_non_public note_remote_non_public
|> Ecto.Changeset.change(%{ |> Ecto.Changeset.change(%{
updated_at: date, updated_at: old_insert_date,
data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end) data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end)
}) })
|> Repo.update!() |> Repo.update!()
@ -83,21 +88,37 @@ test "it prunes old objects from the database" do
refute Object.get_by_id(note_remote_non_public_id) refute Object.get_by_id(note_remote_non_public_id)
end end
test "with the --keep-non-public option it still keeps non-public posts even if they are not local" do test "it cleans up bookmarks", %{old_insert_date: old_insert_date} do
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 user = insert(:user)
{:ok, old_object_activity} = CommonAPI.post(user, %{status: "yadayada"})
date = Repo.one(Object)
Timex.now() |> Ecto.Changeset.change(%{updated_at: old_insert_date})
|> Timex.shift(days: -deadline) |> Repo.update!()
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
{:ok, new_object_activity} = CommonAPI.post(user, %{status: "yadayada"})
{:ok, _} = Bookmark.create(user.id, old_object_activity.id)
{:ok, _} = Bookmark.create(user.id, new_object_activity.id)
assert length(Repo.all(Object)) == 2
assert length(Repo.all(Bookmark)) == 2
Mix.Tasks.Pleroma.Database.run(["prune_objects"])
assert length(Repo.all(Object)) == 1
assert length(Repo.all(Bookmark)) == 1
refute Bookmark.get(user.id, old_object_activity.id)
end
test "with the --keep-non-public option it still keeps non-public posts even if they are not local",
%{old_insert_date: old_insert_date} do
insert(:note) insert(:note)
%{id: note_remote_id} = %{id: note_remote_id} =
:note :note
|> insert() |> insert()
|> Ecto.Changeset.change(%{updated_at: date}) |> Ecto.Changeset.change(%{updated_at: old_insert_date})
|> Repo.update!() |> Repo.update!()
note_remote_non_public = note_remote_non_public =
@ -107,7 +128,7 @@ test "with the --keep-non-public option it still keeps non-public posts even if
note_remote_non_public note_remote_non_public
|> Ecto.Changeset.change(%{ |> Ecto.Changeset.change(%{
updated_at: date, updated_at: old_insert_date,
data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end) data: note_remote_non_public_data |> update_in(["to"], fn _ -> [] end)
}) })
|> Repo.update!() |> Repo.update!()
@ -120,16 +141,10 @@ test "with the --keep-non-public option it still keeps non-public posts even if
refute Object.get_by_id(note_remote_id) refute Object.get_by_id(note_remote_id)
end end
test "with the --keep-threads and --keep-non-public option it keeps old threads with non-public replies even if the interaction is not local" do test "with the --keep-threads and --keep-non-public option it keeps old threads with non-public replies even if the interaction is not local",
%{old_insert_date: old_insert_date} do
# For non-public we only check Create Activities because only these are relevant for threads # For non-public we only check Create Activities because only these are relevant for threads
# Flags are always non-public, Announces from relays can be non-public... # Flags are always non-public, Announces from relays can be non-public...
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1
old_insert_date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
remote_user1 = insert(:user, local: false) remote_user1 = insert(:user, local: false)
remote_user2 = insert(:user, local: false) remote_user2 = insert(:user, local: false)
@ -212,15 +227,9 @@ test "with the --keep-threads option it still keeps non-old threads even with no
assert length(Repo.all(Object)) == 2 assert length(Repo.all(Object)) == 2
end end
test "with the --keep-threads option it deletes old threads with no local interaction" do test "with the --keep-threads option it deletes old threads with no local interaction", %{
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 old_insert_date: old_insert_date
} do
old_insert_date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
remote_user = insert(:user, local: false) remote_user = insert(:user, local: false)
remote_user2 = insert(:user, local: false) remote_user2 = insert(:user, local: false)
@ -261,15 +270,9 @@ test "with the --keep-threads option it deletes old threads with no local intera
assert length(Repo.all(Object)) == 0 assert length(Repo.all(Object)) == 0
end end
test "with the --keep-threads option it keeps old threads with local interaction" do test "with the --keep-threads option it keeps old threads with local interaction", %{
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 old_insert_date: old_insert_date
} do
old_insert_date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
remote_user = insert(:user, local: false) remote_user = insert(:user, local: false)
local_user = insert(:user, local: true) local_user = insert(:user, local: true)
@ -326,15 +329,9 @@ test "with the --keep-threads option it keeps old threads with local interaction
assert length(Repo.all(Object)) == 4 assert length(Repo.all(Object)) == 4
end end
test "with the --keep-threads option it keeps old threads with bookmarked posts" do test "with the --keep-threads option it keeps old threads with bookmarked posts", %{
deadline = Pleroma.Config.get([:instance, :remote_post_retention_days]) + 1 old_insert_date: old_insert_date
} do
old_insert_date =
Timex.now()
|> Timex.shift(days: -deadline)
|> Timex.to_naive_datetime()
|> NaiveDateTime.truncate(:second)
remote_user = insert(:user, local: false) remote_user = insert(:user, local: false)
local_user = insert(:user, local: true) local_user = insert(:user, local: true)

View file

@ -35,7 +35,7 @@ test "always add user and list topics" do
setup do setup do
activity = %Activity{ activity = %Activity{
object: %Object{data: %{"type" => "Note"}}, object: %Object{data: %{"type" => "Note"}},
data: %{"to" => [Pleroma.Constants.as_public()]} data: %{"to" => [Pleroma.Constants.as_public()], "type" => "Create"}
} }
{:ok, activity: activity} {:ok, activity: activity}
@ -114,6 +114,55 @@ test "local action doesn't produce public:remote topic", %{activity: activity} d
end end
end end
describe "public visibility Announces" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => []}},
data: %{"type" => "Announce", "to" => [Pleroma.Constants.as_public()]}
}
{:ok, activity: activity}
end
test "does not generate public topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute "public" in topics
refute "public:remote" in topics
refute "public:local" in topics
end
end
describe "local-public visibility create events" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => []}},
data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
}
{:ok, activity: activity}
end
test "doesn't produce public topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public")
end
test "produces public:local topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:local")
end
test "with no attachments doesn't produce public:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public:media")
refute Enum.member?(topics, "public:local:media")
end
end
describe "public visibility create events with attachments" do describe "public visibility create events with attachments" do
setup do setup do
activity = %Activity{ activity = %Activity{
@ -152,9 +201,36 @@ test "non-local action produces public:remote:media topic", %{activity: activity
end end
end end
describe "local-public visibility create events with attachments" do
setup do
activity = %Activity{
object: %Object{data: %{"attachment" => ["foo"]}},
data: %{"type" => "Create", "to" => [Pleroma.Web.ActivityPub.Utils.as_local_public()]}
}
{:ok, activity: activity}
end
test "do not produce public:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
refute Enum.member?(topics, "public:media")
end
test "produces public:local:media topics", %{activity: activity} do
topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "public:local:media")
end
end
describe "non-public visibility" do describe "non-public visibility" do
test "produces direct topic" do test "produces direct topic" do
activity = %Activity{object: %Object{data: %{"type" => "Note"}}, data: %{"to" => []}} activity = %Activity{
object: %Object{data: %{"type" => "Note"}},
data: %{"to" => [], "type" => "Create"}
}
topics = Topics.get_activity_topics(activity) topics = Topics.get_activity_topics(activity)
assert Enum.member?(topics, "direct") assert Enum.member?(topics, "direct")

View file

@ -328,6 +328,32 @@ test "it disables notifications from strangers" do
refute Notification.create_notification(activity, followed) refute Notification.create_notification(activity, followed)
end end
test "it disables notifications from non-followees" do
follower = insert(:user)
followed =
insert(:user,
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
)
CommonAPI.follow(follower, followed)
{:ok, activity} = CommonAPI.post(follower, %{status: "hey @#{followed.nickname}"})
refute Notification.create_notification(activity, followed)
end
test "it allows notifications from followees" do
poster = insert(:user)
receiver =
insert(:user,
notification_settings: %Pleroma.User.NotificationSetting{block_from_strangers: true}
)
CommonAPI.follow(receiver, poster)
{:ok, activity} = CommonAPI.post(poster, %{status: "hey @#{receiver.nickname}"})
assert Notification.create_notification(activity, receiver)
end
test "it doesn't create a notification for user if he is the activity author" do test "it doesn't create a notification for user if he is the activity author" do
activity = insert(:note_activity) activity = insert(:note_activity)
author = User.get_cached_by_ap_id(activity.data["actor"]) author = User.get_cached_by_ap_id(activity.data["actor"])
@ -1225,5 +1251,32 @@ test "it returns notifications about favorites with filtered word", %{user: user
assert length(Notification.for_user(user)) == 1 assert length(Notification.for_user(user)) == 1
end end
test "it returns notifications when related object is without content and filters are defined",
%{user: user} do
followed_user = insert(:user, is_locked: true)
insert(:filter, user: followed_user, phrase: "test", hide: true)
{:ok, _, _, _activity} = CommonAPI.follow(user, followed_user)
refute FollowingRelationship.following?(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
assert %{type: "follow_request"} =
NotificationView.render("show.json", %{
notification: notification,
for: followed_user
})
assert {:ok, _} = CommonAPI.accept_follow_request(user, followed_user)
assert [notification] = Notification.for_user(followed_user)
assert %{type: "follow"} =
NotificationView.render("show.json", %{
notification: notification,
for: followed_user
})
end
end end
end end

View file

@ -133,5 +133,20 @@ test "should gracefully handle an unsupported language" do
assert {:error, "libre_translate: request failed (code 400)"} = assert {:error, "libre_translate: request failed (code 400)"} =
LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "zoop") LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "zoop")
end end
test "should work when no detected language is received" do
Tesla.Mock.mock(fn
%{method: :post, url: "http://libre.translate/translate"} ->
%Tesla.Env{
status: 200,
body:
Jason.encode!(%{
translatedText: "I will crush you"
})
}
end)
assert {:ok, "", "I will crush you"} = LibreTranslate.translate("ギュギュ握りつぶしちゃうぞ", nil, "en")
end
end end
end end

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Upload.Filter.OnlyMediaTest do
use Pleroma.DataCase, async: true
alias Pleroma.Upload
alias Pleroma.Upload.Filter.OnlyMedia
test "Allows media Content-Type" do
["audio/mpeg", "image/jpeg", "video/mp4"]
|> Enum.each(fn type ->
upload = %Upload{
content_type: type
}
assert {:ok, :noop} = OnlyMedia.filter(upload)
end)
end
test "Disallows non-media Content-Type" do
["application/javascript", "application/pdf", "text/html"]
|> Enum.each(fn type ->
upload = %Upload{
content_type: type
}
assert {:error, _} = OnlyMedia.filter(upload)
end)
end
end

View file

@ -0,0 +1,39 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserNoteTest do
alias Pleroma.UserNote
use Pleroma.DataCase, async: false
import Pleroma.Factory
describe "show/2" do
setup do
{:ok, users: insert_list(2, :user)}
end
test "if record does not exist, returns empty string", %{users: [user1, user2]} do
comment = UserNote.show(user1, user2)
assert comment == ""
end
test "if record exists with comment == nil, returns empty string", %{users: [user1, user2]} do
UserNote.create(user1, user2, nil)
comment = UserNote.show(user1, user2)
assert comment == ""
end
test "if record exists with non-nil comment, returns comment", %{users: [user1, user2]} do
expected_comment = "hello"
UserNote.create(user1, user2, expected_comment)
comment = UserNote.show(user1, user2)
assert comment == expected_comment
end
end
end

View file

@ -557,6 +557,21 @@ test "it fails gracefully with invalid email config" do
refute_email_sent() refute_email_sent()
end end
test "it works when the registering user does not provide an email" do
clear_config([Pleroma.Emails.Mailer, :enabled], false)
clear_config([:instance, :account_activation_required], false)
clear_config([:instance, :account_approval_required], true)
cng = User.register_changeset(%User{}, @full_user_data |> Map.put(:email, ""))
# The user is still created
assert {:ok, %User{nickname: "nick"}} = User.register(cng)
# No emails are sent
ObanHelpers.perform_all()
refute_email_sent()
end
test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do test "it requires an email, name, nickname and password, bio is optional when account_activation_required is enabled" do
clear_config([:instance, :account_activation_required], true) clear_config([:instance, :account_activation_required], true)
@ -2756,4 +2771,35 @@ test "should not error when trying to unfollow a hashtag twice" do
assert user.followed_hashtags |> Enum.count() == 0 assert user.followed_hashtags |> Enum.count() == 0
end end
end end
describe "accepts_direct_messages?/2" do
test "should return true if the recipient follows the sender and has set accept to :people_i_follow" do
recipient =
insert(:user, %{
accepts_direct_messages_from: :people_i_follow
})
sender = insert(:user)
refute User.accepts_direct_messages?(recipient, sender)
CommonAPI.follow(recipient, sender)
assert User.accepts_direct_messages?(recipient, sender)
end
test "should return true if the recipient has set accept to :everyone" do
recipient = insert(:user, %{accepts_direct_messages_from: :everybody})
sender = insert(:user)
assert User.accepts_direct_messages?(recipient, sender)
end
test "should return false if the receipient set accept to :nobody" do
recipient = insert(:user, %{accepts_direct_messages_from: :nobody})
sender = insert(:user)
refute User.accepts_direct_messages?(recipient, sender)
end
end
end end

View file

@ -0,0 +1,52 @@
defmodule Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.MRF.DirectMessageDisabledPolicy
alias Pleroma.User
describe "strips recipients" do
test "when the user denies the direct message" do
sender = insert(:user)
recipient = insert(:user, %{accepts_direct_messages_from: :nobody})
refute User.accepts_direct_messages?(recipient, sender)
message = %{
"actor" => sender.ap_id,
"to" => [recipient.ap_id],
"cc" => [],
"type" => "Create",
"object" => %{
"type" => "Note",
"to" => [recipient.ap_id]
}
}
assert {:ok, %{"to" => [], "object" => %{"to" => []}}} =
DirectMessageDisabledPolicy.filter(message)
end
test "when the user does not deny the direct message" do
sender = insert(:user)
recipient = insert(:user, %{accepts_direct_messages_from: :everybody})
assert User.accepts_direct_messages?(recipient, sender)
message = %{
"actor" => sender.ap_id,
"to" => [recipient.ap_id],
"cc" => [],
"type" => "Create",
"object" => %{
"type" => "Note",
"to" => [recipient.ap_id]
}
}
assert {:ok, message} = DirectMessageDisabledPolicy.filter(message)
assert message["to"] == [recipient.ap_id]
assert message["object"]["to"] == [recipient.ap_id]
end
end
end

View file

@ -0,0 +1,45 @@
defmodule Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicyTest do
use Pleroma.DataCase
import Pleroma.Factory
alias Pleroma.Web.ActivityPub.MRF.RejectNewlyCreatedAccountNotesPolicy
describe "reject notes from new accounts" do
test "rejects notes from accounts created more recently than `age`" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
sender = insert(:user, %{inserted_at: Timex.now(), local: false})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:reject, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
test "does not reject notes from accounts created longer ago" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
a_day_ago = Timex.shift(Timex.now(), days: -1)
sender = insert(:user, %{inserted_at: a_day_ago, local: false})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:ok, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
test "does not affect local users" do
clear_config([:mrf_reject_newly_created_account_notes, :age], 86_400)
sender = insert(:user, %{inserted_at: Timex.now(), local: true})
message = %{
"actor" => sender.ap_id,
"type" => "Create"
}
assert {:ok, _} = RejectNewlyCreatedAccountNotesPolicy.filter(message)
end
end
end

View file

@ -102,7 +102,13 @@ test "it works as expected with noop policy" do
clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy]) clear_config([:mrf, :policies], [Pleroma.Web.ActivityPub.MRF.NoOpPolicy])
expected = %{ expected = %{
mrf_policies: ["NoOpPolicy", "HashtagPolicy", "InlineQuotePolicy", "NormalizeMarkup"], mrf_policies: [
"NoOpPolicy",
"HashtagPolicy",
"InlineQuotePolicy",
"NormalizeMarkup",
"DirectMessageDisabledPolicy"
],
mrf_hashtag: %{ mrf_hashtag: %{
federated_timeline_removal: [], federated_timeline_removal: [],
reject: [], reject: [],
@ -118,7 +124,13 @@ test "it works as expected with mock policy" do
clear_config([:mrf, :policies], [MRFModuleMock]) clear_config([:mrf, :policies], [MRFModuleMock])
expected = %{ expected = %{
mrf_policies: ["MRFModuleMock", "HashtagPolicy", "InlineQuotePolicy", "NormalizeMarkup"], mrf_policies: [
"MRFModuleMock",
"HashtagPolicy",
"InlineQuotePolicy",
"NormalizeMarkup",
"DirectMessageDisabledPolicy"
],
mrf_module_mock: "some config data", mrf_module_mock: "some config data",
mrf_hashtag: %{ mrf_hashtag: %{
federated_timeline_removal: [], federated_timeline_removal: [],

View file

@ -19,12 +19,6 @@ test "gets an actor for the relay" do
assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay" assert user.ap_id == "#{Pleroma.Web.Endpoint.url()}/relay"
end end
test "relay actor is an application" do
# See <https://www.w3.org/TR/activitystreams-vocabulary/#dfn-application>
user = Relay.get_actor()
assert user.actor_type == "Application"
end
test "relay actor is invisible" do test "relay actor is invisible" do
user = Relay.get_actor() user = Relay.get_actor()
assert User.invisible?(user) assert User.invisible?(user)

View file

@ -732,9 +732,7 @@ test "it streams out the announce", %{announce: announce} do
]) do ]) do
{:ok, announce, _} = SideEffects.handle(announce) {:ok, announce, _} = SideEffects.handle(announce)
assert called( assert called(Pleroma.Web.Streamer.stream(["user", "list"], announce))
Pleroma.Web.Streamer.stream(["user", "list", "public", "public:local"], announce)
)
assert called(Pleroma.Web.Push.send(:_)) assert called(Pleroma.Web.Push.send(:_))
end end

View file

@ -316,7 +316,7 @@ test "it strips internal reactions" do
test "it correctly processes messages with non-array to field" do test "it correctly processes messages with non-array to field" do
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!() |> Jason.decode!()
|> Map.put("to", "https://www.w3.org/ns/activitystreams#Public") |> Map.put("to", "https://www.w3.org/ns/activitystreams#Public")
|> put_in(["object", "to"], "https://www.w3.org/ns/activitystreams#Public") |> put_in(["object", "to"], "https://www.w3.org/ns/activitystreams#Public")
@ -333,7 +333,7 @@ test "it correctly processes messages with non-array to field" do
test "it correctly processes messages with non-array cc field" do test "it correctly processes messages with non-array cc field" do
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!() |> Jason.decode!()
|> Map.put("cc", "http://mastodon.example.org/users/admin/followers") |> Map.put("cc", "http://mastodon.example.org/users/admin/followers")
|> put_in(["object", "cc"], "http://mastodon.example.org/users/admin/followers") |> put_in(["object", "cc"], "http://mastodon.example.org/users/admin/followers")
@ -346,7 +346,7 @@ test "it correctly processes messages with non-array cc field" do
test "it correctly processes messages with weirdness in address fields" do test "it correctly processes messages with weirdness in address fields" do
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!() |> Jason.decode!()
|> Map.put("cc", ["http://mastodon.example.org/users/admin/followers", ["¿"]]) |> Map.put("cc", ["http://mastodon.example.org/users/admin/followers", ["¿"]])
|> put_in(["object", "cc"], ["http://mastodon.example.org/users/admin/followers", ["¿"]]) |> put_in(["object", "cc"], ["http://mastodon.example.org/users/admin/followers", ["¿"]])
@ -412,7 +412,7 @@ test "does NOT schedule background fetching of `replies` beyond max thread depth
activity = activity =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
|> Poison.decode!() |> Jason.decode!()
|> Kernel.put_in(["object", "replies"], replies) |> Kernel.put_in(["object", "replies"], replies)
%{activity: activity} %{activity: activity}
@ -783,4 +783,42 @@ test "quote fetching should stop after n levels", _ do
} = Transmogrifier.fix_quote_url(note) } = Transmogrifier.fix_quote_url(note)
end end
end end
test "the standalone note uses its own ID when context is missing" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
activity =
"test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
|> File.read!()
|> Jason.decode!()
{:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
object = Object.normalize(modified, fetch: false)
assert object.data["context"] == object.data["id"]
assert modified.data["context"] == object.data["id"]
end
test "the reply note uses its parent's ID when context is missing and reply is unreachable" do
insert(:user, ap_id: "https://mk.absturztau.be/users/8ozbzjs3o8")
activity =
"test/fixtures/tesla_mock/mk.absturztau.be-93e7nm8wqg-activity.json"
|> File.read!()
|> Jason.decode!()
object =
activity["object"]
|> Map.put("inReplyTo", "https://404.site/object/went-to-buy-milk")
activity =
activity
|> Map.put("object", object)
{:ok, %Activity{} = modified} = Transmogrifier.handle_incoming(activity)
object = Object.normalize(modified, fetch: false)
assert object.data["context"] == object.data["inReplyTo"]
assert modified.data["context"] == object.data["inReplyTo"]
end
end end

View file

@ -225,6 +225,20 @@ test "superusers deleting non-local posts won't federate the delete" do
refute Activity.get_by_id(post.id) refute Activity.get_by_id(post.id)
end end
test "it allows privileged users to delete banned user's posts" do
clear_config([:instance, :moderator_privileges], [:messages_delete])
user = insert(:user)
moderator = insert(:user, is_moderator: true)
{:ok, post} = CommonAPI.post(user, %{status: "namu amida butsu"})
User.set_activation(user, false)
assert {:ok, delete} = CommonAPI.delete(post.id, moderator)
assert delete.local
refute Activity.get_by_id(post.id)
end
end end
test "favoriting race condition" do test "favoriting race condition" do

View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.EmbedControllerTest do
use Pleroma.Web.ConnCase, async: true
import Pleroma.Factory
test "/embed", %{conn: conn} do
activity = insert(:note_activity)
resp =
conn
|> get("/embed/#{activity.id}")
|> response(200)
object = Pleroma.Object.get_by_ap_id(activity.data["object"])
assert String.contains?(resp, object.data["content"])
end
test "/embed with a restricted post", %{conn: conn} do
activity = insert(:note_activity)
clear_config([:restrict_unauthenticated, :activities, :local], true)
conn
|> get("/embed/#{activity.id}")
|> response(401)
end
test "/embed with a private post", %{conn: conn} do
user = insert(:user)
{:ok, activity} =
Pleroma.Web.CommonAPI.post(user, %{
status: "Mega ultra chicken status: #fried",
visibility: "private"
})
conn
|> get("/embed/#{activity.id}")
|> response(401)
end
end

View file

@ -1895,6 +1895,39 @@ test "getting a list of blocks" do
assert [%{"id" => ^id2}] = result assert [%{"id" => ^id2}] = result
end end
test "list of blocks with with_relationships parameter" do
%{user: user, conn: conn} = oauth_access(["read:blocks"])
%{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _, _} = User.follow(other_user1, user)
{:ok, _, _} = User.follow(other_user2, user)
{:ok, _, _} = User.follow(other_user3, user)
{:ok, _} = User.block(user, other_user1)
{:ok, _} = User.block(user, other_user2)
{:ok, _} = User.block(user, other_user3)
assert [
%{
"id" => ^id1,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
},
%{
"id" => ^id2,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
},
%{
"id" => ^id3,
"pleroma" => %{"relationship" => %{"blocking" => true, "followed_by" => false}}
}
] =
conn
|> get("/api/v1/blocks?with_relationships=true")
|> json_response_and_validate_schema(200)
end
test "account lookup", %{conn: conn} do test "account lookup", %{conn: conn} do
%{nickname: acct} = insert(:user, %{nickname: "nickname"}) %{nickname: acct} = insert(:user, %{nickname: "nickname"})
%{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"}) %{nickname: acct_two} = insert(:user, %{nickname: "nickname@notlocaldoma.in"})

View file

@ -674,7 +674,10 @@ test "option limit is enforced", %{conn: conn} do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{ |> post("/api/v1/statuses", %{
"status" => "desu~", "status" => "desu~",
"poll" => %{"options" => Enum.map(0..limit, fn _ -> "desu" end), "expires_in" => 1} "poll" => %{
"options" => Enum.map(0..limit, fn num -> "desu #{num}" end),
"expires_in" => 1
}
}) })
%{"error" => error} = json_response_and_validate_schema(conn, 422) %{"error" => error} = json_response_and_validate_schema(conn, 422)
@ -690,7 +693,7 @@ test "option character limit is enforced", %{conn: conn} do
|> post("/api/v1/statuses", %{ |> post("/api/v1/statuses", %{
"status" => "...", "status" => "...",
"poll" => %{ "poll" => %{
"options" => [Enum.reduce(0..limit, "", fn _, acc -> acc <> "." end)], "options" => [String.duplicate(".", limit + 1), "lol"],
"expires_in" => 1 "expires_in" => 1
} }
}) })
@ -772,6 +775,32 @@ test "scheduled poll", %{conn: conn} do
assert object.data["type"] == "Question" assert object.data["type"] == "Question"
assert length(object.data["oneOf"]) == 3 assert length(object.data["oneOf"]) == 3
end end
test "cannot have only one option", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
test "cannot have only duplicated options", %{conn: conn} do
conn =
conn
|> put_req_header("content-type", "application/json")
|> post("/api/v1/statuses", %{
"status" => "desu~",
"poll" => %{"options" => ["mew", "mew"], "expires_in" => 1}
})
%{"error" => error} = json_response_and_validate_schema(conn, 422)
assert error == "Poll must contain at least 2 options"
end
end end
test "get a status" do test "get a status" do
@ -1044,6 +1073,27 @@ test "when you're an admin or moderator", %{conn: conn} do
refute Activity.get_by_id(activity1.id) refute Activity.get_by_id(activity1.id)
refute Activity.get_by_id(activity2.id) refute Activity.get_by_id(activity2.id)
end end
test "when you're privileged and the user is banned", %{conn: conn} do
clear_config([:instance, :moderator_privileges], [:messages_delete])
posting_user = insert(:user, is_active: false)
refute posting_user.is_active
activity = insert(:note_activity, user: posting_user)
user = insert(:user, is_moderator: true)
res_conn =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:statuses"]))
|> delete("/api/v1/statuses/#{activity.id}")
assert %{} = json_response_and_validate_schema(res_conn, 200)
# assert ModerationLog |> Repo.one() |> ModerationLog.get_log_entry_message() ==
# "@#{user.nickname} deleted status ##{activity.id}"
refute Activity.get_by_id(activity.id)
end
end end
describe "reblogging" do describe "reblogging" do
@ -1960,6 +2010,10 @@ test "index" do
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅") {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
User.mute(user, other_user) User.mute(user, other_user)
deactivated_user = insert(:user)
{:ok, _} = CommonAPI.react_with_emoji(activity.id, deactivated_user, "🎅")
User.set_activation(deactivated_user, false)
result = result =
conn conn
|> get("/api/v1/statuses/?ids[]=#{activity.id}") |> get("/api/v1/statuses/?ids[]=#{activity.id}")
@ -1967,6 +2021,7 @@ test "index" do
assert [ assert [
%{ %{
"emoji_reactions" => [],
"pleroma" => %{ "pleroma" => %{
"emoji_reactions" => [] "emoji_reactions" => []
} }

View file

@ -727,4 +727,56 @@ test "actor_type field has a higher priority than bot", %{conn: conn} do
assert account["source"]["pleroma"]["actor_type"] == "Person" assert account["source"]["pleroma"]["actor_type"] == "Person"
end end
end end
describe "Updating direct message settings" do
setup do: oauth_access(["write:accounts"])
setup :request_content_type
test "changing to :everybody", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "everybody"
})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "everybody"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from == :everybody
end
test "changing to :nobody", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{accepts_direct_messages_from: "nobody"})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "nobody"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from == :nobody
end
test "changing to :people_i_follow", %{conn: conn} do
account =
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "people_i_follow"
})
|> json_response_and_validate_schema(200)
assert account["accepts_direct_messages_from"]
assert account["accepts_direct_messages_from"] == "people_i_follow"
assert Pleroma.User.get_by_ap_id(account["url"]).accepts_direct_messages_from ==
:people_i_follow
end
test "changing to an unsupported value", %{conn: conn} do
conn
|> patch("/api/v1/accounts/update_credentials", %{
accepts_direct_messages_from: "unsupported"
})
|> json_response(400)
end
end
end end

View file

@ -397,7 +397,22 @@ test "represent a relationship for the blocking and blocked user" do
expected = expected =
Map.merge( Map.merge(
@blank_response, @blank_response,
%{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)} %{following: false, blocking: true, blocked_by: false, id: to_string(other_user.id)}
)
test_relationship_rendering(user, other_user, expected)
end
test "blocks are not visible to the blocked user" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationship} = User.block(other_user, user)
expected =
Map.merge(
@blank_response,
%{following: false, blocking: false, blocked_by: false, id: to_string(other_user.id)}
) )
test_relationship_rendering(user, other_user, expected) test_relationship_rendering(user, other_user, expected)

View file

@ -199,6 +199,15 @@ test "mediaproxy whitelist" do
assert unencoded == url assert unencoded == url
end end
test "mediaproxy blocklist" do
clear_config([:media_proxy, :whitelist], ["https://google.com"])
clear_config([:media_proxy, :blocklist], ["https://feld.me"])
url = "https://feld.me/foo.png"
unencoded = MediaProxy.url(url)
assert unencoded == url
end
# TODO: delete after removing support bare domains for media proxy whitelist # TODO: delete after removing support bare domains for media proxy whitelist
test "mediaproxy whitelist bare domains whitelist (deprecated)" do test "mediaproxy whitelist bare domains whitelist (deprecated)" do
clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"]) clear_config([:media_proxy, :whitelist], ["google.com", "feld.me"])
@ -220,6 +229,18 @@ test "does not change whitelisted urls" do
assert String.starts_with?(encoded, media_url) assert String.starts_with?(encoded, media_url)
end end
test "does not change blocked urls" do
clear_config([:media_proxy, :whitelist], ["mycdn.akamai.com"])
clear_config([:media_proxy, :base_url], "https://cache.pleroma.social")
media_url = "https://mycdn.akamai.com"
url = "#{media_url}/static/logo.png"
encoded = MediaProxy.url(url)
assert String.starts_with?(encoded, media_url)
end
test "ensure Pleroma.Upload base_url is always whitelisted" do test "ensure Pleroma.Upload base_url is always whitelisted" do
media_url = "https://media.pleroma.social" media_url = "https://media.pleroma.social"
clear_config([Pleroma.Upload, :base_url], media_url) clear_config([Pleroma.Upload, :base_url], media_url)

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