Merge remote-tracking branch 'origin/develop' into shigusegubu

This commit is contained in:
Henry Jameson 2024-10-08 18:01:04 +03:00
commit 8b249431da
107 changed files with 1536 additions and 619 deletions

View file

@ -1,8 +1,8 @@
image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.13.4-otp-25 image: git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.14.5-otp-25
variables: &global_variables variables: &global_variables
# Only used for the release # Only used for the release
ELIXIR_VER: 1.13.4 ELIXIR_VER: 1.14.5
POSTGRES_DB: pleroma_test POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres POSTGRES_PASSWORD: postgres
@ -71,7 +71,7 @@ check-changelog:
tags: tags:
- amd64 - amd64
build-1.13.4-otp-25: build-1.14.5-otp-25:
extends: extends:
- .build_changes_policy - .build_changes_policy
- .using-ci-base - .using-ci-base
@ -119,7 +119,7 @@ benchmark:
- mix ecto.migrate - mix ecto.migrate
- mix pleroma.load_testing - mix pleroma.load_testing
unit-testing-1.13.4-otp-25: unit-testing-1.14.5-otp-25:
extends: extends:
- .build_changes_policy - .build_changes_policy
- .using-ci-base - .using-ci-base
@ -134,7 +134,7 @@ unit-testing-1.13.4-otp-25:
script: &testing_script script: &testing_script
- mix ecto.create - mix ecto.create
- mix ecto.migrate - mix ecto.migrate
- mix test --cover --preload-modules - mix pleroma.test_runner --cover --preload-modules
coverage: '/^Line total: ([^ ]*%)$/' coverage: '/^Line total: ([^ ]*%)$/'
artifacts: artifacts:
reports: reports:

View file

@ -1,7 +1,8 @@
# https://hub.docker.com/r/hexpm/elixir/tags
ARG ELIXIR_IMG=hexpm/elixir ARG ELIXIR_IMG=hexpm/elixir
ARG ELIXIR_VER=1.13.4 ARG ELIXIR_VER=1.14.5
ARG ERLANG_VER=24.3.4.15 ARG ERLANG_VER=25.3.2.14
ARG ALPINE_VER=3.17.5 ARG ALPINE_VER=3.17.9
FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} as build FROM ${ELIXIR_IMG}:${ELIXIR_VER}-erlang-${ERLANG_VER}-alpine-${ALPINE_VER} as build

View file

@ -0,0 +1 @@
Added support for argon2 passwords and their conversion for migration from Akkoma fork to upstream.

View file

View file

@ -0,0 +1 @@
Restrict incoming activities from unknown actors to a subset that does not imply a previous relationship and early rejection of unrecognized activity types.

View file

View file

@ -0,0 +1 @@
Elixir 1.14 and Erlang/OTP 23 is now the minimum supported release

View file

@ -0,0 +1 @@
Resolved edge case where the API can report you are following a user but the relationship is not fully established.

1
changelog.d/ldap-ca.add Normal file
View file

@ -0,0 +1 @@
LDAP configuration now permits overriding the CA root certificate file for TLS validation.

View file

@ -0,0 +1 @@
LDAP authentication has been refactored to operate as a GenServer process which will maintain an active connection to the LDAP server.

1
changelog.d/ldap-tls.fix Normal file
View file

@ -0,0 +1 @@
STARTTLS certificate and hostname verification for LDAP authentication

View file

1
changelog.d/ldaps.fix Normal file
View file

@ -0,0 +1 @@
LDAPS connections (implicit TLS) are now supported.

View file

@ -0,0 +1 @@
Include list id in StatusView

View file

View file

@ -0,0 +1 @@
Add `id_filter` to MRF to filter URLs and their domain prior to fetching

View file

@ -0,0 +1 @@
Add `group_key` to notifications

View file

@ -0,0 +1 @@
Add a rate limiter to the OAuth App creation endpoint and ensure registered apps are assigned to users.

View file

@ -0,0 +1 @@
ReceiverWorker will cancel processing jobs instead of retrying if the user cannot be fetched due to 403, 404, or 410 errors or if the account is disabled locally.

View file

@ -0,0 +1 @@
Adjust more Oban workers to enforce unique job constraints.

View file

@ -0,0 +1 @@
Oban updated to 2.18.3

View file

@ -0,0 +1 @@
Allow providing avatar/header descriptions

View file

@ -0,0 +1 @@
Rich Media preview fetching will skip making an HTTP HEAD request to check a URL for allowed content type and length if the Tesla adapter is Gun or Finch

View file

@ -0,0 +1 @@
scrubbers/default: Allow "mention hashtag" classes used by Mastodon

View file

@ -0,0 +1 @@
Added dependencies for Swoosh's Mua mail adapter

View file

View file

@ -0,0 +1 @@
Accept application/activity+json for requests to .well-known/nodeinfo

View file

@ -1 +0,0 @@
docker buildx build --platform linux/amd64,linux/arm64,linux/arm/v7 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.12 --push .

View file

@ -1,8 +0,0 @@
FROM elixir:1.13.4-otp-25
# Single RUN statement, otherwise intermediate images are created
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
RUN apt-get update &&\
apt-get install -y libmagic-dev cmake libimage-exiftool-perl ffmpeg &&\
mix local.hex --force &&\
mix local.rebar --force

View file

@ -1,4 +1,4 @@
FROM elixir:1.12.3 FROM elixir:1.14.5-otp-25
# Single RUN statement, otherwise intermediate images are created # Single RUN statement, otherwise intermediate images are created
# https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run # https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run

View file

@ -1 +1 @@
docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.13.4-otp-25 --push . docker buildx build --platform linux/amd64,linux/arm64 -t git.pleroma.social:5050/pleroma/pleroma/ci-base:elixir-1.14.5-otp-25 --push .

View file

@ -342,7 +342,7 @@ config :pleroma, :manifest,
icons: [ icons: [
%{ %{
src: "/static/logo.svg", src: "/static/logo.svg",
sizes: "144x144", sizes: "512x512",
purpose: "any", purpose: "any",
type: "image/svg+xml" type: "image/svg+xml"
} }
@ -610,7 +610,8 @@ config :pleroma, Oban,
plugins: [{Oban.Plugins.Pruner, max_age: 900}], plugins: [{Oban.Plugins.Pruner, max_age: 900}],
crontab: [ crontab: [
{"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker}, {"0 0 * * 0", Pleroma.Workers.Cron.DigestEmailsWorker},
{"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker} {"0 0 * * *", Pleroma.Workers.Cron.NewUsersDigestWorker},
{"*/10 * * * *", Pleroma.Workers.Cron.AppCleanupWorker}
] ]
config :pleroma, Pleroma.Formatter, config :pleroma, Pleroma.Formatter,
@ -624,14 +625,17 @@ config :pleroma, Pleroma.Formatter,
config :pleroma, :ldap, config :pleroma, :ldap,
enabled: System.get_env("LDAP_ENABLED") == "true", enabled: System.get_env("LDAP_ENABLED") == "true",
host: System.get_env("LDAP_HOST") || "localhost", host: System.get_env("LDAP_HOST", "localhost"),
port: String.to_integer(System.get_env("LDAP_PORT") || "389"), port: String.to_integer(System.get_env("LDAP_PORT", "389")),
ssl: System.get_env("LDAP_SSL") == "true", ssl: System.get_env("LDAP_SSL") == "true",
sslopts: [], sslopts: [],
tls: System.get_env("LDAP_TLS") == "true", tls: System.get_env("LDAP_TLS") == "true",
tlsopts: [], tlsopts: [],
base: System.get_env("LDAP_BASE") || "dc=example,dc=com", base: System.get_env("LDAP_BASE", "dc=example,dc=com"),
uid: System.get_env("LDAP_UID") || "cn" uid: System.get_env("LDAP_UID", "cn"),
# defaults to CAStore's Mozilla roots
cacertfile: System.get_env("LDAP_CACERTFILE", nil),
mail: System.get_env("LDAP_MAIL", "mail")
oauth_consumer_strategies = oauth_consumer_strategies =
System.get_env("OAUTH_CONSUMER_STRATEGIES") System.get_env("OAUTH_CONSUMER_STRATEGIES")
@ -724,6 +728,7 @@ config :pleroma, :rate_limit,
timeline: {500, 3}, timeline: {500, 3},
search: [{1000, 10}, {1000, 30}], search: [{1000, 10}, {1000, 30}],
app_account_creation: {1_800_000, 25}, app_account_creation: {1_800_000, 25},
oauth_app_creation: {900_000, 5},
relations_actions: {10_000, 10}, relations_actions: {10_000, 10},
relation_id_action: {60_000, 2}, relation_id_action: {60_000, 2},
statuses_actions: {10_000, 15}, statuses_actions: {10_000, 15},

View file

@ -2241,14 +2241,8 @@ config :pleroma, :config_description, [
label: "SSL options", label: "SSL options",
type: :keyword, type: :keyword,
description: "Additional SSL options", description: "Additional SSL options",
suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer], suggestions: [verify: :verify_peer],
children: [ children: [
%{
key: :cacertfile,
type: :string,
description: "Path to file with PEM encoded cacerts",
suggestions: ["path/to/file/with/PEM/cacerts"]
},
%{ %{
key: :verify, key: :verify,
type: :atom, type: :atom,
@ -2268,14 +2262,8 @@ config :pleroma, :config_description, [
label: "TLS options", label: "TLS options",
type: :keyword, type: :keyword,
description: "Additional TLS options", description: "Additional TLS options",
suggestions: [cacertfile: "path/to/file/with/PEM/cacerts", verify: :verify_peer], suggestions: [verify: :verify_peer],
children: [ children: [
%{
key: :cacertfile,
type: :string,
description: "Path to file with PEM encoded cacerts",
suggestions: ["path/to/file/with/PEM/cacerts"]
},
%{ %{
key: :verify, key: :verify,
type: :atom, type: :atom,
@ -2292,11 +2280,25 @@ config :pleroma, :config_description, [
}, },
%{ %{
key: :uid, key: :uid,
label: "UID", label: "UID Attribute",
type: :string, type: :string,
description: description:
"LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"", "LDAP attribute name to authenticate the user, e.g. when \"cn\", the filter will be \"cn=username,base\"",
suggestions: ["cn"] suggestions: ["cn"]
},
%{
key: :cacertfile,
label: "CACertfile",
type: :string,
description: "Path to CA certificate file"
},
%{
key: :mail,
label: "Mail Attribute",
type: :string,
description:
"LDAP attribute name to use as the email address when automatically registering the user on first login",
suggestions: ["mail"]
} }
] ]
}, },

View file

@ -742,6 +742,21 @@ config :pleroma, Pleroma.Emails.Mailer,
auth: :always auth: :always
``` ```
An example for Mua adapter:
```elixir
config :pleroma, Pleroma.Emails.Mailer,
enabled: true,
adapter: Swoosh.Adapters.Mua,
relay: "mail.example.com",
port: 465,
auth: [
username: "YOUR_USERNAME@domain.tld",
password: "YOUR_SMTP_PASSWORD"
],
protocol: :ssl
```
### :email_notifications ### :email_notifications
Email notifications settings. Email notifications settings.
@ -968,12 +983,13 @@ Pleroma account will be created with the same name as the LDAP user name.
* `enabled`: enables LDAP authentication * `enabled`: enables LDAP authentication
* `host`: LDAP server hostname * `host`: LDAP server hostname
* `port`: LDAP port, e.g. 389 or 636 * `port`: LDAP port, e.g. 389 or 636
* `ssl`: true to use SSL, usually implies the port 636 * `ssl`: true to use implicit SSL/TLS, usually port 636
* `sslopts`: additional SSL options * `sslopts`: additional SSL options
* `tls`: true to start TLS, usually implies the port 389 * `tls`: true to use explicit TLS (STARTTLS), usually port 389
* `tlsopts`: additional TLS options * `tlsopts`: additional TLS options
* `base`: LDAP base, e.g. "dc=example,dc=com" * `base`: LDAP base, e.g. "dc=example,dc=com"
* `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base" * `uid`: LDAP attribute name to authenticate the user, e.g. when "cn", the filter will be "cn=username,base"
* `cacertfile`: Path to alternate CA root certificates file
Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an Note, if your LDAP server is an Active Directory server the correct value is commonly `uid: "cn"`, but if you use an
OpenLDAP server the value may be `uid: "uid"`. OpenLDAP server the value may be `uid: "uid"`.

View file

@ -433,7 +433,7 @@ Response:
* On success: URL of the unfollowed relay * On success: URL of the unfollowed relay
```json ```json
{"https://example.com/relay"} "https://example.com/relay"
``` ```
## `POST /api/v1/pleroma/admin/users/invite_token` ## `POST /api/v1/pleroma/admin/users/invite_token`
@ -1193,20 +1193,23 @@ Loads json generated from `config/descriptions.exs`.
- Response: - Response:
```json ```json
[ {
{ "items": [
"id": 1234, {
"data": { "id": 1234,
"actor": { "data": {
"id": 1, "actor": {
"nickname": "lain" "id": 1,
"nickname": "lain"
},
"action": "relay_follow"
}, },
"action": "relay_follow" "time": 1502812026, // timestamp
}, "message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message
"time": 1502812026, // timestamp }
"message": "[2017-08-15 15:47:06] @nick0 followed relay: https://example.org/relay" // log message ],
} "total": 1
] }
``` ```
## `POST /api/v1/pleroma/admin/reload_emoji` ## `POST /api/v1/pleroma/admin/reload_emoji`

View file

@ -42,6 +42,7 @@ Has these additional fields under the `pleroma` object:
- `quotes_count`: the count of status quotes. - `quotes_count`: the count of status quotes.
- `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen. - `non_anonymous`: true if the source post specifies the poll results are not anonymous. Currently only implemented by Smithereen.
- `bookmark_folder`: the ID of the folder bookmark is stored within (if any). - `bookmark_folder`: the ID of the folder bookmark is stored within (if any).
- `list_id`: the ID of the list the post is addressed to (if any, only returned to author).
The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes: The `GET /api/v1/statuses/:id/source` endpoint additionally has the following attributes:
@ -103,7 +104,7 @@ Has these additional fields under the `pleroma` object:
- `background_image`: nullable URL string, background image of the user - `background_image`: nullable URL string, background image of the user
- `tags`: Lists an array of tags for the user - `tags`: Lists an array of tags for the user
- `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/ - `relationship` (object): Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
- `is_moderator`: boolean, nullable, true if user is a moderator - `is_moderator`: boolean, nullable, true if user is a moderator
- `is_admin`: boolean, nullable, true if user is an admin - `is_admin`: boolean, nullable, true if user is an admin
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_favorites`: boolean, true when the user has hiding favorites enabled - `hide_favorites`: boolean, true when the user has hiding favorites enabled
@ -120,6 +121,8 @@ Has these additional fields under the `pleroma` object:
- `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned. - `notification_settings`: object, can be absent. See `/api/v1/pleroma/notification_settings` for the parameters/keys returned.
- `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user - `accepts_chat_messages`: boolean, but can be null if we don't have that information about a user
- `favicon`: nullable URL string, Favicon image of the user's instance - `favicon`: nullable URL string, Favicon image of the user's instance
- `avatar_description`: string, image description for user avatar, defaults to empty string
- `header_description`: string, image description for user banner, defaults to empty string
### Source ### Source
@ -255,6 +258,8 @@ Additional parameters can be added to the JSON body/Form data:
- `actor_type` - the type of this account. - `actor_type` - the type of this account.
- `accepts_chat_messages` - if false, this account will reject all chat messages. - `accepts_chat_messages` - if false, this account will reject all chat messages.
- `language` - user's preferred language for receiving emails (digest, confirmation, etc.) - `language` - user's preferred language for receiving emails (digest, confirmation, etc.)
- `avatar_description` - image description for user avatar
- `header_description` - image description for user banner
All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file. All images (avatar, banner and background) can be reset to the default by sending an empty string ("") instead of a file.

View file

@ -14,7 +14,7 @@ Note: This article is potentially outdated because at this time we may not have
- PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) - PostgreSQL 11.0以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
- `postgresql-contrib` 11.0以上 (同上) - `postgresql-contrib` 11.0以上 (同上)
- Elixir 1.13 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) - Elixir 1.14 以上 ([Debianのリポジトリからインストールしないこと!!! ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- `erlang-dev` - `erlang-dev`
- `erlang-nox` - `erlang-nox`
- `git` - `git`

View file

@ -1,8 +1,8 @@
## Required dependencies ## Required dependencies
* PostgreSQL >=11.0 * PostgreSQL >=11.0
* Elixir >=1.13.0 <1.17 * Elixir >=1.14.0 <1.17
* Erlang OTP >=22.2.0 (supported: <27) * Erlang OTP >=23.0.0 (supported: <27)
* git * git
* file / libmagic * file / libmagic
* gcc or clang * gcc or clang

View file

@ -0,0 +1,25 @@
defmodule Mix.Tasks.Pleroma.TestRunner do
@shortdoc "Retries tests once if they fail"
use Mix.Task
def run(args \\ []) do
case System.cmd("mix", ["test"] ++ args, into: IO.stream(:stdio, :line)) do
{_, 0} ->
:ok
_ ->
retry(args)
end
end
def retry(args) do
case System.cmd("mix", ["test", "--failed"] ++ args, into: IO.stream(:stdio, :line)) do
{_, 0} ->
:ok
_ ->
exit(1)
end
end
end

View file

@ -94,6 +94,7 @@ defmodule Pleroma.Application do
children = children =
[ [
Pleroma.PromEx, Pleroma.PromEx,
Pleroma.LDAP,
Pleroma.Repo, Pleroma.Repo,
Config.TransferTask, Config.TransferTask,
Pleroma.Emoji, Pleroma.Emoji,

View file

@ -22,7 +22,8 @@ defmodule Pleroma.Config.TransferTask do
{:pleroma, :markup}, {:pleroma, :markup},
{:pleroma, :streamer}, {:pleroma, :streamer},
{:pleroma, :pools}, {:pleroma, :pools},
{:pleroma, :connections_pool} {:pleroma, :connections_pool},
{:pleroma, :ldap}
] ]
defp reboot_time_subkeys, defp reboot_time_subkeys,

View file

@ -85,6 +85,36 @@ defmodule Pleroma.Constants do
] ]
) )
const(activity_types,
do: [
"Create",
"Update",
"Delete",
"Follow",
"Accept",
"Reject",
"Add",
"Remove",
"Like",
"Announce",
"Undo",
"Flag",
"EmojiReact"
]
)
const(allowed_activity_types_from_strangers,
do: [
"Block",
"Create",
"Flag",
"Follow",
"Like",
"EmojiReact",
"Announce"
]
)
# basic regex, just there to weed out potential mistakes # basic regex, just there to weed out potential mistakes
# https://datatracker.ietf.org/doc/html/rfc2045#section-5.1 # https://datatracker.ietf.org/doc/html/rfc2045#section-5.1
const(mime_regex, const(mime_regex,

View file

@ -52,6 +52,7 @@ defmodule Pleroma.HTTP.AdapterHelper do
case adapter() do case adapter() do
Tesla.Adapter.Gun -> AdapterHelper.Gun Tesla.Adapter.Gun -> AdapterHelper.Gun
Tesla.Adapter.Hackney -> AdapterHelper.Hackney Tesla.Adapter.Hackney -> AdapterHelper.Hackney
{Tesla.Adapter.Finch, _} -> AdapterHelper.Finch
_ -> AdapterHelper.Default _ -> AdapterHelper.Default
end end
end end
@ -118,4 +119,13 @@ defmodule Pleroma.HTTP.AdapterHelper do
host_charlist host_charlist
end end
end end
@spec can_stream? :: bool()
def can_stream? do
case Application.get_env(:tesla, :adapter) do
Tesla.Adapter.Gun -> true
{Tesla.Adapter.Finch, _} -> true
_ -> false
end
end
end end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTTP.AdapterHelper.Finch do
@behaviour Pleroma.HTTP.AdapterHelper
alias Pleroma.Config
alias Pleroma.HTTP.AdapterHelper
@spec options(keyword(), URI.t()) :: keyword()
def options(incoming_opts \\ [], %URI{} = _uri) do
proxy =
[:http, :proxy_url]
|> Config.get()
|> AdapterHelper.format_proxy()
config_opts = Config.get([:http, :adapter], [])
config_opts
|> Keyword.merge(incoming_opts)
|> AdapterHelper.maybe_add_proxy(proxy)
|> maybe_stream()
end
# Finch uses [response: :stream]
defp maybe_stream(opts) do
case Keyword.pop(opts, :stream, nil) do
{true, opts} -> Keyword.put(opts, :response, :stream)
{_, opts} -> opts
end
end
end

View file

@ -32,6 +32,7 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
|> AdapterHelper.maybe_add_proxy(proxy) |> AdapterHelper.maybe_add_proxy(proxy)
|> Keyword.merge(incoming_opts) |> Keyword.merge(incoming_opts)
|> put_timeout() |> put_timeout()
|> maybe_stream()
end end
defp add_scheme_opts(opts, %{scheme: "http"}), do: opts defp add_scheme_opts(opts, %{scheme: "http"}), do: opts
@ -47,6 +48,14 @@ defmodule Pleroma.HTTP.AdapterHelper.Gun do
Keyword.put(opts, :timeout, recv_timeout) Keyword.put(opts, :timeout, recv_timeout)
end end
# Gun uses [body_as: :stream]
defp maybe_stream(opts) do
case Keyword.pop(opts, :stream, nil) do
{true, opts} -> Keyword.put(opts, :body_as, :stream)
{_, opts} -> opts
end
end
@spec pool_timeout(pool()) :: non_neg_integer() @spec pool_timeout(pool()) :: non_neg_integer()
def pool_timeout(pool) do def pool_timeout(pool) do
default = Config.get([:pools, :default, :recv_timeout], 5_000) default = Config.get([:pools, :default, :recv_timeout], 5_000)

245
lib/pleroma/ldap.ex Normal file
View file

@ -0,0 +1,245 @@
defmodule Pleroma.LDAP do
use GenServer
require Logger
alias Pleroma.Config
alias Pleroma.User
import Pleroma.Web.Auth.Helpers, only: [fetch_user: 1]
@connection_timeout 2_000
@search_timeout 2_000
def start_link(_) do
GenServer.start_link(__MODULE__, [], name: __MODULE__)
end
@impl true
def init(state) do
case {Config.get(Pleroma.Web.Auth.Authenticator), Config.get([:ldap, :enabled])} do
{Pleroma.Web.Auth.LDAPAuthenticator, true} ->
{:ok, state, {:continue, :connect}}
{Pleroma.Web.Auth.LDAPAuthenticator, false} ->
Logger.error(
"LDAP Authenticator enabled but :pleroma, :ldap is not enabled. Auth will not work."
)
{:ok, state}
{_, true} ->
Logger.warning(
":pleroma, :ldap is enabled but Pleroma.Web.Authenticator is not set to the LDAPAuthenticator. LDAP will not be used."
)
{:ok, state}
_ ->
{:ok, state}
end
end
@impl true
def handle_continue(:connect, _state), do: do_handle_connect()
@impl true
def handle_info(:connect, _state), do: do_handle_connect()
def handle_info({:bind_after_reconnect, name, password, from}, state) do
result = bind_user(state[:handle], name, password)
GenServer.reply(from, result)
{:noreply, state}
end
defp do_handle_connect do
state =
case connect() do
{:ok, handle} ->
:eldap.controlling_process(handle, self())
Process.link(handle)
[handle: handle]
_ ->
Logger.error("Failed to connect to LDAP. Retrying in 5000ms")
Process.send_after(self(), :connect, 5_000)
[]
end
{:noreply, state}
end
@impl true
def handle_call({:bind_user, name, password}, from, state) do
case bind_user(state[:handle], name, password) do
:needs_reconnect ->
Process.send(self(), {:bind_after_reconnect, name, password, from}, [])
{:noreply, state, {:continue, :connect}}
result ->
{:reply, result, state, :hibernate}
end
end
@impl true
def terminate(_, state) do
handle = Keyword.get(state, :handle)
if not is_nil(handle) do
:eldap.close(handle)
end
:ok
end
def bind_user(name, password) do
GenServer.call(__MODULE__, {:bind_user, name, password})
end
defp connect do
ldap = Config.get(:ldap, [])
host = Keyword.get(ldap, :host, "localhost")
port = Keyword.get(ldap, :port, 389)
ssl = Keyword.get(ldap, :ssl, false)
tls = Keyword.get(ldap, :tls, false)
cacertfile = Keyword.get(ldap, :cacertfile) || CAStore.file_path()
if ssl, do: Application.ensure_all_started(:ssl)
default_secure_opts = [
verify: :verify_peer,
cacerts: decode_certfile(cacertfile),
customize_hostname_check: [
fqdn_fun: fn _ -> to_charlist(host) end
]
]
sslopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :sslopts, []))
tlsopts = Keyword.merge(default_secure_opts, Keyword.get(ldap, :tlsopts, []))
default_options = [{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}]
# :sslopts can only be included in :eldap.open/2 when {ssl: true}
# or the connection will fail
options =
if ssl do
default_options ++ [{:sslopts, sslopts}]
else
default_options
end
case :eldap.open([to_charlist(host)], options) do
{:ok, handle} ->
try do
cond do
tls ->
case :eldap.start_tls(
handle,
tlsopts,
@connection_timeout
) do
:ok ->
{:ok, handle}
error ->
Logger.error("Could not start TLS: #{inspect(error)}")
:eldap.close(handle)
end
true ->
{:ok, handle}
end
after
:ok
end
{:error, error} ->
Logger.error("Could not open LDAP connection: #{inspect(error)}")
{:error, {:ldap_connection_error, error}}
end
end
defp bind_user(handle, name, password) do
uid = Config.get([:ldap, :uid], "cn")
base = Config.get([:ldap, :base])
case :eldap.simple_bind(handle, "#{uid}=#{name},#{base}", password) do
:ok ->
case fetch_user(name) do
%User{} = user ->
user
_ ->
register_user(handle, base, uid, name)
end
# eldap does not inform us of socket closure
# until it is used
{:error, {:gen_tcp_error, :closed}} ->
:eldap.close(handle)
:needs_reconnect
{:error, error} = e ->
Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}")
e
end
end
defp register_user(handle, base, uid, name) do
case :eldap.search(handle, [
{:base, to_charlist(base)},
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
{:scope, :eldap.wholeSubtree()},
{:timeout, @search_timeout}
]) do
# The :eldap_search_result record structure changed in OTP 24.3 and added a controls field
# https://github.com/erlang/otp/pull/5538
{:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} ->
try_register(name, attributes)
{:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} ->
try_register(name, attributes)
error ->
Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}")
{:error, {:ldap_search_error, error}}
end
end
defp try_register(name, attributes) do
mail_attribute = Config.get([:ldap, :mail])
params = %{
name: name,
nickname: name,
password: nil
}
params =
case List.keyfind(attributes, to_charlist(mail_attribute), 0) do
{_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
_ -> params
end
changeset = User.register_changeset_ldap(%User{}, params)
case User.register(changeset) do
{:ok, user} -> user
error -> error
end
end
defp decode_certfile(file) do
with {:ok, data} <- File.read(file) do
data
|> :public_key.pem_decode()
|> Enum.map(fn {_, b, _} -> b end)
else
_ ->
Logger.error("Unable to read certfile: #{file}")
[]
end
end
end

View file

@ -20,15 +20,13 @@ defmodule Pleroma.Maps do
end end
def filter_empty_values(data) do def filter_empty_values(data) do
# TODO: Change to Map.filter in Elixir 1.13+
data data
|> Enum.filter(fn |> Map.filter(fn
{_k, nil} -> false {_k, nil} -> false
{_k, ""} -> false {_k, ""} -> false
{_k, []} -> false {_k, []} -> false
{_k, %{} = v} -> Map.keys(v) != [] {_k, %{} = v} -> Map.keys(v) != []
{_k, _v} -> true {_k, _v} -> true
end) end)
|> Map.new()
end end
end end

View file

@ -58,8 +58,12 @@ defmodule Pleroma.Object.Fetcher do
end end
end end
@typep fetcher_errors ::
:error | :reject | :allowed_depth | :fetch | :containment | :transmogrifier
# Note: will create a Create activity, which we need internally at the moment. # Note: will create a Create activity, which we need internally at the moment.
@spec fetch_object_from_id(String.t(), list()) :: {:ok, Object.t()} | {:error | :reject, any()} @spec fetch_object_from_id(String.t(), list()) ::
{:ok, Object.t()} | {fetcher_errors(), any()} | Pipeline.errors()
def fetch_object_from_id(id, options \\ []) do def fetch_object_from_id(id, options \\ []) do
with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)}, with {_, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
{_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])}, {_, true} <- {:allowed_depth, Federator.allowed_thread_distance?(options[:depth])},
@ -141,6 +145,7 @@ defmodule Pleroma.Object.Fetcher do
Logger.debug("Fetching object #{id} via AP") Logger.debug("Fetching object #{id} via AP")
with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")}, with {:scheme, true} <- {:scheme, String.starts_with?(id, "http")},
{_, true} <- {:mrf, MRF.id_filter(id)},
{:ok, body} <- get_object(id), {:ok, body} <- get_object(id),
{:ok, data} <- safe_json_decode(body), {:ok, data} <- safe_json_decode(body),
:ok <- Containment.contain_origin_from_id(id, data) do :ok <- Containment.contain_origin_from_id(id, data) do
@ -156,6 +161,9 @@ defmodule Pleroma.Object.Fetcher do
{:error, e} -> {:error, e} ->
{:error, e} {:error, e}
{:mrf, false} ->
{:error, {:reject, "Filtered by id"}}
e -> e ->
{:error, e} {:error, e}
end end

View file

@ -419,6 +419,11 @@ defmodule Pleroma.User do
end end
end end
def image_description(image, default \\ "")
def image_description(%{"name" => name}, _default), do: name
def image_description(_, default), do: default
# Should probably be renamed or removed # Should probably be renamed or removed
@spec ap_id(User.t()) :: String.t() @spec ap_id(User.t()) :: String.t()
def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}" def ap_id(%User{nickname: nickname}), do: "#{Endpoint.url()}/users/#{nickname}"
@ -586,16 +591,26 @@ defmodule Pleroma.User do
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types()) |> validate_inclusion(:actor_type, Pleroma.Constants.allowed_user_actor_types())
|> validate_image_description(:avatar_description, params)
|> validate_image_description(:header_description, params)
|> put_fields() |> put_fields()
|> put_emoji() |> put_emoji()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)}) |> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar)) |> put_change_if_present(
|> put_change_if_present(:banner, &put_upload(&1, :banner)) :avatar,
&put_upload(&1, :avatar, Map.get(params, :avatar_description))
)
|> put_change_if_present(
:banner,
&put_upload(&1, :banner, Map.get(params, :header_description))
)
|> put_change_if_present(:background, &put_upload(&1, :background)) |> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present( |> put_change_if_present(
:pleroma_settings_store, :pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)} &{:ok, Map.merge(struct.pleroma_settings_store, &1)}
) )
|> maybe_update_image_description(:avatar, Map.get(params, :avatar_description))
|> maybe_update_image_description(:banner, Map.get(params, :header_description))
|> validate_fields(false) |> validate_fields(false)
end end
@ -674,13 +689,41 @@ defmodule Pleroma.User do
end end
end end
defp put_upload(value, type) do defp put_upload(value, type, description \\ nil) do
with %Plug.Upload{} <- value, with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do {:ok, object} <- ActivityPub.upload(value, type: type, description: description) do
{:ok, object.data} {:ok, object.data}
end end
end end
defp validate_image_description(changeset, key, params) do
description_limit = Config.get([:instance, :description_limit], 5_000)
description = Map.get(params, key)
if is_binary(description) and String.length(description) > description_limit do
changeset
|> add_error(key, "#{key} is too long")
else
changeset
end
end
defp maybe_update_image_description(changeset, image_field, description)
when is_binary(description) do
with {:image_missing, true} <- {:image_missing, not changed?(changeset, image_field)},
{:existing_image, %{"id" => id}} <-
{:existing_image, Map.get(changeset.data, image_field)},
{:object, %Object{} = object} <- {:object, Object.get_by_ap_id(id)},
{:ok, object} <- Object.update_data(object, %{"name" => description}) do
put_change(changeset, image_field, object.data)
else
{:description_too_long, true} -> {:error}
_ -> changeset
end
end
defp maybe_update_image_description(changeset, _, _), do: changeset
def update_as_admin_changeset(struct, params) do def update_as_admin_changeset(struct, params) do
struct struct
|> update_changeset(params) |> update_changeset(params)

View file

@ -92,9 +92,6 @@ defmodule Pleroma.User.Backup do
else else
true -> true ->
{:error, "Backup is missing id. Please insert it into the Repo first."} {:error, "Backup is missing id. Please insert it into the Repo first."}
e ->
{:error, e}
end end
end end
@ -121,14 +118,13 @@ defmodule Pleroma.User.Backup do
end end
defp permitted?(user) do defp permitted?(user) do
with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)}, with {_, %__MODULE__{inserted_at: inserted_at}} <- {:last, get_last(user)} do
days = Config.get([__MODULE__, :limit_days]), days = Config.get([__MODULE__, :limit_days])
diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days), diff = Timex.diff(NaiveDateTime.utc_now(), inserted_at, :days)
{_, true} <- {:diff, diff > days} do
true diff > days
else else
{:last, nil} -> true {:last, nil} -> true
{:diff, false} -> false
end end
end end
@ -297,9 +293,6 @@ defmodule Pleroma.User.Backup do
) )
acc acc
_ ->
acc
end end
end) end)

View file

@ -12,7 +12,7 @@ defmodule Pleroma.User.Import do
require Logger require Logger
@spec perform(atom(), User.t(), list()) :: :ok | list() | {:error, any()} @spec perform(atom(), User.t(), String.t()) :: :ok | {:error, any()}
def perform(:mute_import, %User{} = user, actor) do def perform(:mute_import, %User{} = user, actor) do
with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor), with {:ok, %User{} = muted_user} <- User.get_or_fetch(actor),
{_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)}, {_, false} <- {:existing_mute, User.mutes_user?(user, muted_user)},
@ -49,7 +49,7 @@ defmodule Pleroma.User.Import do
defp handle_error(op, user_id, error) do defp handle_error(op, user_id, error) do
Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}") Logger.debug("#{op} failed for #{user_id} with: #{inspect(error)}")
error {:error, error}
end end
def blocks_import(%User{} = user, [_ | _] = actors) do def blocks_import(%User{} = user, [_ | _] = actors) do

View file

@ -1542,16 +1542,23 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp get_actor_url(_url), do: nil defp get_actor_url(_url), do: nil
defp normalize_image(%{"url" => url}) do defp normalize_image(%{"url" => url} = data) do
%{ %{
"type" => "Image", "type" => "Image",
"url" => [%{"href" => url}] "url" => [%{"href" => url}]
} }
|> maybe_put_description(data)
end end
defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image() defp normalize_image(urls) when is_list(urls), do: urls |> List.first() |> normalize_image()
defp normalize_image(_), do: nil defp normalize_image(_), do: nil
defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do
Map.put(map, "name", description)
end
defp maybe_put_description(map, _), do: map
defp object_to_user_data(data, additional) do defp object_to_user_data(data, additional) do
fields = fields =
data data

View file

@ -311,7 +311,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
post_inbox_relayed_create(conn, params) post_inbox_relayed_create(conn, params)
else else
conn conn
|> put_status(:bad_request) |> put_status(403)
|> json("Not federating") |> json("Not federating")
end end
end end

View file

@ -108,6 +108,14 @@ defmodule Pleroma.Web.ActivityPub.MRF do
def filter(%{} = object), do: get_policies() |> filter(object) def filter(%{} = object), do: get_policies() |> filter(object)
def id_filter(policies, id) when is_binary(id) do
policies
|> Enum.filter(&function_exported?(&1, :id_filter, 1))
|> Enum.all?(& &1.id_filter(id))
end
def id_filter(id) when is_binary(id), do: get_policies() |> id_filter(id)
@impl true @impl true
def pipeline_filter(%{} = message, meta) do def pipeline_filter(%{} = message, meta) do
object = meta[:object_data] object = meta[:object_data]

View file

@ -13,6 +13,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.DropPolicy do
{:reject, activity} {:reject, activity}
end end
@impl true
def id_filter(id) do
Logger.debug("REJECTING #{id}")
false
end
@impl true @impl true
def describe, do: {:ok, %{}} def describe, do: {:ok, %{}}
end end

View file

@ -4,6 +4,7 @@
defmodule Pleroma.Web.ActivityPub.MRF.Policy do defmodule Pleroma.Web.ActivityPub.MRF.Policy do
@callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()} @callback filter(Pleroma.Activity.t()) :: {:ok | :reject, Pleroma.Activity.t()}
@callback id_filter(String.t()) :: boolean()
@callback describe() :: {:ok | :error, map()} @callback describe() :: {:ok | :error, map()}
@callback config_description() :: %{ @callback config_description() :: %{
optional(:children) => [map()], optional(:children) => [map()],
@ -13,5 +14,5 @@ defmodule Pleroma.Web.ActivityPub.MRF.Policy do
description: String.t() description: String.t()
} }
@callback history_awareness() :: :auto | :manual @callback history_awareness() :: :auto | :manual
@optional_callbacks config_description: 0, history_awareness: 0 @optional_callbacks config_description: 0, history_awareness: 0, id_filter: 1
end end

View file

@ -191,6 +191,18 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do
|> MRF.instance_list_from_tuples() |> MRF.instance_list_from_tuples()
end end
@impl true
def id_filter(id) do
host_info = URI.parse(id)
with {:ok, _} <- check_accept(host_info, %{}),
{:ok, _} <- check_reject(host_info, %{}) do
true
else
_ -> false
end
end
@impl true @impl true
def filter(%{"type" => "Delete", "actor" => actor} = activity) do def filter(%{"type" => "Delete", "actor" => actor} = activity) do
%{host: actor_host} = URI.parse(actor) %{host: actor_host} = URI.parse(actor)

View file

@ -22,22 +22,27 @@ defmodule Pleroma.Web.ActivityPub.Pipeline do
defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub) defp activity_pub, do: Config.get([:pipeline, :activity_pub], ActivityPub)
defp config, do: Config.get([:pipeline, :config], Config) defp config, do: Config.get([:pipeline, :config], Config)
@spec common_pipeline(map(), keyword()) :: @type results :: {:ok, Activity.t() | Object.t(), keyword()}
{:ok, Activity.t() | Object.t(), keyword()} | {:error | :reject, any()} @type errors :: {:error | :reject, any()}
# The Repo.transaction will wrap the result in an {:ok, _}
# and only returns an {:error, _} if the error encountered was related
# to the SQL transaction
@spec common_pipeline(map(), keyword()) :: results() | errors()
def common_pipeline(object, meta) do def common_pipeline(object, meta) do
case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do case Repo.transaction(fn -> do_common_pipeline(object, meta) end, Utils.query_timeout()) do
{:ok, {:ok, activity, meta}} -> {:ok, {:ok, activity, meta}} ->
side_effects().handle_after_transaction(meta) side_effects().handle_after_transaction(meta)
{:ok, activity, meta} {:ok, activity, meta}
{:ok, value} -> {:ok, {:error, _} = error} ->
value error
{:ok, {:reject, _} = error} ->
error
{:error, e} -> {:error, e} ->
{:error, e} {:error, e}
{:reject, e} ->
{:reject, e}
end end
end end

View file

@ -129,8 +129,22 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"vcard:bday" => birthday, "vcard:bday" => birthday,
"webfinger" => "acct:#{User.full_nickname(user)}" "webfinger" => "acct:#{User.full_nickname(user)}"
} }
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user)) |> Map.merge(
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user)) maybe_make_image(
&User.avatar_url/2,
User.image_description(user.avatar, nil),
"icon",
user
)
)
|> Map.merge(
maybe_make_image(
&User.banner_url/2,
User.image_description(user.banner, nil),
"image",
user
)
)
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -305,16 +319,24 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end end
end end
defp maybe_make_image(func, key, user) do defp maybe_make_image(func, description, key, user) do
if image = func.(user, no_default: true) do if image = func.(user, no_default: true) do
%{ %{
key => %{ key =>
"type" => "Image", %{
"url" => image "type" => "Image",
} "url" => image
}
|> maybe_put_description(description)
} }
else else
%{} %{}
end end
end end
defp maybe_put_description(map, description) when is_binary(description) do
Map.put(map, "name", description)
end
defp maybe_put_description(map, _description), do: map
end end

View file

@ -813,6 +813,16 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
allOf: [BooleanLike], allOf: [BooleanLike],
nullable: true, nullable: true,
description: "User's birthday will be visible" description: "User's birthday will be visible"
},
avatar_description: %Schema{
type: :string,
nullable: true,
description: "Avatar image description."
},
header_description: %Schema{
type: :string,
nullable: true,
description: "Header image description."
} }
}, },
example: %{ example: %{

View file

@ -158,6 +158,10 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
type: :object, type: :object,
properties: %{ properties: %{
id: %Schema{type: :string}, id: %Schema{type: :string},
group_key: %Schema{
type: :string,
description: "Group key shared by similar notifications"
},
type: notification_type(), type: notification_type(),
created_at: %Schema{type: :string, format: :"date-time"}, created_at: %Schema{type: :string, format: :"date-time"},
account: %Schema{ account: %Schema{
@ -180,6 +184,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
}, },
example: %{ example: %{
"id" => "34975861", "id" => "34975861",
"group-key" => "ungrouped-34975861",
"type" => "mention", "type" => "mention",
"created_at" => "2019-11-23T07:49:02.064Z", "created_at" => "2019-11-23T07:49:02.064Z",
"account" => Account.schema().example, "account" => Account.schema().example,

View file

@ -111,7 +111,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
format: :uri, format: :uri,
nullable: true, nullable: true,
description: "Favicon image of the user's instance" description: "Favicon image of the user's instance"
} },
avatar_description: %Schema{type: :string},
header_description: %Schema{type: :string}
} }
}, },
source: %Schema{ source: %Schema{
@ -152,6 +154,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
example: %{ example: %{
"acct" => "foobar", "acct" => "foobar",
"avatar" => "https://mypleroma.com/images/avi.png", "avatar" => "https://mypleroma.com/images/avi.png",
"avatar_description" => "",
"avatar_static" => "https://mypleroma.com/images/avi.png", "avatar_static" => "https://mypleroma.com/images/avi.png",
"bot" => false, "bot" => false,
"created_at" => "2020-03-24T13:05:58.000Z", "created_at" => "2020-03-24T13:05:58.000Z",
@ -162,6 +165,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
"followers_count" => 0, "followers_count" => 0,
"following_count" => 1, "following_count" => 1,
"header" => "https://mypleroma.com/images/banner.png", "header" => "https://mypleroma.com/images/banner.png",
"header_description" => "",
"header_static" => "https://mypleroma.com/images/banner.png", "header_static" => "https://mypleroma.com/images/banner.png",
"id" => "9tKi3esbG7OQgZ2920", "id" => "9tKi3esbG7OQgZ2920",
"locked" => false, "locked" => false,

View file

@ -249,6 +249,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
nullable: true, nullable: true,
description: description:
"A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned" "A datetime (ISO 8601) that states when the post was pinned or `null` if the post is not pinned"
},
list_id: %Schema{
type: :integer,
nullable: true,
description:
"The ID of the list the post is addressed to (if any, only returned to author)"
} }
} }
}, },

View file

@ -3,18 +3,14 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Auth.LDAPAuthenticator do defmodule Pleroma.Web.Auth.LDAPAuthenticator do
alias Pleroma.LDAP
alias Pleroma.User alias Pleroma.User
require Logger import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1]
import Pleroma.Web.Auth.Helpers, only: [fetch_credentials: 1, fetch_user: 1]
@behaviour Pleroma.Web.Auth.Authenticator @behaviour Pleroma.Web.Auth.Authenticator
@base Pleroma.Web.Auth.PleromaAuthenticator @base Pleroma.Web.Auth.PleromaAuthenticator
@connection_timeout 10_000
@search_timeout 10_000
defdelegate get_registration(conn), to: @base defdelegate get_registration(conn), to: @base
defdelegate create_from_registration(conn, registration), to: @base defdelegate create_from_registration(conn, registration), to: @base
defdelegate handle_error(conn, error), to: @base defdelegate handle_error(conn, error), to: @base
@ -24,7 +20,7 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
def get_user(%Plug.Conn{} = conn) do def get_user(%Plug.Conn{} = conn) do
with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])}, with {:ldap, true} <- {:ldap, Pleroma.Config.get([:ldap, :enabled])},
{:ok, {name, password}} <- fetch_credentials(conn), {:ok, {name, password}} <- fetch_credentials(conn),
%User{} = user <- ldap_user(name, password) do %User{} = user <- LDAP.bind_user(name, password) do
{:ok, user} {:ok, user}
else else
{:ldap, _} -> {:ldap, _} ->
@ -34,107 +30,4 @@ defmodule Pleroma.Web.Auth.LDAPAuthenticator do
error error
end end
end end
defp ldap_user(name, password) do
ldap = Pleroma.Config.get(:ldap, [])
host = Keyword.get(ldap, :host, "localhost")
port = Keyword.get(ldap, :port, 389)
ssl = Keyword.get(ldap, :ssl, false)
sslopts = Keyword.get(ldap, :sslopts, [])
options =
[{:port, port}, {:ssl, ssl}, {:timeout, @connection_timeout}] ++
if sslopts != [], do: [{:sslopts, sslopts}], else: []
case :eldap.open([to_charlist(host)], options) do
{:ok, connection} ->
try do
if Keyword.get(ldap, :tls, false) do
:application.ensure_all_started(:ssl)
case :eldap.start_tls(
connection,
Keyword.get(ldap, :tlsopts, []),
@connection_timeout
) do
:ok ->
:ok
error ->
Logger.error("Could not start TLS: #{inspect(error)}")
end
end
bind_user(connection, ldap, name, password)
after
:eldap.close(connection)
end
{:error, error} ->
Logger.error("Could not open LDAP connection: #{inspect(error)}")
{:error, {:ldap_connection_error, error}}
end
end
defp bind_user(connection, ldap, name, password) do
uid = Keyword.get(ldap, :uid, "cn")
base = Keyword.get(ldap, :base)
case :eldap.simple_bind(connection, "#{uid}=#{name},#{base}", password) do
:ok ->
case fetch_user(name) do
%User{} = user ->
user
_ ->
register_user(connection, base, uid, name)
end
error ->
Logger.error("Could not bind LDAP user #{name}: #{inspect(error)}")
{:error, {:ldap_bind_error, error}}
end
end
defp register_user(connection, base, uid, name) do
case :eldap.search(connection, [
{:base, to_charlist(base)},
{:filter, :eldap.equalityMatch(to_charlist(uid), to_charlist(name))},
{:scope, :eldap.wholeSubtree()},
{:timeout, @search_timeout}
]) do
# The :eldap_search_result record structure changed in OTP 24.3 and added a controls field
# https://github.com/erlang/otp/pull/5538
{:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals}} ->
try_register(name, attributes)
{:ok, {:eldap_search_result, [{:eldap_entry, _object, attributes}], _referrals, _controls}} ->
try_register(name, attributes)
error ->
Logger.error("Couldn't register user because LDAP search failed: #{inspect(error)}")
{:error, {:ldap_search_error, error}}
end
end
defp try_register(name, attributes) do
params = %{
name: name,
nickname: name,
password: nil
}
params =
case List.keyfind(attributes, ~c"mail", 0) do
{_, [mail]} -> Map.put_new(params, :email, :erlang.list_to_binary(mail))
_ -> params
end
changeset = User.register_changeset_ldap(%User{}, params)
case User.register(changeset) do
{:ok, user} -> user
error -> error
end
end
end end

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.CommonAPI do
require Pleroma.Constants require Pleroma.Constants
require Logger require Logger
@spec block(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec block(User.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def block(blocked, blocker) do def block(blocked, blocker) do
with {:ok, block_data, _} <- Builder.block(blocker, blocked), with {:ok, block_data, _} <- Builder.block(blocker, blocked),
{:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do {:ok, block, _} <- Pipeline.common_pipeline(block_data, local: true) do
@ -35,7 +35,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
@spec post_chat_message(User.t(), User.t(), String.t(), list()) :: @spec post_chat_message(User.t(), User.t(), String.t(), list()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | Pipeline.errors()
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]), with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
:ok <- validate_chat_attachment_attribution(maybe_attachment, user), :ok <- validate_chat_attachment_attribution(maybe_attachment, user),
@ -58,7 +58,7 @@ defmodule Pleroma.Web.CommonAPI do
)} do )} do
{:ok, activity} {:ok, activity}
else else
{:common_pipeline, {:reject, _} = e} -> e {:common_pipeline, e} -> e
e -> e e -> e
end end
end end
@ -99,7 +99,8 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec unblock(User.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec unblock(User.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :no_activity} | Pipeline.errors() | {:error, :not_blocking}
def unblock(blocked, blocker) do def unblock(blocked, blocker) do
with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)}, with {_, %Activity{} = block} <- {:fetch_block, Utils.fetch_latest_block(blocker, blocked)},
{:ok, unblock_data, _} <- Builder.undo(blocker, block), {:ok, unblock_data, _} <- Builder.undo(blocker, block),
@ -120,7 +121,9 @@ defmodule Pleroma.Web.CommonAPI do
end end
@spec follow(User.t(), User.t()) :: @spec follow(User.t(), User.t()) ::
{:ok, User.t(), User.t(), Activity.t() | Object.t()} | {:error, :rejected} {:ok, User.t(), User.t(), Activity.t() | Object.t()}
| {:error, :rejected}
| Pipeline.errors()
def follow(followed, follower) do def follow(followed, follower) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -145,7 +148,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} @spec accept_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors()
def accept_follow_request(follower, followed) do def accept_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, accept_data, _} <- Builder.accept(followed, follow_activity), {:ok, accept_data, _} <- Builder.accept(followed, follow_activity),
@ -154,7 +157,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | {:error, any()} | nil @spec reject_follow_request(User.t(), User.t()) :: {:ok, User.t()} | Pipeline.errors() | nil
def reject_follow_request(follower, followed) do def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, reject_data, _} <- Builder.reject(followed, follow_activity), {:ok, reject_data, _} <- Builder.reject(followed, follow_activity),
@ -163,7 +166,8 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec delete(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec delete(String.t(), User.t()) ::
{:ok, Activity.t()} | Pipeline.errors() | {:error, :not_found | String.t()}
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, filter: [])}, {:find_activity, Activity.get_by_id(activity_id, filter: [])},
@ -213,7 +217,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} @spec repeat(String.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, :not_found}
def repeat(id, user, params \\ %{}) do def repeat(id, user, params \\ %{}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false), object = %Object{} <- Object.normalize(activity, fetch: false),
@ -231,7 +235,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec unrepeat(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, :not_found | String.t()}
def unrepeat(id, user) do def unrepeat(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)}, {:find_activity, Activity.get_by_id(id)},
@ -247,7 +251,8 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec favorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec favorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:ok, :already_liked} | {:error, :not_found | String.t()}
def favorite(id, %User{} = user) do def favorite(id, %User{} = user) do
case favorite_helper(user, id) do case favorite_helper(user, id) do
{:ok, _} = res -> {:ok, _} = res ->
@ -285,7 +290,8 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec unfavorite(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, any()} @spec unfavorite(String.t(), User.t()) ::
{:ok, Activity.t()} | {:error, :not_found | String.t()}
def unfavorite(id, user) do def unfavorite(id, user) do
with {_, %Activity{data: %{"type" => "Create"}} = activity} <- with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)}, {:find_activity, Activity.get_by_id(id)},
@ -302,7 +308,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
@spec react_with_emoji(String.t(), User.t(), String.t()) :: @spec react_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, String.t()}
def react_with_emoji(id, user, emoji) do def react_with_emoji(id, user, emoji) do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
object <- Object.normalize(activity, fetch: false), object <- Object.normalize(activity, fetch: false),
@ -316,7 +322,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
@spec unreact_with_emoji(String.t(), User.t(), String.t()) :: @spec unreact_with_emoji(String.t(), User.t(), String.t()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, String.t()}
def unreact_with_emoji(id, user, emoji) do def unreact_with_emoji(id, user, emoji) do
with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji), with %Activity{} = reaction_activity <- Utils.get_latest_reaction(id, user, emoji),
{_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)}, {_, {:ok, _}} <- {:cancel_jobs, maybe_cancel_jobs(reaction_activity)},
@ -329,7 +335,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | {:error, any()} @spec vote(Object.t(), User.t(), list()) :: {:ok, list(), Object.t()} | Pipeline.errors()
def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do def vote(%Object{data: %{"type" => "Question"}} = object, %User{} = user, choices) do
with :ok <- validate_not_author(object, user), with :ok <- validate_not_author(object, user),
:ok <- validate_existing_votes(user, object), :ok <- validate_existing_votes(user, object),
@ -461,7 +467,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} @spec update(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, nil}
def update(orig_activity, %User{} = user, changes) do def update(orig_activity, %User{} = user, changes) do
with orig_object <- Object.normalize(orig_activity), with orig_object <- Object.normalize(orig_activity),
{:ok, new_object} <- make_update_data(user, orig_object, changes), {:ok, new_object} <- make_update_data(user, orig_object, changes),
@ -497,7 +503,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} @spec pin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def pin(id, %User{} = user) do def pin(id, %User{} = user) do
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),
true <- activity_belongs_to_actor(activity, user.ap_id), true <- activity_belongs_to_actor(activity, user.ap_id),
@ -537,7 +543,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | {:error, term()} @spec unpin(String.t(), User.t()) :: {:ok, Activity.t()} | Pipeline.errors()
def unpin(id, user) do def unpin(id, user) do
with %Activity{} = activity <- create_activity_by_id(id), with %Activity{} = activity <- create_activity_by_id(id),
{:ok, unpin_data, _} <- Builder.unpin(user, activity.object), {:ok, unpin_data, _} <- Builder.unpin(user, activity.object),
@ -552,7 +558,7 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
@spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, any()} @spec add_mute(Activity.t(), User.t(), map()) :: {:ok, Activity.t()} | {:error, String.t()}
def add_mute(activity, user, params \\ %{}) do def add_mute(activity, user, params \\ %{}) do
expires_in = Map.get(params, :expires_in, 0) expires_in = Map.get(params, :expires_in, 0)

View file

@ -102,7 +102,8 @@ defmodule Pleroma.Web.Federator do
# NOTE: we use the actor ID to do the containment, this is fine because an # NOTE: we use the actor ID to do the containment, this is fine because an
# actor shouldn't be acting on objects outside their own AP server. # actor shouldn't be acting on objects outside their own AP server.
with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)}, with {_, {:ok, user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)},
{:user_active, true} <- {:user_active, match?(true, user.is_active)},
nil <- Activity.normalize(params["id"]), nil <- Activity.normalize(params["id"]),
{_, :ok} <- {_, :ok} <-
{:correct_origin?, Containment.contain_origin_from_id(actor, params)}, {:correct_origin?, Containment.contain_origin_from_id(actor, params)},
@ -121,11 +122,6 @@ defmodule Pleroma.Web.Federator do
Logger.debug("Unhandled actor #{actor}, #{inspect(e)}") Logger.debug("Unhandled actor #{actor}, #{inspect(e)}")
{:error, e} {:error, e}
{:error, {:validate_object, _}} = e ->
Logger.error("Incoming AP doc validation error: #{inspect(e)}")
Logger.debug(Jason.encode!(params, pretty: true))
e
e -> e ->
# Just drop those for now # Just drop those for now
Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end) Logger.debug(fn -> "Unhandled activity\n" <> Jason.encode!(params, pretty: true) end)

View file

@ -232,6 +232,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|> Maps.put_if_present(:is_discoverable, params[:discoverable]) |> Maps.put_if_present(:is_discoverable, params[:discoverable])
|> Maps.put_if_present(:birthday, params[:birthday]) |> Maps.put_if_present(:birthday, params[:birthday])
|> 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(:avatar_description, params[:avatar_description])
|> Maps.put_if_present(:header_description, params[:header_description])
# What happens here: # What happens here:
# #
@ -277,6 +279,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} -> {:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Name is too long") render_error(conn, :request_entity_too_large, "Name is too long")
{:error, %Ecto.Changeset{errors: [{:avatar_description, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Avatar description is too long")
{:error, %Ecto.Changeset{errors: [{:header_description, {_, _}} | _]}} ->
render_error(conn, :request_entity_too_large, "Banner description is too long")
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} -> {:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
render_error(conn, :request_entity_too_large, "One or more field entries are too long") render_error(conn, :request_entity_too_large, "One or more field entries are too long")

View file

@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController) action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(Pleroma.Web.Plugs.RateLimiter, [name: :oauth_app_creation] when action == :create)
plug(:skip_auth when action in [:create, :verify_credentials]) plug(:skip_auth when action in [:create, :verify_credentials])
plug(Pleroma.Web.ApiSpec.CastAndValidate) plug(Pleroma.Web.ApiSpec.CastAndValidate)

View file

@ -92,14 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
User.get_follow_state(reading_user, target) User.get_follow_state(reading_user, target)
end end
followed_by = followed_by = FollowingRelationship.following?(target, reading_user)
if following_relationships do following = FollowingRelationship.following?(reading_user, target)
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: :follow_accept} -> true requested =
_ -> false cond do
end following -> false
else true -> match?(:follow_pending, follow_state)
User.following?(target, reading_user)
end end
subscribing = subscribing =
@ -114,7 +113,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags # NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{ %{
id: to_string(target.id), id: to_string(target.id),
following: follow_state == :follow_accept, following: following,
followed_by: followed_by, followed_by: followed_by,
blocking: blocking:
UserRelationship.exists?( UserRelationship.exists?(
@ -150,7 +149,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
), ),
subscribing: subscribing, subscribing: subscribing,
notifying: subscribing, notifying: subscribing,
requested: follow_state == :follow_pending, requested: requested,
domain_blocking: User.blocks_domain?(reading_user, target), domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs: showing_reblogs:
not UserRelationship.exists?( not UserRelationship.exists?(
@ -220,8 +219,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
avatar = User.avatar_url(user) |> MediaProxy.url() avatar = User.avatar_url(user) |> MediaProxy.url()
avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true) avatar_static = User.avatar_url(user) |> MediaProxy.preview_url(static: true)
avatar_description = User.image_description(user.avatar)
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true) header_static = User.banner_url(user) |> MediaProxy.preview_url(static: true)
header_description = User.image_description(user.banner)
following_count = following_count =
if !user.hide_follows_count or !user.hide_follows or self, if !user.hide_follows_count or !user.hide_follows or self,
@ -322,7 +323,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
skip_thread_containment: user.skip_thread_containment, skip_thread_containment: user.skip_thread_containment,
background_image: image_url(user.background) |> MediaProxy.url(), background_image: image_url(user.background) |> MediaProxy.url(),
accepts_chat_messages: user.accepts_chat_messages, accepts_chat_messages: user.accepts_chat_messages,
favicon: favicon favicon: favicon,
avatar_description: avatar_description,
header_description: header_description
} }
} }
|> maybe_put_role(user, opts[:for]) |> maybe_put_role(user, opts[:for])

View file

@ -95,6 +95,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
response = %{ response = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-" <> to_string(notification.id),
type: notification.type, type: notification.type,
created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at), created_at: CommonAPI.Utils.to_masto_date(notification.inserted_at),
account: account, account: account,

View file

@ -465,7 +465,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
parent_visible: visible_for_user?(reply_to, opts[:for]), parent_visible: visible_for_user?(reply_to, opts[:for]),
pinned_at: pinned_at, pinned_at: pinned_at,
quotes_count: object.data["quotesCount"] || 0, quotes_count: object.data["quotesCount"] || 0,
bookmark_folder: bookmark_folder bookmark_folder: bookmark_folder,
list_id: get_list_id(object, client_posted_this_activity)
} }
} }
end end
@ -803,19 +804,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
defp build_application(_), do: nil defp build_application(_), do: nil
# Workaround for Elixir issue #10771 @spec build_image_url(URI.t(), URI.t()) :: String.t()
# Avoid applying URI.merge unless necessary
# TODO: revert to always attempting URI.merge(image_url_data, page_url_data)
# when Elixir 1.12 is the minimum supported version
@spec build_image_url(struct() | nil, struct()) :: String.t() | nil
defp build_image_url(
%URI{scheme: image_scheme, host: image_host} = image_url_data,
%URI{} = _page_url_data
)
when not is_nil(image_scheme) and not is_nil(image_host) do
image_url_data |> to_string
end
defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do defp build_image_url(%URI{} = image_url_data, %URI{} = page_url_data) do
URI.merge(page_url_data, image_url_data) |> to_string URI.merge(page_url_data, image_url_data) |> to_string
end end
@ -847,4 +836,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
nil nil
end end
end end
defp get_list_id(object, client_posted_this_activity) do
with true <- client_posted_this_activity,
%{data: %{"listMessage" => list_ap_id}} when is_binary(list_ap_id) <- object,
%{id: list_id} <- Pleroma.List.get_by_ap_id(list_ap_id) do
list_id
else
_ -> nil
end
end
end end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.Web.OAuth.App do
import Ecto.Query import Ecto.Query
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.OAuth.Token
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -155,4 +156,29 @@ defmodule Pleroma.Web.OAuth.App do
Map.put(acc, key, error) Map.put(acc, key, error)
end) end)
end end
@spec maybe_update_owner(Token.t()) :: :ok
def maybe_update_owner(%Token{app_id: app_id, user_id: user_id}) when not is_nil(user_id) do
__MODULE__.update(app_id, %{user_id: user_id})
:ok
end
def maybe_update_owner(_), do: :ok
@spec remove_orphans(pos_integer()) :: :ok
def remove_orphans(limit \\ 100) do
fifteen_mins_ago = DateTime.add(DateTime.utc_now(), -900, :second)
Repo.transaction(fn ->
from(a in __MODULE__,
where: is_nil(a.user_id) and a.inserted_at < ^fifteen_mins_ago,
limit: ^limit
)
|> Repo.all()
|> Enum.each(&Repo.delete(&1))
end)
:ok
end
end end

View file

@ -318,6 +318,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do
def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params)
def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do def after_token_exchange(%Plug.Conn{} = conn, %{token: token} = view_params) do
App.maybe_update_owner(token)
conn conn
|> AuthHelper.put_session_token(token.token) |> AuthHelper.put_session_token(token.token)
|> json(OAuthView.render("token.json", view_params)) |> json(OAuthView.render("token.json", view_params))

View file

@ -47,6 +47,11 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
Pleroma.Password.Pbkdf2.verify_pass(password, password_hash) Pleroma.Password.Pbkdf2.verify_pass(password, password_hash)
end end
def checkpw(password, "$argon2" <> _ = password_hash) do
# Handle argon2 passwords for Akkoma migration
Argon2.verify_pass(password, password_hash)
end
def checkpw(_password, _password_hash) do def checkpw(_password, _password_hash) do
Logger.error("Password hash not recognized") Logger.error("Password hash not recognized")
false false
@ -56,6 +61,10 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
do_update_password(user, password) do_update_password(user, password)
end end
def maybe_update_password(%User{password_hash: "$argon2" <> _} = user, password) do
do_update_password(user, password)
end
def maybe_update_password(user, _), do: {:ok, user} def maybe_update_password(user, _), do: {:ok, user}
defp do_update_password(user, password) do defp do_update_password(user, password) do

View file

@ -0,0 +1,89 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.InboxGuardPlug do
import Plug.Conn
import Pleroma.Constants, only: [activity_types: 0, allowed_activity_types_from_strangers: 0]
alias Pleroma.Config
alias Pleroma.User
def init(options) do
options
end
def call(%{assigns: %{valid_signature: true}} = conn, _opts) do
with {_, true} <- {:federating, Config.get!([:instance, :federating])} do
conn
|> filter_activity_types()
else
{:federating, false} ->
conn
|> json(403, "Not federating")
|> halt()
end
end
def call(conn, _opts) do
with {_, true} <- {:federating, Config.get!([:instance, :federating])},
conn = filter_activity_types(conn),
{:known, true} <- {:known, known_actor?(conn)} do
conn
else
{:federating, false} ->
conn
|> json(403, "Not federating")
|> halt()
{:known, false} ->
conn
|> filter_from_strangers()
end
end
# Early rejection of unrecognized types
defp filter_activity_types(%{body_params: %{"type" => type}} = conn) do
with true <- type in activity_types() do
conn
else
_ ->
conn
|> json(400, "Invalid activity type")
|> halt()
end
end
# If signature failed but we know this actor we should
# accept it as we may only need to refetch their public key
# during processing
defp known_actor?(%{body_params: data}) do
case Pleroma.Object.Containment.get_actor(data) |> User.get_cached_by_ap_id() do
%User{} -> true
_ -> false
end
end
# Only permit a subset of activity types from strangers
# or else it will add actors you've never interacted with
# to the database
defp filter_from_strangers(%{body_params: %{"type" => type}} = conn) do
with true <- type in allowed_activity_types_from_strangers() do
conn
else
_ ->
conn
|> json(400, "Invalid activity type for an unknown actor")
|> halt()
end
end
defp json(conn, status, resp) do
json_resp = Jason.encode!(resp)
conn
|> put_resp_content_type("application/json")
|> resp(status, json_resp)
|> halt()
end
end

View file

@ -11,16 +11,39 @@ defmodule Pleroma.Web.RichMedia.Helpers do
@spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors() @spec rich_media_get(String.t()) :: {:ok, String.t()} | get_errors()
def rich_media_get(url) do def rich_media_get(url) do
headers = [{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}] case Pleroma.HTTP.AdapterHelper.can_stream?() do
true -> stream(url)
false -> head_first(url)
end
|> handle_result(url)
end
defp stream(url) do
with {_, {:ok, %Tesla.Env{status: 200, body: stream_body, headers: headers}}} <-
{:get, Pleroma.HTTP.get(url, req_headers(), http_options())},
{_, :ok} <- {:content_type, check_content_type(headers)},
{_, :ok} <- {:content_length, check_content_length(headers)},
{:read_stream, {:ok, body}} <- {:read_stream, read_stream(stream_body)} do
{:ok, body}
end
end
defp head_first(url) do
with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <- with {_, {:ok, %Tesla.Env{status: 200, headers: headers}}} <-
{:head, Pleroma.HTTP.head(url, headers, http_options())}, {:head, Pleroma.HTTP.head(url, req_headers(), http_options())},
{_, :ok} <- {:content_type, check_content_type(headers)}, {_, :ok} <- {:content_type, check_content_type(headers)},
{_, :ok} <- {:content_length, check_content_length(headers)}, {_, :ok} <- {:content_length, check_content_length(headers)},
{_, {:ok, %Tesla.Env{status: 200, body: body}}} <- {_, {:ok, %Tesla.Env{status: 200, body: body}}} <-
{:get, Pleroma.HTTP.get(url, headers, http_options())} do {:get, Pleroma.HTTP.get(url, req_headers(), http_options())} do
{:ok, body} {:ok, body}
else end
end
defp handle_result(result, url) do
case result do
{:ok, body} ->
{:ok, body}
{:head, _} -> {:head, _} ->
Logger.debug("Rich media error for #{url}: HTTP HEAD failed") Logger.debug("Rich media error for #{url}: HTTP HEAD failed")
{:error, :head} {:error, :head}
@ -29,8 +52,12 @@ defmodule Pleroma.Web.RichMedia.Helpers do
Logger.debug("Rich media error for #{url}: content-type is #{type}") Logger.debug("Rich media error for #{url}: content-type is #{type}")
{:error, :content_type} {:error, :content_type}
{:content_length, {_, length}} -> {:content_length, :error} ->
Logger.debug("Rich media error for #{url}: content-length is #{length}") Logger.debug("Rich media error for #{url}: content-length exceeded")
{:error, :body_too_large}
{:read_stream, :error} ->
Logger.debug("Rich media error for #{url}: content-length exceeded")
{:error, :body_too_large} {:error, :body_too_large}
{:get, _} -> {:get, _} ->
@ -59,7 +86,7 @@ defmodule Pleroma.Web.RichMedia.Helpers do
{_, maybe_content_length} -> {_, maybe_content_length} ->
case Integer.parse(maybe_content_length) do case Integer.parse(maybe_content_length) do
{content_length, ""} when content_length <= max_body -> :ok {content_length, ""} when content_length <= max_body -> :ok
{_, ""} -> {:error, maybe_content_length} {_, ""} -> :error
_ -> :ok _ -> :ok
end end
@ -68,13 +95,37 @@ defmodule Pleroma.Web.RichMedia.Helpers do
end end
end end
defp http_options do defp read_stream(stream) do
timeout = Config.get!([:rich_media, :timeout]) max_body = Keyword.get(http_options(), :max_body)
try do
result =
Stream.transform(stream, 0, fn chunk, total_bytes ->
new_total = total_bytes + byte_size(chunk)
if new_total > max_body do
raise("Exceeds max body limit of #{max_body}")
else
{[chunk], new_total}
end
end)
|> Enum.into(<<>>)
{:ok, result}
rescue
_ -> :error
end
end
defp http_options do
[ [
pool: :rich_media, pool: :rich_media,
max_body: Config.get([:rich_media, :max_body], 5_000_000), max_body: Config.get([:rich_media, :max_body], 5_000_000),
tesla_middleware: [{Tesla.Middleware.Timeout, timeout: timeout}] stream: true
] ]
end end
defp req_headers do
[{"user-agent", Pleroma.Application.user_agent() <> "; Bot"}]
end
end end

View file

@ -189,7 +189,7 @@ defmodule Pleroma.Web.Router do
end end
pipeline :well_known do pipeline :well_known do
plug(:accepts, ["json", "jrd", "jrd+json", "xml", "xrd+xml"]) plug(:accepts, ["activity+json", "json", "jrd", "jrd+json", "xml", "xrd+xml"])
end end
pipeline :config do pipeline :config do
@ -217,6 +217,10 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug) plug(Pleroma.Web.Plugs.MappedSignatureToIdentityPlug)
end end
pipeline :inbox_guard do
plug(Pleroma.Web.Plugs.InboxGuardPlug)
end
pipeline :static_fe do pipeline :static_fe do
plug(Pleroma.Web.Plugs.StaticFEPlug) plug(Pleroma.Web.Plugs.StaticFEPlug)
end end
@ -920,7 +924,7 @@ defmodule Pleroma.Web.Router do
end end
scope "/", Pleroma.Web.ActivityPub do scope "/", Pleroma.Web.ActivityPub do
pipe_through(:activitypub) pipe_through([:activitypub, :inbox_guard])
post("/inbox", ActivityPubController, :inbox) post("/inbox", ActivityPubController, :inbox)
post("/users/:nickname/inbox", ActivityPubController, :inbox) post("/users/:nickname/inbox", ActivityPubController, :inbox)
end end

View file

@ -0,0 +1,21 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.Cron.AppCleanupWorker do
@moduledoc """
Cleans up registered apps that were never associated with a user.
"""
use Oban.Worker, queue: "background"
alias Pleroma.Web.OAuth.App
@impl true
def perform(_job) do
App.remove_orphans()
end
@impl true
def timeout(_job), do: :timer.seconds(30)
end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.Federator alias Pleroma.Web.Federator
use Oban.Worker, queue: :federator_incoming, max_attempts: 5 use Oban.Worker, queue: :federator_incoming, max_attempts: 5, unique: [period: :infinity]
@impl true @impl true
@ -33,7 +33,7 @@ defmodule Pleroma.Workers.ReceiverWorker do
query_string: query_string query_string: query_string
} }
with {:ok, %User{} = _actor} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]), with {:ok, %User{}} <- User.get_or_fetch_by_ap_id(conn_data.params["actor"]),
{:ok, _public_key} <- Signature.refetch_public_key(conn_data), {:ok, _public_key} <- Signature.refetch_public_key(conn_data),
{:signature, true} <- {:signature, Signature.validate_signature(conn_data)}, {:signature, true} <- {:signature, Signature.validate_signature(conn_data)},
{:ok, res} <- Federator.perform(:incoming_ap_doc, params) do {:ok, res} <- Federator.perform(:incoming_ap_doc, params) do
@ -56,17 +56,29 @@ defmodule Pleroma.Workers.ReceiverWorker do
def timeout(_job), do: :timer.seconds(5) def timeout(_job), do: :timer.seconds(5)
defp process_errors({:error, {:error, _} = error}), do: process_errors(error)
defp process_errors(errors) do defp process_errors(errors) do
case errors do case errors do
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed} # User fetch failures
{:error, :already_present} -> {:cancel, :already_present}
{:error, {:validate_object, _} = reason} -> {:cancel, reason}
{:error, {:error, {:validate, {:error, _changeset} = reason}}} -> {:cancel, reason}
{:error, {:reject, _} = reason} -> {:cancel, reason}
{:signature, false} -> {:cancel, :invalid_signature}
{:error, "Object has been deleted"} = reason -> {:cancel, reason}
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
{:error, :not_found} = reason -> {:cancel, reason} {:error, :not_found} = reason -> {:cancel, reason}
{:error, :forbidden} = reason -> {:cancel, reason}
# Inactive user
{:error, {:user_active, false} = reason} -> {:cancel, reason}
# Validator will error and return a changeset error
# e.g., duplicate activities or if the object was deleted
{:error, {:validate, {:error, _changeset} = reason}} -> {:cancel, reason}
# Duplicate detection during Normalization
{:error, :already_present} -> {:cancel, :already_present}
# MRFs will return a reject
{:error, {:reject, _} = reason} -> {:cancel, reason}
# HTTP Sigs
{:signature, false} -> {:cancel, :invalid_signature}
# Origin / URL validation failed somewhere possibly due to spoofing
{:error, :origin_containment_failed} -> {:cancel, :origin_containment_failed}
# Unclear if this can be reached
{:error, {:side_effects, {:error, :no_object_actor}} = reason} -> {:cancel, reason}
# Catchall
{:error, _} = e -> e {:error, _} = e -> e
e -> {:error, e} e -> {:error, e}
end end

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Workers.RemoteFetcherWorker do defmodule Pleroma.Workers.RemoteFetcherWorker do
alias Pleroma.Object.Fetcher alias Pleroma.Object.Fetcher
use Oban.Worker, queue: :background use Oban.Worker, queue: :background, unique: [period: :infinity]
@impl true @impl true
def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do def perform(%Job{args: %{"op" => "fetch_remote", "id" => id} = args}) do

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.RichMediaWorker do
alias Pleroma.Web.RichMedia.Backfill alias Pleroma.Web.RichMedia.Backfill
alias Pleroma.Web.RichMedia.Card alias Pleroma.Web.RichMedia.Card
use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: 300] use Oban.Worker, queue: :background, max_attempts: 3, unique: [period: :infinity]
@impl true @impl true
def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do def perform(%Job{args: %{"op" => "expire", "url" => url} = _args}) do

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Workers.UserRefreshWorker do defmodule Pleroma.Workers.UserRefreshWorker do
use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: 300] use Oban.Worker, queue: :background, max_attempts: 1, unique: [period: :infinity]
alias Pleroma.User alias Pleroma.User

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Workers.WebPusherWorker do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.Web.Push.Impl alias Pleroma.Web.Push.Impl
use Oban.Worker, queue: :web_push use Oban.Worker, queue: :web_push, unique: [period: :infinity]
@impl true @impl true
def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do def perform(%Job{args: %{"op" => "web_push", "notification_id" => notification_id}}) do

View file

@ -5,7 +5,7 @@ defmodule Pleroma.Mixfile do
[ [
app: :pleroma, app: :pleroma,
version: version("2.7.0"), version: version("2.7.0"),
elixir: "~> 1.13", elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()), elixirc_paths: elixirc_paths(Mix.env()),
compilers: Mix.compilers(), compilers: Mix.compilers(),
elixirc_options: [warnings_as_errors: warnings_as_errors(), prune_code_paths: false], elixirc_options: [warnings_as_errors: warnings_as_errors(), prune_code_paths: false],
@ -153,7 +153,7 @@ defmodule Pleroma.Mixfile do
{:calendar, "~> 1.0"}, {:calendar, "~> 1.0"},
{:cachex, "~> 3.2"}, {:cachex, "~> 3.2"},
{:tesla, "~> 1.11"}, {:tesla, "~> 1.11"},
{:castore, "~> 0.1"}, {:castore, "~> 1.0"},
{:cowlib, "~> 2.9", override: true}, {:cowlib, "~> 2.9", override: true},
{:gun, "~> 2.0.0-rc.1", override: true}, {:gun, "~> 2.0.0-rc.1", override: true},
{:finch, "~> 0.15"}, {:finch, "~> 0.15"},
@ -169,6 +169,8 @@ defmodule Pleroma.Mixfile do
{:swoosh, "~> 1.16.9"}, {:swoosh, "~> 1.16.9"},
{:phoenix_swoosh, "~> 1.1"}, {:phoenix_swoosh, "~> 1.1"},
{:gen_smtp, "~> 0.13"}, {:gen_smtp, "~> 0.13"},
{:mua, "~> 0.2.0"},
{:mail, "~> 0.3.0"},
{:ex_syslogger, "~> 1.4"}, {:ex_syslogger, "~> 1.4"},
{:floki, "~> 0.35"}, {:floki, "~> 0.35"},
{:timex, "~> 3.6"}, {:timex, "~> 3.6"},
@ -203,6 +205,7 @@ defmodule Pleroma.Mixfile do
{:websock_adapter, "~> 0.5.6"}, {:websock_adapter, "~> 0.5.6"},
{:oban_live_dashboard, "~> 0.1.1"}, {:oban_live_dashboard, "~> 0.1.1"},
{:multipart, "~> 0.4.0", optional: true}, {:multipart, "~> 0.4.0", optional: true},
{:argon2_elixir, "~> 4.0"},
## dev & test ## dev & test
{:phoenix_live_reload, "~> 1.3.3", only: :dev}, {:phoenix_live_reload, "~> 1.3.3", only: :dev},

View file

@ -1,5 +1,6 @@
%{ %{
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"}, "accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
"argon2_elixir": {:hex, :argon2_elixir, "4.0.0", "7f6cd2e4a93a37f61d58a367d82f830ad9527082ff3c820b8197a8a736648941", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "f9da27cf060c9ea61b1bd47837a28d7e48a8f6fa13a745e252556c14f9132c7f"},
"bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"}, "bandit": {:hex, :bandit, "1.5.5", "df28f1c41f745401fe9e85a6882033f5f3442ab6d30c8a2948554062a4ab56e0", [:mix], [{:hpax, "~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}, {:thousand_island, "~> 1.0", [hex: :thousand_island, repo: "hexpm", optional: false]}, {:websock, "~> 0.5", [hex: :websock, repo: "hexpm", optional: false]}], "hexpm", "f21579a29ea4bc08440343b2b5f16f7cddf2fea5725d31b72cf973ec729079e1"},
"base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"}, "base62": {:hex, :base62, "1.2.2", "85c6627eb609317b70f555294045895ffaaeb1758666ab9ef9ca38865b11e629", [:mix], [{:custom_base, "~> 0.2.1", [hex: :custom_base, repo: "hexpm", optional: false]}], "hexpm", "d41336bda8eaa5be197f1e4592400513ee60518e5b9f4dcf38f4b4dae6f377bb"},
"bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"}, "bbcode_pleroma": {:hex, :bbcode_pleroma, "0.2.0", "d36f5bca6e2f62261c45be30fa9b92725c0655ad45c99025cb1c3e28e25803ef", [:mix], [{:nimble_parsec, "~> 0.5", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "19851074419a5fedb4ef49e1f01b30df504bb5dbb6d6adfc135238063bebd1c3"},
@ -10,7 +11,7 @@
"cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"}, "cachex": {:hex, :cachex, "3.6.0", "14a1bfbeee060dd9bec25a5b6f4e4691e3670ebda28c8ba2884b12fe30b36bf8", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:jumper, "~> 1.0", [hex: :jumper, repo: "hexpm", optional: false]}, {:sleeplocks, "~> 1.1", [hex: :sleeplocks, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm", "ebf24e373883bc8e0c8d894a63bbe102ae13d918f790121f5cfe6e485cc8e2e2"},
"calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"}, "calendar": {:hex, :calendar, "1.0.0", "f52073a708528482ec33d0a171954ca610fe2bd28f1e871f247dc7f1565fa807", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603 or ~> 1.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "990e9581920c82912a5ee50e62ff5ef96da6b15949a2ee4734f935fdef0f0a6f"},
"captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]}, "captcha": {:git, "https://git.pleroma.social/pleroma/elixir-libraries/elixir-captcha.git", "6630c42aaaab124e697b4e513190c89d8b64e410", [ref: "6630c42aaaab124e697b4e513190c89d8b64e410"]},
"castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"},
"cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"}, "cc_precompiler": {:hex, :cc_precompiler, "0.1.9", "e8d3364f310da6ce6463c3dd20cf90ae7bbecbf6c5203b98bf9b48035592649b", [:mix], [{:elixir_make, "~> 0.7", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "9dcab3d0f3038621f1601f13539e7a9ee99843862e66ad62827b0c42b2f58a54"},
"certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
@ -22,7 +23,7 @@
"cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"}, "cowboy": {:hex, :cowboy, "2.12.0", "f276d521a1ff88b2b9b4c54d0e753da6c66dd7be6c9fca3d9418b561828a3731", [:make, :rebar3], [{:cowlib, "2.13.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "8a7abe6d183372ceb21caa2709bec928ab2b72e18a3911aa1771639bef82651e"},
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"},
"cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"}, "cowlib": {:hex, :cowlib, "2.13.0", "db8f7505d8332d98ef50a3ef34b34c1afddec7506e4ee4dd4a3a266285d282ca", [:make, :rebar3], [], "hexpm", "e1e1284dc3fc030a64b1ad0d8382ae7e99da46c3246b815318a4b848873800a4"},
"credo": {:hex, :credo, "1.7.3", "05bb11eaf2f2b8db370ecaa6a6bda2ec49b2acd5e0418bc106b73b07128c0436", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "35ea675a094c934c22fb1dca3696f3c31f2728ae6ef5a53b5d648c11180a4535"}, "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"},
"crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crontab": {:hex, :crontab, "1.1.8", "2ce0e74777dfcadb28a1debbea707e58b879e6aa0ffbf9c9bb540887bce43617", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"},
"custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"}, "custom_base": {:hex, :custom_base, "0.2.1", "4a832a42ea0552299d81652aa0b1f775d462175293e99dfbe4d7dbaab785a706", [:mix], [], "hexpm", "8df019facc5ec9603e94f7270f1ac73ddf339f56ade76a721eaa57c1493ba463"},
"db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"},
@ -71,6 +72,7 @@
"jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"}, "jumper": {:hex, :jumper, "1.0.2", "68cdcd84472a00ac596b4e6459a41b3062d4427cbd4f1e8c8793c5b54f1406a7", [:mix], [], "hexpm", "9b7782409021e01ab3c08270e26f36eb62976a38c1aa64b2eaf6348422f165e1"},
"linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"}, "linkify": {:hex, :linkify, "0.5.3", "5f8143d8f61f5ff08d3aeeff47ef6509492b4948d8f08007fbf66e4d2246a7f2", [:mix], [], "hexpm", "3ef35a1377d47c25506e07c1c005ea9d38d700699d92ee92825f024434258177"},
"logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"}, "logger_backends": {:hex, :logger_backends, "1.0.0", "09c4fad6202e08cb0fbd37f328282f16539aca380f512523ce9472b28edc6bdf", [:mix], [], "hexpm", "1faceb3e7ec3ef66a8f5746c5afd020e63996df6fd4eb8cdb789e5665ae6c9ce"},
"mail": {:hex, :mail, "0.3.1", "cb0a14e4ed8904e4e5a08214e686ccf6f9099346885db17d8c309381f865cc5c", [:mix], [], "hexpm", "1db701e89865c1d5fa296b2b57b1cd587587cca8d8a1a22892b35ef5a8e352a6"},
"majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"}, "majic": {:hex, :majic, "1.0.0", "37e50648db5f5c2ff0c9fb46454d034d11596c03683807b9fb3850676ffdaab3", [:make, :mix], [{:elixir_make, "~> 0.6.1", [hex: :elixir_make, repo: "hexpm", optional: false]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "7905858f76650d49695f14ea55cd9aaaee0c6654fa391671d4cf305c275a0a9e"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"}, "makeup_elixir": {:hex, :makeup_elixir, "0.14.1", "4f0e96847c63c17841d42c08107405a005a2680eb9c7ccadfd757bd31dabccfb", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "f2438b1a80eaec9ede832b5c41cd4f373b38fd7aa33e3b22d9db79e640cbde11"},
@ -84,12 +86,13 @@
"mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"}, "mock": {:hex, :mock, "0.3.8", "7046a306b71db2488ef54395eeb74df0a7f335a7caca4a3d3875d1fc81c884dd", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "7fa82364c97617d79bb7d15571193fc0c4fe5afd0c932cef09426b3ee6fe2022"},
"mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"}, "mogrify": {:hex, :mogrify, "0.9.3", "238c782f00271dace01369ad35ae2e9dd020feee3443b9299ea5ea6bed559841", [:mix], [], "hexpm", "0189b1e1de27455f2b9ae8cf88239cefd23d38de9276eb5add7159aea51731e6"},
"mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"},
"mua": {:hex, :mua, "0.2.3", "46b29b7b2bb14105c0b7be9526f7c452df17a7841b30b69871c024a822ff551c", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}], "hexpm", "7fe861a87fcc06a980d3941bbcb2634e5f0f30fd6ad15ef6c0423ff9dc7e46de"},
"multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"}, "multipart": {:hex, :multipart, "0.4.0", "634880a2148d4555d050963373d0e3bbb44a55b2badd87fa8623166172e9cda0", [:mix], [{:mime, "~> 1.2 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}], "hexpm", "3c5604bc2fb17b3137e5d2abdf5dacc2647e60c5cc6634b102cf1aef75a06f0a"},
"nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"},
"nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"}, "nimble_parsec": {:hex, :nimble_parsec, "0.6.0", "32111b3bf39137144abd7ba1cce0914533b2d16ef35e8abc5ec8be6122944263", [:mix], [], "hexpm", "27eac315a94909d4dc68bc07a4a83e06c8379237c5ea528a9acff4ca1c873c52"},
"nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"}, "nimble_pool": {:hex, :nimble_pool, "0.2.6", "91f2f4c357da4c4a0a548286c84a3a28004f68f05609b4534526871a22053cde", [:mix], [], "hexpm", "1c715055095d3f2705c4e236c18b618420a35490da94149ff8b580a2144f653f"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "2.18.2", "583e78965ee15263ac968e38c983bad169ae55eadaa8e1e39912562badff93ba", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9dd25fd35883a91ed995e9fe516e479344d3a8623dfe2b8c3fc8e5be0228ec3a"}, "oban": {:hex, :oban, "2.18.3", "1608c04f8856c108555c379f2f56bc0759149d35fa9d3b825cb8a6769f8ae926", [:mix], [{:ecto_sql, "~> 3.10", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:ecto_sqlite3, "~> 0.9", [hex: :ecto_sqlite3, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.16", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "36ca6ca84ef6518f9c2c759ea88efd438a3c81d667ba23b02b062a0aa785475e"},
"oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"}, "oban_live_dashboard": {:hex, :oban_live_dashboard, "0.1.1", "8aa4ceaf381c818f7d5c8185cc59942b8ac82ef0cf559881aacf8d3f8ac7bdd3", [:mix], [{:oban, "~> 2.15", [hex: :oban, repo: "hexpm", optional: false]}, {:phoenix_live_dashboard, "~> 0.7", [hex: :phoenix_live_dashboard, repo: "hexpm", optional: false]}], "hexpm", "16dc4ce9c9a95aa2e655e35ed4e675652994a8def61731a18af85e230e1caa63"},
"octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"}, "octo_fetch": {:hex, :octo_fetch, "0.4.0", "074b5ecbc08be10b05b27e9db08bc20a3060142769436242702931c418695b19", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "cf8be6f40cd519d7000bb4e84adcf661c32e59369ca2827c4e20042eda7a7fc6"},
"open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"}, "open_api_spex": {:hex, :open_api_spex, "3.18.2", "8c855e83bfe8bf81603d919d6e892541eafece3720f34d1700b58024dadde247", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.0 or ~> 4.0 or ~> 5.0", [hex: :poison, repo: "hexpm", optional: true]}, {:ymlr, "~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :ymlr, repo: "hexpm", optional: true]}], "hexpm", "aa3e6dcfc0ad6a02596b2172662da21c9dd848dac145ea9e603f54e3d81b8d2b"},

View file

@ -0,0 +1,21 @@
defmodule Pleroma.Repo.Migrations.AssignAppUser do
use Ecto.Migration
alias Pleroma.Repo
alias Pleroma.Web.OAuth.App
alias Pleroma.Web.OAuth.Token
def up do
Repo.all(Token)
|> Enum.group_by(fn x -> Map.get(x, :app_id) end)
|> Enum.each(fn {_app_id, tokens} ->
token =
Enum.filter(tokens, fn x -> not is_nil(x.user_id) end)
|> List.first()
App.maybe_update_owner(token)
end)
end
def down, do: :ok
end

View file

@ -22,7 +22,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"u-url", "u-url",
"mention", "mention",
"u-url mention", "u-url mention",
"mention u-url" "mention u-url",
"mention hashtag"
]) ])
Meta.allow_tag_with_this_attribute_values(:a, "rel", [ Meta.allow_tag_with_this_attribute_values(:a, "rel", [

View file

@ -23,7 +23,8 @@ defmodule Pleroma.HTML.Scrubber.TwitterText do
"u-url", "u-url",
"mention", "mention",
"u-url mention", "u-url mention",
"mention u-url" "mention u-url",
"mention hashtag"
]) ])
Meta.allow_tag_with_this_attribute_values(:a, "rel", [ Meta.allow_tag_with_this_attribute_values(:a, "rel", [

View file

@ -1,117 +0,0 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"Curve25519Key": "toot:Curve25519Key",
"Device": "toot:Device",
"Ed25519Key": "toot:Ed25519Key",
"Ed25519Signature": "toot:Ed25519Signature",
"EncryptedMessage": "toot:EncryptedMessage",
"PropertyValue": "schema:PropertyValue",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"cipherText": "toot:cipherText",
"claim": {
"@id": "toot:claim",
"@type": "@id"
},
"deviceId": "toot:deviceId",
"devices": {
"@id": "toot:devices",
"@type": "@id"
},
"discoverable": "toot:discoverable",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"fingerprintKey": {
"@id": "toot:fingerprintKey",
"@type": "@id"
},
"focalPoint": {
"@container": "@list",
"@id": "toot:focalPoint"
},
"identityKey": {
"@id": "toot:identityKey",
"@type": "@id"
},
"indexable": "toot:indexable",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"memorial": "toot:memorial",
"messageFranking": "toot:messageFranking",
"messageType": "toot:messageType",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"publicKeyBase64": "toot:publicKeyBase64",
"schema": "http://schema.org#",
"suspended": "toot:suspended",
"toot": "http://joinmastodon.org/ns#",
"value": "schema:value"
}
],
"attachment": [
{
"name": "Website",
"type": "PropertyValue",
"value": "<a href=\"https://bastianallgeier.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">bastianallgeier.com</span><span class=\"invisible\"></span></a>"
},
{
"name": "Project",
"type": "PropertyValue",
"value": "<a href=\"https://getkirby.com\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">getkirby.com</span><span class=\"invisible\"></span></a>"
},
{
"name": "Github",
"type": "PropertyValue",
"value": "<a href=\"https://github.com/bastianallgeier\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\" translate=\"no\"><span class=\"invisible\">https://</span><span class=\"\">github.com/bastianallgeier</span><span class=\"invisible\"></span></a>"
}
],
"devices": "https://mastodon.social/users/bastianallgeier/collections/devices",
"discoverable": true,
"endpoints": {
"sharedInbox": "https://mastodon.social/inbox"
},
"featured": "https://mastodon.social/users/bastianallgeier/collections/featured",
"featuredTags": "https://mastodon.social/users/bastianallgeier/collections/tags",
"followers": "https://mastodon.social/users/bastianallgeier/followers",
"following": "https://mastodon.social/users/bastianallgeier/following",
"icon": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://files.mastodon.social/accounts/avatars/000/007/393/original/0180a20079617c71.jpg"
},
"id": "https://mastodon.social/users/bastianallgeier",
"image": {
"mediaType": "image/jpeg",
"type": "Image",
"url": "https://files.mastodon.social/accounts/headers/000/007/393/original/13d644ab46d50478.jpeg"
},
"inbox": "https://mastodon.social/users/bastianallgeier/inbox",
"indexable": false,
"manuallyApprovesFollowers": false,
"memorial": false,
"name": "Bastian Allgeier",
"outbox": "https://mastodon.social/users/bastianallgeier/outbox",
"preferredUsername": "bastianallgeier",
"publicKey": {
"id": "https://mastodon.social/users/bastianallgeier#main-key",
"owner": "https://mastodon.social/users/bastianallgeier",
"publicKeyPem": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3fz+hpgVztO9z6HUhyzv\nwP++ERBBoIwSLKf1TyIM8bvzGFm2YXaO5uxu1HvumYFTYc3ACr3q4j8VUb7NMxkQ\nlzu4QwPjOFJ43O+fY+HSPORXEDW5fXDGC5DGpox4+i08LxRmx7L6YPRUSUuPN8nI\nWyq1Qsq1zOQrNY/rohMXkBdSXxqC3yIRqvtLt4otCgay/5tMogJWkkS6ZKyFhb9z\nwVVy1fsbV10c9C+SHy4NH26CKaTtpTYLRBMjhTCS8bX8iDSjGIf2aZgYs1ir7gEz\n9wf5CvLiENmVWGwm64t6KSEAkA4NJ1hzgHUZPCjPHZE2SmhO/oHaxokTzqtbbENJ\n1QIDAQAB\n-----END PUBLIC KEY-----\n"
},
"published": "2016-11-01T00:00:00Z",
"summary": "<p>Designer &amp; developer. Creator of Kirby CMS</p>",
"tag": [],
"type": "Person",
"url": "https://mastodon.social/@bastianallgeier"
}

View file

@ -1,62 +1,109 @@
{ {
"@context": [ "@context": [
"https://www.w3.org/ns/activitystreams", "https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{ {
"claim": {
"@id": "toot:claim",
"@type": "@id"
},
"memorial": "toot:memorial",
"atomUri": "ostatus:atomUri", "atomUri": "ostatus:atomUri",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"blurhash": "toot:blurhash", "blurhash": "toot:blurhash",
"conversation": "ostatus:conversation", "ostatus": "http://ostatus.org#",
"discoverable": "toot:discoverable",
"focalPoint": { "focalPoint": {
"@container": "@list", "@container": "@list",
"@id": "toot:focalPoint" "@id": "toot:focalPoint"
}, },
"inReplyToAtomUri": "ostatus:inReplyToAtomUri", "votersCount": "toot:votersCount",
"ostatus": "http://ostatus.org#", "Hashtag": "as:Hashtag",
"Emoji": "toot:Emoji",
"alsoKnownAs": {
"@id": "as:alsoKnownAs",
"@type": "@id"
},
"sensitive": "as:sensitive", "sensitive": "as:sensitive",
"movedTo": {
"@id": "as:movedTo",
"@type": "@id"
},
"inReplyToAtomUri": "ostatus:inReplyToAtomUri",
"conversation": "ostatus:conversation",
"Device": "toot:Device",
"schema": "http://schema.org#",
"toot": "http://joinmastodon.org/ns#", "toot": "http://joinmastodon.org/ns#",
"votersCount": "toot:votersCount" "cipherText": "toot:cipherText",
"suspended": "toot:suspended",
"messageType": "toot:messageType",
"featuredTags": {
"@id": "toot:featuredTags",
"@type": "@id"
},
"Curve25519Key": "toot:Curve25519Key",
"deviceId": "toot:deviceId",
"Ed25519Signature": "toot:Ed25519Signature",
"featured": {
"@id": "toot:featured",
"@type": "@id"
},
"devices": {
"@id": "toot:devices",
"@type": "@id"
},
"value": "schema:value",
"PropertyValue": "schema:PropertyValue",
"messageFranking": "toot:messageFranking",
"publicKeyBase64": "toot:publicKeyBase64",
"identityKey": {
"@id": "toot:identityKey",
"@type": "@id"
},
"Ed25519Key": "toot:Ed25519Key",
"indexable": "toot:indexable",
"EncryptedMessage": "toot:EncryptedMessage",
"fingerprintKey": {
"@id": "toot:fingerprintKey",
"@type": "@id"
}
} }
], ],
"atomUri": "https://chaos.social/users/distantnative/statuses/109336635639931467", "actor": "https://phpc.social/users/denniskoch",
"attachment": [ "cc": [],
{ "id": "https://phpc.social/users/denniskoch/statuses/112847382711461301/activity",
"blurhash": "UAK1zS00OXIUxuMxIUM{?b-:-;W:Di?b%2M{",
"height": 960,
"mediaType": "image/jpeg",
"name": null,
"type": "Document",
"url": "https://assets.chaos.social/media_attachments/files/109/336/634/286/114/657/original/2e6122063d8bfb26.jpeg",
"width": 346
}
],
"attributedTo": "https://chaos.social/users/distantnative",
"cc": [
"https://chaos.social/users/distantnative/followers"
],
"content": "<p>Favorite piece of anthropology meta discourse.</p>",
"contentMap": {
"en": "<p>Favorite piece of anthropology meta discourse.</p>"
},
"conversation": "tag:chaos.social,2022-11-13:objectId=71843781:objectType=Conversation",
"id": "https://chaos.social/users/distantnative/statuses/109336635639931467",
"inReplyTo": null, "inReplyTo": null,
"inReplyToAtomUri": null, "inReplyToAtomUri": null,
"published": "2022-11-13T13:04:20Z", "object": {
"replies": { "atomUri": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"first": { "attachment": [],
"items": [], "attributedTo": "https://phpc.social/users/denniskoch",
"next": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies?only_other_accounts=true&page=true", "cc": [],
"partOf": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies", "content": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>",
"type": "CollectionPage" "contentMap": {
"en": "<p><span class=\"h-card\" translate=\"no\"><a href=\"https://mastodon.social/@bastianallgeier\" class=\"u-url mention\">@<span>bastianallgeier</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://chaos.social/@distantnative\" class=\"u-url mention\">@<span>distantnative</span></a></span> <span class=\"h-card\" translate=\"no\"><a href=\"https://fosstodon.org/@kev\" class=\"u-url mention\">@<span>kev</span></a></span> Another main argument: Discord is popular. Many people have an account, so you can just join an server quickly. Also you know the app and how to get around.</p>"
}, },
"id": "https://chaos.social/users/distantnative/statuses/109336635639931467/replies", "conversation": "tag:mastodon.social,2024-07-25:objectId=760068442:objectType=Conversation",
"type": "Collection" "id": "https://phpc.social/users/denniskoch/statuses/112847382711461301",
"published": "2024-07-25T13:33:29Z",
"replies": null,
"sensitive": false,
"tag": [],
"to": [
"https://www.w3.org/ns/activitystreams#Public"
],
"type": "Note",
"url": "https://phpc.social/@denniskoch/112847382711461301"
},
"published": "2024-07-25T13:33:29Z",
"signature": {
"created": "2024-07-25T13:33:29Z",
"creator": "https://phpc.social/users/denniskoch#main-key",
"signatureValue": "slz9BKJzd2n1S44wdXGOU+bV/wsskdgAaUpwxj8R16mYOL8+DTpE6VnfSKoZGsBBJT8uG5gnVfVEz1YsTUYtymeUgLMh7cvd8VnJnZPS+oixbmBRVky/Myf91TEgQQE7G4vDmTdB4ii54hZrHcOOYYf5FKPNRSkMXboKA6LMqNtekhbI+JTUJYIB02WBBK6PUyo15f6B1RJ6HGWVgud9NE0y1EZXfrkqUt682p8/9D49ORf7AwjXUJibKic2RbPvhEBj70qUGfBm4vvgdWhSUn1IG46xh+U0+NrTSUED82j1ZVOeua/2k/igkGs8cSBkY35quXTkPz6gbqCCH66CuA==",
"type": "RsaSignature2017"
}, },
"sensitive": false,
"summary": null,
"tag": [],
"to": [ "to": [
"https://www.w3.org/ns/activitystreams#Public" "https://www.w3.org/ns/activitystreams#Public"
], ],
"type": "Note", "type": "Create"
"url": "https://chaos.social/@distantnative/109336635639931467"
} }

View file

@ -41,6 +41,10 @@ defmodule Pleroma.HTMLTest do
<span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span> <span class="h-card"><a class="u-url mention animate-spin">@<span>foo</span></a></span>
""" """
@mention_hashtags_sample """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
describe "StripTags scrubber" do describe "StripTags scrubber" do
test "works as expected" do test "works as expected" do
expected = """ expected = """
@ -126,6 +130,15 @@ defmodule Pleroma.HTMLTest do
Pleroma.HTML.Scrubber.TwitterText Pleroma.HTML.Scrubber.TwitterText
) )
end end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end end
describe "default scrubber" do describe "default scrubber" do
@ -189,6 +202,15 @@ defmodule Pleroma.HTMLTest do
Pleroma.HTML.Scrubber.Default Pleroma.HTML.Scrubber.Default
) )
end end
test "does allow mention hashtags" do
expected = """
<a href="https://mastodon.example/tags/linux" class="mention hashtag" rel="tag">#<span>linux</span></a>
"""
assert expected ==
HTML.filter_tags(@mention_hashtags_sample, Pleroma.HTML.Scrubber.Default)
end
end end
describe "extract_first_external_url_from_object" do describe "extract_first_external_url_from_object" do

View file

@ -657,7 +657,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
end end
test "without valid signature, " <> test "without valid signature, " <>
"it only accepts Create activities and requires enabled federation", "it accepts Create activities and requires enabled federation",
%{conn: conn} do %{conn: conn} do
data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!() data = File.read!("test/fixtures/mastodon-post-activity.json") |> Jason.decode!()
non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!() non_create_data = File.read!("test/fixtures/mastodon-announce.json") |> Jason.decode!()
@ -684,6 +684,54 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
|> json_response(400) |> json_response(400)
end end
# When activity is delivered to the inbox and we cannot immediately verify signature
# we capture all the params and process it later in the Oban job.
# Once we begin processing it through Oban we risk fetching the actor to validate the
# activity which just leads to inserting a new user to process a Delete not relevant to us.
test "Activities of certain types from an unknown actor are discarded", %{conn: conn} do
example_bad_types =
Pleroma.Constants.activity_types() --
Pleroma.Constants.allowed_activity_types_from_strangers()
Enum.each(example_bad_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody"
}
|> Jason.encode!()
conn
|> assign(:valid_signature, false)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "Unknown activity types are discarded", %{conn: conn} do
unknown_types = ["Poke", "Read", "Dazzle"]
Enum.each(unknown_types, fn bad_type ->
params =
%{
"type" => bad_type,
"actor" => "https://unknown.mastodon.instance/users/somebody"
}
|> Jason.encode!()
conn
|> assign(:valid_signature, true)
|> put_req_header("content-type", "application/activity+json")
|> post("/inbox", params)
|> json_response(400)
assert all_enqueued() == []
end)
end
test "accepts Add/Remove activities", %{conn: conn} do test "accepts Add/Remove activities", %{conn: conn} do
object_id = "c61d6733-e256-4fe1-ab13-1e369789423f" object_id = "c61d6733-e256-4fe1-ab13-1e369789423f"

View file

@ -232,12 +232,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert user.avatar == %{ assert user.avatar == %{
"type" => "Image", "type" => "Image",
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}] "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
"name" => "profile picture"
} }
assert user.banner == %{ assert user.banner == %{
"type" => "Image", "type" => "Image",
"url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}] "url" => [%{"href" => "https://jk.nipponalba.scot/images/profile.jpg"}],
"name" => "profile picture"
} }
end end
@ -432,6 +434,35 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
assert user.birthday == ~D[2001-02-12] assert user.birthday == ~D[2001-02-12]
end end
test "fetches avatar description" do
user_id = "https://example.com/users/marcin"
user_data =
"test/fixtures/users_mock/user.json"
|> File.read!()
|> String.replace("{{nickname}}", "marcin")
|> Jason.decode!()
|> Map.delete("featured")
|> Map.update("icon", %{}, fn image -> Map.put(image, "name", "image description") end)
|> Jason.encode!()
Tesla.Mock.mock(fn
%{
method: :get,
url: ^user_id
} ->
%Tesla.Env{
status: 200,
body: user_data,
headers: [{"content-type", "application/activity+json"}]
}
end)
{:ok, user} = ActivityPub.make_user_from_ap_id(user_id)
assert user.avatar["name"] == "image description"
end
end end
test "it fetches the appropriate tag-restricted posts" do test "it fetches the appropriate tag-restricted posts" do

View file

@ -252,6 +252,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message() remote_message = build_remote_message()
assert SimplePolicy.filter(remote_message) == {:ok, remote_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
assert SimplePolicy.id_filter(remote_message["actor"])
end end
test "activity has a matching host" do test "activity has a matching host" do
@ -260,6 +261,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message() remote_message = build_remote_message()
assert {:reject, _} = SimplePolicy.filter(remote_message) assert {:reject, _} = SimplePolicy.filter(remote_message)
refute SimplePolicy.id_filter(remote_message["actor"])
end end
test "activity matches with wildcard domain" do test "activity matches with wildcard domain" do
@ -268,6 +270,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_message = build_remote_message() remote_message = build_remote_message()
assert {:reject, _} = SimplePolicy.filter(remote_message) assert {:reject, _} = SimplePolicy.filter(remote_message)
refute SimplePolicy.id_filter(remote_message["actor"])
end end
test "actor has a matching host" do test "actor has a matching host" do
@ -276,6 +279,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_user = build_remote_user() remote_user = build_remote_user()
assert {:reject, _} = SimplePolicy.filter(remote_user) assert {:reject, _} = SimplePolicy.filter(remote_user)
refute SimplePolicy.id_filter(remote_user["id"])
end end
test "reject Announce when object would be rejected" do test "reject Announce when object would be rejected" do
@ -288,6 +292,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
} }
assert {:reject, _} = SimplePolicy.filter(announce) assert {:reject, _} = SimplePolicy.filter(announce)
# Note: Non-Applicable for id_filter/1
end end
test "reject by URI object" do test "reject by URI object" do
@ -300,6 +305,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
} }
assert {:reject, _} = SimplePolicy.filter(announce) assert {:reject, _} = SimplePolicy.filter(announce)
# Note: Non-Applicable for id_filter/1
end end
end end
@ -370,6 +376,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
assert SimplePolicy.id_filter(local_message["actor"])
assert SimplePolicy.id_filter(remote_message["actor"])
end end
test "is not empty but activity doesn't have a matching host" do test "is not empty but activity doesn't have a matching host" do
@ -380,6 +388,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert {:reject, _} = SimplePolicy.filter(remote_message) assert {:reject, _} = SimplePolicy.filter(remote_message)
assert SimplePolicy.id_filter(local_message["actor"])
refute SimplePolicy.id_filter(remote_message["actor"])
end end
test "activity has a matching host" do test "activity has a matching host" do
@ -390,6 +400,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
assert SimplePolicy.id_filter(local_message["actor"])
assert SimplePolicy.id_filter(remote_message["actor"])
end end
test "activity matches with wildcard domain" do test "activity matches with wildcard domain" do
@ -400,6 +412,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
assert SimplePolicy.filter(local_message) == {:ok, local_message} assert SimplePolicy.filter(local_message) == {:ok, local_message}
assert SimplePolicy.filter(remote_message) == {:ok, remote_message} assert SimplePolicy.filter(remote_message) == {:ok, remote_message}
assert SimplePolicy.id_filter(local_message["actor"])
assert SimplePolicy.id_filter(remote_message["actor"])
end end
test "actor has a matching host" do test "actor has a matching host" do
@ -408,6 +422,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicyTest do
remote_user = build_remote_user() remote_user = build_remote_user()
assert SimplePolicy.filter(remote_user) == {:ok, remote_user} assert SimplePolicy.filter(remote_user) == {:ok, remote_user}
assert SimplePolicy.id_filter(remote_user["id"])
end end
end end

View file

@ -68,6 +68,23 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
result = UserView.render("user.json", %{user: user}) result = UserView.render("user.json", %{user: user})
assert result["icon"]["url"] == "https://someurl" assert result["icon"]["url"] == "https://someurl"
assert result["image"]["url"] == "https://somebanner" assert result["image"]["url"] == "https://somebanner"
refute result["icon"]["name"]
refute result["image"]["name"]
end
test "Avatar has a description if the user set one" do
user =
insert(:user,
avatar: %{
"url" => [%{"href" => "https://someurl"}],
"name" => "a drawing of pleroma-tan using pleroma groups"
}
)
result = UserView.render("user.json", %{user: user})
assert result["icon"]["name"] == "a drawing of pleroma-tan using pleroma groups"
end end
test "renders an invisible user with the invisible property set to true" do test "renders an invisible user with the invisible property set to true" do

View file

@ -430,6 +430,75 @@ defmodule Pleroma.Web.MastodonAPI.UpdateCredentialsTest do
assert :ok == File.rm(Path.absname("test/tmp/large_binary.data")) assert :ok == File.rm(Path.absname("test/tmp/large_binary.data"))
end end
test "adds avatar description with a new avatar", %{user: user, conn: conn} do
new_avatar = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
res =
patch(conn, "/api/v1/accounts/update_credentials", %{
"avatar" => new_avatar,
"avatar_description" => "me and pleroma tan"
})
assert json_response_and_validate_schema(res, 200)
user = User.get_by_id(user.id)
assert user.avatar["name"] == "me and pleroma tan"
end
test "adds avatar description to existing avatar", %{user: user, conn: conn} do
new_avatar = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert user.avatar == %{}
conn
|> patch("/api/v1/accounts/update_credentials", %{"avatar" => new_avatar})
assert conn
|> assign(:user, User.get_by_id(user.id))
|> patch("/api/v1/accounts/update_credentials", %{
"avatar_description" => "me and pleroma tan"
})
|> json_response_and_validate_schema(200)
user = User.get_by_id(user.id)
assert user.avatar["name"] == "me and pleroma tan"
end
test "limit", %{user: user, conn: conn} do
new_header = %Plug.Upload{
content_type: "image/jpeg",
path: Path.absname("test/fixtures/image.jpg"),
filename: "an_image.jpg"
}
assert user.banner == %{}
conn
|> patch("/api/v1/accounts/update_credentials", %{"header" => new_header})
description_limit = Config.get([:instance, :description_limit], 100)
description = String.duplicate(".", description_limit + 1)
conn =
conn
|> assign(:user, User.get_by_id(user.id))
|> patch("/api/v1/accounts/update_credentials", %{
"header_description" => description
})
assert %{"error" => "Banner description is too long"} =
json_response_and_validate_schema(conn, 413)
end
test "Strip / from upload files", %{user: user, conn: conn} do test "Strip / from upload files", %{user: user, conn: conn} do
new_image = %Plug.Upload{ new_image = %Plug.Upload{
content_type: "image/jpeg", content_type: "image/jpeg",

View file

@ -96,7 +96,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false, hide_follows_count: false,
relationship: %{}, relationship: %{},
skip_thread_containment: false, skip_thread_containment: false,
accepts_chat_messages: nil accepts_chat_messages: nil,
avatar_description: "",
header_description: ""
} }
} }
@ -340,7 +342,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
hide_follows_count: false, hide_follows_count: false,
relationship: %{}, relationship: %{},
skip_thread_containment: false, skip_thread_containment: false,
accepts_chat_messages: nil accepts_chat_messages: nil,
avatar_description: "",
header_description: ""
} }
} }
@ -456,6 +460,45 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
test_relationship_rendering(user, other_user, expected) test_relationship_rendering(user, other_user, expected)
end end
test "relationship does not indicate following if a FollowingRelationship is missing" do
user = insert(:user)
other_user = insert(:user, local: false)
# Create a follow relationship with the real Follow Activity and Accept it
assert {:ok, _, _, _} = CommonAPI.follow(other_user, user)
assert {:ok, _} = CommonAPI.accept_follow_request(user, other_user)
assert %{data: %{"state" => "accept"}} =
Pleroma.Web.ActivityPub.Utils.fetch_latest_follow(user, other_user)
# Fetch the relationship and forcibly delete it to simulate
# a Follow Accept that did not complete processing
%{following_relationships: [relationship]} =
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
assert {:ok, _} = Pleroma.Repo.delete(relationship)
assert %{following_relationships: [], user_relationships: []} ==
Pleroma.UserRelationship.view_relationships_option(user, [other_user])
expected =
Map.merge(
@blank_response,
%{
following: false,
followed_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
notifying: false,
showing_reblogs: true,
id: to_string(other_user.id)
}
)
test_relationship_rendering(user, other_user, expected)
end
test "represent a relationship for the blocking and blocked user" do test "represent a relationship for the blocking and blocked user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -56,6 +56,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "pleroma:chat_mention", type: "pleroma:chat_mention",
account: AccountView.render("show.json", %{user: user, for: recipient}), account: AccountView.render("show.json", %{user: user, for: recipient}),
@ -75,6 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "mention", type: "mention",
account: account:
@ -99,6 +101,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "favourite", type: "favourite",
account: AccountView.render("show.json", %{user: another_user, for: user}), account: AccountView.render("show.json", %{user: another_user, for: user}),
@ -119,6 +122,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "reblog", type: "reblog",
account: AccountView.render("show.json", %{user: another_user, for: user}), account: AccountView.render("show.json", %{user: another_user, for: user}),
@ -137,6 +141,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "follow", type: "follow",
account: AccountView.render("show.json", %{user: follower, for: followed}), account: AccountView.render("show.json", %{user: follower, for: followed}),
@ -165,6 +170,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "move", type: "move",
account: AccountView.render("show.json", %{user: old_user, for: follower}), account: AccountView.render("show.json", %{user: old_user, for: follower}),
@ -190,6 +196,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "pleroma:emoji_reaction", type: "pleroma:emoji_reaction",
emoji: "☕", emoji: "☕",
@ -229,6 +236,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "pleroma:emoji_reaction", type: "pleroma:emoji_reaction",
emoji: ":dinosaur:", emoji: ":dinosaur:",
@ -248,6 +256,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "poll", type: "poll",
account: account:
@ -274,6 +283,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "pleroma:report", type: "pleroma:report",
account: AccountView.render("show.json", %{user: reporting_user, for: moderator_user}), account: AccountView.render("show.json", %{user: reporting_user, for: moderator_user}),
@ -300,6 +310,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "update", type: "update",
account: AccountView.render("show.json", %{user: user, for: repeat_user}), account: AccountView.render("show.json", %{user: user, for: repeat_user}),
@ -322,6 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: true, is_muted: true}, pleroma: %{is_seen: true, is_muted: true},
type: "favourite", type: "favourite",
account: AccountView.render("show.json", %{user: another_user, for: user}), account: AccountView.render("show.json", %{user: another_user, for: user}),
@ -345,6 +357,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
expected = %{ expected = %{
id: to_string(notification.id), id: to_string(notification.id),
group_key: "ungrouped-#{to_string(notification.id)}",
pleroma: %{is_seen: false, is_muted: false}, pleroma: %{is_seen: false, is_muted: false},
type: "status", type: "status",
account: account:

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