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

This commit is contained in:
Henry Jameson 2026-04-29 11:00:00 +03:00
commit db7bca8945
203 changed files with 2904 additions and 1090 deletions

View file

@ -0,0 +1,25 @@
name: 'Bug report'
about: 'Report a bug in Pleroma'
body:
- type: markdown
attributes:
value: |
### Precheck
* For support use https://git.pleroma.social/pleroma/pleroma-support or [community channels](https://git.pleroma.social/pleroma/pleroma#community-channels).
* Please do a quick search to ensure no similar bug has been reported before. If the bug has not been addressed after 2 weeks, it's fine to bump it.
* Try to ensure that the bug is actually related to the Pleroma backend. For example, if a bug happens in Pleroma-FE but not in Mastodon-FE or mobile clients, it's likely that the bug should be filed in [Pleroma-FE](https://git.pleroma.social/pleroma/pleroma-fe/issues/new) repository.
- type: textarea
id: environment
attributes:
label: Environment
value: |
* Installation type (OTP or From Source):
* Pleroma version (could be found in the "Version" tab of settings in Pleroma-FE):
* Elixir version (`elixir -v` for from source installations, N/A for OTP):
* Operating system:
* PostgreSQL version (`psql -V`):
- type: textarea
id: bug-description
attributes:
label: Bug description

View file

@ -0,0 +1,13 @@
### Checklist
- [ ] Adding a changelog: In the `changelog.d` directory, create a file named `<code>.<type>`.
<!--
`<code>` can be anything, but we recommend using a more or less unique identifier to avoid collisions, such as the branch name.
`<type>` can be `add`, `change`, `remove`, `fix`, `security` or `skip`. `skip` is only used if there is no user-visible change in the PR (for example, only editing comments in the code). Otherwise, choose a type that corresponds to your change.
In the file, write the changelog entry. For example, if a PR adds group functionality, we can create a file named `group.add` and write `Add group functionality` in it.
If one changelog entry is not enough, you may add more. But that might mean you can split it into two PRs. Only use more than one changelog entry if you really need to (for example, when one change in the code fix two different bugs, or when refactoring).
-->

View file

@ -0,0 +1,9 @@
when:
- event: pull_request
steps:
check-changelog:
image: docker.io/alpine:3.23
commands:
- apk add --no-cache git
- sh ./tools/check-changelog

65
.woodpecker/lint.yaml Normal file
View file

@ -0,0 +1,65 @@
when:
- event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
steps:
mix-format:
image: &elixir-image
docker.io/elixir:1.15-alpine
failure: ignore
commands:
- |
if ! mix format --check-formatted; then
touch fail.stamp
exit 1
fi
credo:
image: *elixir-image
failure: ignore
environment:
MIX_ENV: test
commands:
- apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
- adduser -D -h /home/testuser testuser
- mkdir -p /home/testuser/.mix /home/testuser/.hex
- chown -R testuser:testuser . /home/testuser
- su testuser -c "HOME=/home/testuser mix local.hex --force"
- su testuser -c "HOME=/home/testuser mix local.rebar --force"
- su testuser -c "HOME=/home/testuser mix deps.get"
- |
if ! su testuser -c "HOME=/home/testuser mix analyze"; then
touch fail.stamp
exit 1
fi
# cycles:
# image: *elixir-image
# failure: ignore
# commands:
# - apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
# - adduser -D -h /home/testuser testuser
# - mkdir -p /home/testuser/.mix /home/testuser/.hex
# - chown -R testuser:testuser . /home/testuser
# - su testuser -c "HOME=/home/testuser mix local.hex --force"
# - su testuser -c "HOME=/home/testuser mix local.rebar --force"
# - su testuser -c "HOME=/home/testuser mix compile"
# - |
# if ! su testuser -c "HOME=/home/testuser mix xref graph --format cycles --label compile | awk '{print $0} END{exit ($0 != \"No cycles found\")}'"; then
# touch fail.stamp
# exit 1
# fi
ensure-status:
image: *elixir-image
commands: |
if test -f fail.stamp; then
echo "One or more previous steps fails. Failing workflow..."
exit 1
else
echo "All steps passed"
exit 0
fi

View file

@ -0,0 +1,34 @@
when:
- event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
depends_on:
- lint
steps:
unit-testing-elixir-1.15:
image: elixir:1.15-alpine
environment:
MIX_ENV: test
DB_HOST: postgres
DB_PORT: 5432
commands:
- apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
- adduser -D -h /home/testuser testuser
- mkdir -p /home/testuser/.mix /home/testuser/.hex
- chown -R testuser:testuser . /home/testuser
- su testuser -c "HOME=/home/testuser mix local.hex --force"
- su testuser -c "HOME=/home/testuser mix local.rebar --force"
- su testuser -c "HOME=/home/testuser mix deps.get"
- su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules"
services:
postgres:
image: postgres:13-alpine
environment:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

View file

@ -0,0 +1,34 @@
when:
- event: pull_request
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
- event: push
branch: ${CI_REPO_DEFAULT_BRANCH}
path: [ "*.ex", "*.eex", "*.exs", "mix.lock", ".woodpecker/**" ]
depends_on:
- lint
steps:
unit-testing-elixir-1.18:
image: elixir:1.18-otp-27-alpine
environment:
MIX_ENV: test
DB_HOST: postgres
DB_PORT: 5432
commands:
- apk add --no-cache build-base cmake exiftool ffmpeg file-dev git openssl
- adduser -D -h /home/testuser testuser
- mkdir -p /home/testuser/.mix /home/testuser/.hex
- chown -R testuser:testuser . /home/testuser
- su testuser -c "HOME=/home/testuser mix local.hex --force"
- su testuser -c "HOME=/home/testuser mix local.rebar --force"
- su testuser -c "HOME=/home/testuser mix deps.get"
- su testuser -c "HOME=/home/testuser mix pleroma.test_runner --preload-modules"
services:
postgres:
image: postgres:13-alpine
environment:
POSTGRES_DB: pleroma_test
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres

View file

@ -1,4 +1,4 @@
<img src="https://git.pleroma.social/pleroma/pleroma/uploads/8cec84f5a084d887339f57deeb8a293e/pleroma-banner-vector-nopad-notext.svg" width="300px" /> <img src="https://git.pleroma.social/attachments/06a95f5a-7cac-42ad-8b1d-1483f1739f38" width="300px" />
## About ## About

View file

@ -0,0 +1 @@
Allow assigning users to reports

View file

@ -0,0 +1 @@
Move avatar_description and header_description fields to the account object

View file

@ -0,0 +1 @@
Update Bandit to 1.10.4

View file

@ -0,0 +1 @@
Various bookmark folders-related improvements

View file

@ -0,0 +1 @@
Allow fine-grained announce visibilities

View file

@ -0,0 +1 @@
Add immutable tag on cache-control header for several endpoints that's serving the same exact things.

View file

@ -0,0 +1 @@
Add reasonable defaults for :database_config_whitelist

View file

@ -0,0 +1 @@
No-op code correctness improvements detected by Elixir 1.19 compiler

View file

@ -0,0 +1 @@
Fix the daily email digest job which was not executing

View file

@ -0,0 +1 @@
Support lists `exclusive` param

View file

@ -0,0 +1 @@
Gopher: Fix Ranch listener not being stopped properly on Pleroma restart when database configuration is enabled

View file

@ -0,0 +1 @@
Downgrade Hackney to 1.20.1

View file

@ -0,0 +1 @@
Fix fetching Hubzilla Actors with alsoKnownAs as string

View file

@ -0,0 +1 @@
Add /api/v2/instance profile fields limits info used by Mastodon

View file

View file

@ -0,0 +1 @@
Fix /phoenix/live_dashboard redirect not working when user added a path segment

View file

View file

@ -0,0 +1 @@
Fix 404 error codes for missing static files

View file

View file

View file

@ -0,0 +1 @@
Correct old migrations for expiring activities and user access tokens.

View file

View file

@ -0,0 +1 @@
Federate `votersCount` correctly

View file

@ -0,0 +1 @@
DB prune: Check if user follows hashtag with no objects before deletion

View file

@ -0,0 +1 @@
Stop the rate limiter from crashing when run with wrong settings.

View file

@ -0,0 +1 @@
Add mute/block expiry to the relationship object

View file

@ -0,0 +1 @@
Restore embeds route

View file

@ -0,0 +1 @@
ReverseProxy: Recursively follow redirects until redirect_limit is reached

View file

@ -0,0 +1 @@
Filter indexable activities before inserting indexing jobs into the queue.

View file

View file

View file

@ -0,0 +1 @@
Update comment for prepare_object, rename prepare_outgoing

View file

@ -0,0 +1 @@
Avoid code duplication in UserView

View file

View file

@ -960,6 +960,15 @@ config :pleroma, Pleroma.Search.QdrantSearch,
vectors: %{size: 384, distance: "Cosine"} vectors: %{size: 384, distance: "Cosine"}
} }
config :pleroma, :database_config_whitelist, [
{:pleroma},
{:cors_plug},
{:ex_aws, :s3},
{:mime},
{:prometheus, Pleroma.Web.Endpoint.MetricsExporter},
{:web_push_encryption, :vapid_details}
]
# Import environment specific config. This must remain at the bottom # Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above. # of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs" import_config "#{Mix.env()}.exs"

View file

@ -169,4 +169,18 @@ This forcibly removes any enabled MRF that does not exist and will fix the abili
=== "From Source" === "From Source"
```sh ```sh
mix pleroma.config fix_mrf_policies mix pleroma.config fix_mrf_policies
``` ```
## Remove non-whitelisted configs from the database
This removes any configuration value that is not explicitly whitelisted by `:pleroma, :database_config_whitelist`. Might be useful after updating the whitelist.
=== "OTP"
```sh
./bin/pleroma_ctl config filter_whitelisted
```
=== "From Source"
```sh
mix pleroma.config filter_whitelisted
```

View file

@ -1132,8 +1132,9 @@ Boolean, enables/disables in-database configuration. Read [Transferring the conf
List of valid configuration sections which are allowed to be configured from the List of valid configuration sections which are allowed to be configured from the
database. Settings stored in the database before the whitelist is configured are database. Settings stored in the database before the whitelist is configured are
still applied, so it is suggested to only use the whitelist on instances that still applied. Consider running the `mix pleroma.config filter_whitelisted` task
have not migrated the config to the database. after updating the whitelist. Read [Remove non-whitelisted configs from the database](../administration/CLI_tasks/config.md#remove-non-whitelisted-configs-from-the-database)
for more information.
Example: Example:
```elixir ```elixir

View file

@ -665,6 +665,7 @@ Status: 404
- *optional* `limit`: **integer** the number of records to retrieve - *optional* `limit`: **integer** the number of records to retrieve
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of log entries per page (default is `50`) - *optional* `page_size`: **integer** number of log entries per page (default is `50`)
- *optional* `assigned_account`: **string** assigned account ID
- Response: - Response:
- On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin - On failure: 403 Forbidden error `{"error": "error_msg"}` when requested by anonymous or non-admin
- On success: JSON, returns a list of reports, where: - On success: JSON, returns a list of reports, where:
@ -749,6 +750,7 @@ Status: 404
"url": "https://pleroma.example.org/users/lain", "url": "https://pleroma.example.org/users/lain",
"username": "lain" "username": "lain"
}, },
"assigned_account": null,
"content": "Please delete it", "content": "Please delete it",
"created_at": "2019-04-29T19:48:15.000Z", "created_at": "2019-04-29T19:48:15.000Z",
"id": "9iJGOv1j8hxuw19bcm", "id": "9iJGOv1j8hxuw19bcm",
@ -868,6 +870,37 @@ Status: 404
] ]
``` ```
- Response:
- On failure:
- 400 Bad Request, JSON:
```json
[
{
`id`, // report id
`error` // error message
}
]
```
- On success: `204`, empty response
## `POST /api/v1/pleroma/admin/reports/assign_account`
### Assign account to one or multiple reports
- Params:
```json
`reports`: [
{
`id`, // required, report id
`nickname` // account nickname, use null to unassign account
},
...
]
```
- Response: - Response:
- On failure: - On failure:
- 400 Bad Request, JSON: - 400 Bad Request, JSON:

View file

@ -74,7 +74,7 @@ Pleroma does not process remote images and therefore cannot include fields such
The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID. The `GET /api/v1/bookmarks` endpoint accepts optional parameter `folder_id` for bookmark folder ID.
The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. The `POST /api/v1/statuses/:id/bookmark` endpoint accepts optional parameter `folder_id` for bookmark folder ID. Bookmarking an already bookmarked post will update the folder association, or remove it if `folder_id` is omitted or `null`.
## Accounts ## Accounts
@ -127,8 +127,6 @@ 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

View file

@ -690,6 +690,7 @@ Audio scrobbling in Pleroma is **deprecated**.
* `album`: the album of the media playing [optional] * `album`: the album of the media playing [optional]
* `artist`: the artist of the media playing [optional] * `artist`: the artist of the media playing [optional]
* `length`: the length of the media playing [optional] * `length`: the length of the media playing [optional]
* `external_link`: a URL referencing the media playing [optional]
* Response: the newly created media metadata entity representing the Listen activity * Response: the newly created media metadata entity representing the Listen activity
# Emoji Reactions # Emoji Reactions

View file

@ -234,6 +234,61 @@ defmodule Mix.Tasks.Pleroma.Config do
end) end)
end end
# Removes non-whitelisted configuration sections
def run(["filter_whitelisted" | rest]) do
{options, [], []} =
OptionParser.parse(
rest,
strict: [force: :boolean],
aliases: [f: :force]
)
force = Keyword.get(options, :force, false)
start_pleroma()
whitelisted_configs = Pleroma.Config.get(:database_config_whitelist)
if whitelisted_configs in [nil, false] do
shell_error("No unwanted settings in ConfigDB. No changes made.")
else
whitelisted_groups =
whitelisted_configs
|> Enum.filter(fn
{_group} -> true
_ -> false
end)
|> Enum.map(fn {group} -> group end)
whitelisted_keys =
whitelisted_configs
|> Enum.filter(fn
{_group, _key} -> true
_ -> false
end)
filtered =
from(c in ConfigDB)
|> Repo.all()
|> Enum.filter(&not_whitelisted?(&1, whitelisted_groups, whitelisted_keys))
if not Enum.empty?(filtered) do
shell_info("The following settings will be removed from ConfigDB:\n")
Enum.each(filtered, &dump(&1))
if force or shell_prompt("Are you sure you want to continue?", "n") in ~w(Yn Y y) do
filtered_ids = Enum.map(filtered, fn %{id: id} -> id end)
Repo.delete_all(from(c in ConfigDB, where: c.id in ^filtered_ids))
else
shell_error("No changes made.")
end
else
shell_error("No unwanted settings in ConfigDB. No changes made.")
end
end
end
@spec migrate_to_db(Path.t() | nil) :: any() @spec migrate_to_db(Path.t() | nil) :: any()
def migrate_to_db(file_path \\ nil) do def migrate_to_db(file_path \\ nil) do
with :ok <- Pleroma.Config.DeprecationWarnings.warn() do with :ok <- Pleroma.Config.DeprecationWarnings.warn() do
@ -434,4 +489,9 @@ defmodule Mix.Tasks.Pleroma.Config do
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;") Ecto.Adapters.SQL.query!(Repo, "TRUNCATE config;")
Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;") Ecto.Adapters.SQL.query!(Repo, "ALTER SEQUENCE config_id_seq RESTART;")
end end
defp not_whitelisted?(%{group: group, key: key}, whitelisted_groups, whitelisted_keys) do
not Enum.member?(whitelisted_groups, group) and
not Enum.member?(whitelisted_keys, {group, key})
end
end end

View file

@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do
DELETE FROM hashtags AS ht DELETE FROM hashtags AS ht
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 FROM hashtags_objects hto SELECT 1 FROM hashtags_objects hto
WHERE ht.id = hto.hashtag_id) WHERE ht.id = hto.hashtag_id
)
AND NOT EXISTS (
SELECT 1 FROM user_follows_hashtag ufh
WHERE ht.id = ufh.hashtag_id
)
""" """
|> Repo.query() |> Repo.query()

View file

@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do
else else
{_, errors} -> {_, errors} ->
IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"])) IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"]))
Enum.map(errors, &IO.puts/1) Enum.each(errors, &IO.puts/1)
raise "Spec check failed" raise "Spec check failed"
end end

View file

@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
query, query,
timeout: :infinity timeout: :infinity
) )
|> Stream.map(&Pleroma.Search.Meilisearch.object_to_search_data/1) |> Stream.map(&Pleroma.Search.object_to_search_data/1)
|> Stream.filter(fn o -> not is_nil(o) end) |> Stream.filter(fn o -> not is_nil(o) end)
|> Stream.chunk_every(chunk_size) |> Stream.chunk_every(chunk_size)
|> Stream.transform(0, fn objects, acc -> |> Stream.transform(0, fn objects, acc ->

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do
def invalidate_cache_for(activity_id) do def invalidate_cache_for(activity_id) do
keys = get_cache_keys_for(activity_id) keys = get_cache_keys_for(activity_id)
Enum.map(keys, &@cachex.del(:scrubber_cache, &1)) Enum.each(keys, &@cachex.del(:scrubber_cache, &1))
@cachex.del(:scrubber_management_cache, activity_id) @cachex.del(:scrubber_management_cache, activity_id)
end end

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Bookmark do
schema "bookmarks" do schema "bookmarks" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType) belongs_to(:activity, Activity, type: FlakeId.Ecto.CompatType)
belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.CompatType) belongs_to(:folder, BookmarkFolder, type: FlakeId.Ecto.Type)
timestamps() timestamps()
end end
@ -38,7 +38,7 @@ defmodule Pleroma.Bookmark do
|> validate_required([:user_id, :activity_id]) |> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index) |> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> Repo.insert( |> Repo.insert(
on_conflict: [set: [folder_id: folder_id]], on_conflict: [set: [folder_id: folder_id, updated_at: NaiveDateTime.utc_now()]],
conflict_target: [:user_id, :activity_id] conflict_target: [:user_id, :activity_id]
) )
end end
@ -76,11 +76,4 @@ defmodule Pleroma.Bookmark do
|> Repo.one() |> Repo.one()
|> Repo.delete() |> Repo.delete()
end end
def set_folder(bookmark, folder_id) do
bookmark
|> cast(%{folder_id: folder_id}, [:folder_id])
|> validate_required([:folder_id])
|> Repo.update()
end
end end

View file

@ -14,7 +14,7 @@ defmodule Pleroma.BookmarkFolder do
alias Pleroma.User alias Pleroma.User
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@primary_key {:id, FlakeId.Ecto.CompatType, autogenerate: true} @primary_key {:id, FlakeId.Ecto.Type, autogenerate: true}
schema "bookmark_folders" do schema "bookmark_folders" do
field(:name, :string) field(:name, :string)

View file

@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do
import Pleroma.Web.Gettext import Pleroma.Web.Gettext
alias __MODULE__ alias __MODULE__
alias Pleroma.EctoType.Config.RateLimit
alias Pleroma.Repo alias Pleroma.Repo
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do
|> cast(params, [:key, :group, :value]) |> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value]) |> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index) |> unique_constraint(:key, name: :config_group_key_index)
|> validate_rate_limit()
end end
defp validate_rate_limit(changeset) do
group = get_field(changeset, :group)
key = get_field(changeset, :key)
if group == :pleroma and key == :rate_limit do
value = get_field(changeset, :value)
case normalize_rate_limit(value) do
{:ok, normalized_value} ->
put_change(changeset, :value, normalized_value)
{:error, {limiter_name, reason}} ->
add_error(
changeset,
:value,
"invalid :rate_limit value for #{inspect(limiter_name)}: #{reason}"
)
end
else
changeset
end
end
defp normalize_rate_limit(nil), do: {:ok, nil}
defp normalize_rate_limit(%{} = value), do: normalize_rate_limit(Map.to_list(value))
defp normalize_rate_limit(value) when is_list(value) do
if Keyword.keyword?(value) do
value
|> Enum.reduce_while({:ok, []}, fn {limiter_name, limiter_value}, {:ok, acc} ->
case RateLimit.cast_with_error(limiter_value) do
{:ok, normalized_limiter_value} ->
{:cont, {:ok, [{limiter_name, normalized_limiter_value} | acc]}}
{:error, reason} ->
{:halt, {:error, {limiter_name, reason}}}
end
end)
|> case do
{:ok, acc} -> {:ok, Enum.reverse(acc)}
{:error, _} = error -> error
end
else
{:error, {:rate_limit, "must be a keyword list"}}
end
end
defp normalize_rate_limit(_), do: {:error, {:rate_limit, "must be a keyword list"}}
defp create(params) do defp create(params) do
%ConfigDB{} %ConfigDB{}
|> changeset(params) |> changeset(params)

View file

@ -22,13 +22,14 @@ defmodule Pleroma.Constants do
"generator", "generator",
"rules", "rules",
"language", "language",
"voters" "voters",
"assigned_account"
] ]
) )
const(static_only_files, const(static_only_files,
do: do:
~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js favicon.png schemas doc embed.js embed.css) ~w(index.html robots.txt static static-fe finmoji emoji packs sounds images instance sw.js sw-pleroma.js schemas doc embed.js embed.css)
) )
const(status_updatable_fields, const(status_updatable_fields,

View file

@ -0,0 +1,71 @@
# Pleroma: A lightweight social networking server
# Copyright © Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.EctoType.Config.RateLimit do
@moduledoc false
use Ecto.Type
@type t ::
nil
| {non_neg_integer(), non_neg_integer()}
| [{non_neg_integer(), non_neg_integer()}]
@impl true
def type, do: :term
@impl true
def cast(value) do
case cast_with_error(value) do
{:ok, normalized} -> {:ok, normalized}
{:error, _reason} -> :error
end
end
@impl true
def load(value), do: cast(value)
@impl true
def dump(value), do: cast(value)
@spec cast_with_error(term()) :: {:ok, t()} | {:error, String.t()}
def cast_with_error(nil), do: {:ok, nil}
def cast_with_error({scale, limit}) do
with {:ok, scale} <- parse_integer(scale, "scale"),
{:ok, limit} <- parse_integer(limit, "limit"),
true <- scale >= 1 and limit >= 1 do
{:ok, {scale, limit}}
else
false -> {:error, "scale and limit must be >= 1"}
{:error, reason} -> {:error, reason}
end
end
def cast_with_error([{_, _} = unauth, {_, _} = auth]) do
with {:ok, unauth} <- cast_with_error(unauth),
{:ok, auth} <- cast_with_error(auth) do
{:ok, [unauth, auth]}
else
{:error, reason} -> {:error, reason}
end
end
def cast_with_error(_),
do:
{:error, "must be a {scale, limit} tuple, a [{scale, limit}, {scale, limit}] list, or nil"}
defp parse_integer(value, _label) when is_integer(value), do: {:ok, value}
defp parse_integer(value, label) when is_binary(value) do
value = String.trim(value)
case Integer.parse(value) do
{number, ""} -> {:ok, number}
_ -> {:error, "#{label} must be an integer"}
end
end
defp parse_integer(_value, label), do: {:error, "#{label} must be an integer"}
end

View file

@ -21,10 +21,13 @@ defmodule Pleroma.Gopher.Server do
def init([ip, port]) do def init([ip, port]) do
Logger.info("Starting gopher server on #{port}") Logger.info("Starting gopher server on #{port}")
Process.flag(:trap_exit, true)
listener = :gopher
{:ok, _pid} = {:ok, _pid} =
:ranch.start_listener( :ranch.start_listener(
:gopher, listener,
:ranch_tcp, :ranch_tcp,
%{ %{
num_acceptors: 100, num_acceptors: 100,
@ -35,7 +38,11 @@ defmodule Pleroma.Gopher.Server do
[] []
) )
{:ok, %{ip: ip, port: port}} {:ok, %{ip: ip, port: port, listener: listener}}
end
def terminate(_reason, state) do
:ranch.stop_listener(state.listener)
end end
end end

View file

@ -17,13 +17,14 @@ defmodule Pleroma.List do
field(:title, :string) field(:title, :string)
field(:following, {:array, :string}, default: []) field(:following, {:array, :string}, default: [])
field(:ap_id, :string) field(:ap_id, :string)
field(:exclusive, :boolean, default: false)
timestamps() timestamps()
end end
def title_changeset(list, attrs \\ %{}) do def update_changeset(list, attrs \\ %{}) do
list list
|> cast(attrs, [:title]) |> cast(attrs, [:title, :exclusive])
|> validate_required([:title]) |> validate_required([:title])
end end
@ -91,14 +92,14 @@ defmodule Pleroma.List do
|> Repo.all() |> Repo.all()
end end
def rename(%Pleroma.List{} = list, title) do def update(%Pleroma.List{} = list, params) do
list list
|> title_changeset(%{title: title}) |> update_changeset(params)
|> Repo.update() |> Repo.update()
end end
def create(title, %User{} = creator) do def create(params, %User{} = creator) do
changeset = title_changeset(%Pleroma.List{user_id: creator.id}, %{title: title}) changeset = update_changeset(%Pleroma.List{user_id: creator.id}, params)
if changeset.valid? do if changeset.valid? do
Repo.transaction(fn -> Repo.transaction(fn ->
@ -149,4 +150,14 @@ defmodule Pleroma.List do
end end
def member?(_, _), do: false def member?(_, _), do: false
def get_exclusive_list_members(%User{id: user_id}) do
Pleroma.List
|> where([l], l.user_id == ^user_id)
|> where([l], l.exclusive == true)
|> select([l], l.following)
|> Repo.all()
|> List.flatten()
|> Enum.uniq()
end
end end

View file

@ -78,7 +78,7 @@ defmodule Pleroma.Marker do
defp get_marker(user, timeline) do defp get_marker(user, timeline) do
case Repo.find_resource(get_query(user, timeline)) do case Repo.find_resource(get_query(user, timeline)) do
{:ok, marker} -> %__MODULE__{marker | user: user} {:ok, %__MODULE__{} = marker} -> %{marker | user: user}
_ -> %__MODULE__{timeline: timeline, user_id: user.id} _ -> %__MODULE__{timeline: timeline, user_id: user.id}
end end
end end

View file

@ -8,7 +8,8 @@ defmodule Pleroma.MFA.Changeset do
alias Pleroma.User alias Pleroma.User
def disable(%Ecto.Changeset{} = changeset, force \\ false) do def disable(%Ecto.Changeset{} = changeset, force \\ false) do
settings = %Settings{} =
settings =
changeset changeset
|> Ecto.Changeset.apply_changes() |> Ecto.Changeset.apply_changes()
|> MFA.fetch_settings() |> MFA.fetch_settings()
@ -20,20 +21,20 @@ defmodule Pleroma.MFA.Changeset do
end end
end end
def disable_totp(%User{multi_factor_authentication_settings: settings} = user) do def disable_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do
user user
|> put_change(%Settings{settings | totp: %Settings.TOTP{}}) |> put_change(%Settings{settings | totp: %Settings.TOTP{}})
end end
def confirm_totp(%User{multi_factor_authentication_settings: settings} = user) do def confirm_totp(%User{multi_factor_authentication_settings: %Settings{} = settings} = user) do
totp_settings = %Settings.TOTP{settings.totp | confirmed: true} totp_settings = %Settings.TOTP{(%Settings.TOTP{} = settings.totp) | confirmed: true}
user user
|> put_change(%Settings{settings | totp: totp_settings, enabled: true}) |> put_change(%Settings{settings | totp: totp_settings, enabled: true})
end end
def setup_totp(%User{} = user, attrs) do def setup_totp(%User{} = user, attrs) do
mfa_settings = MFA.fetch_settings(user) %Settings{} = mfa_settings = MFA.fetch_settings(user)
totp_settings = totp_settings =
%Settings.TOTP{} %Settings.TOTP{}
@ -46,7 +47,7 @@ defmodule Pleroma.MFA.Changeset do
def cast_backup_codes(%User{} = user, codes) do def cast_backup_codes(%User{} = user, codes) do
user user
|> put_change(%Settings{ |> put_change(%Settings{
user.multi_factor_authentication_settings (%Settings{} = user.multi_factor_authentication_settings)
| backup_codes: codes | backup_codes: codes
}) })
end end

View file

@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do
end end
def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs) def insert_log(%{actor: %User{}, action: action, subject: %Activity{} = subject} = attrs)
when action in ["report_note_delete", "report_update", "report_note"] do when action in [
"report_note_delete",
"report_update",
"report_note",
"report_unassigned",
"report_assigned"
] do
data = data =
attrs attrs
|> prepare_log_data |> prepare_log_data
|> Pleroma.Maps.put_if_present("text", attrs[:text]) |> Pleroma.Maps.put_if_present("text", attrs[:text])
|> Pleroma.Maps.put_if_present("assigned_account", attrs[:assigned_account])
|> Map.merge(%{"subject" => report_to_map(subject)}) |> Map.merge(%{"subject" => report_to_map(subject)})
insert_log_entry_with_message(%ModerationLog{data: data}) insert_log_entry_with_message(%ModerationLog{data: data})
@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do
" with '#{state}' state" " with '#{state}' state"
end end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_assigned",
"subject" => %{"id" => subject_id, "type" => "report"},
"assigned_account" => assigned_account
}
} = log
) do
"@#{actor_nickname} assigned report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" to user #{assigned_account}"
end
def get_log_entry_message(
%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "report_unassigned",
"subject" => %{"id" => subject_id, "type" => "report"}
}
} = log
) do
"@#{actor_nickname} unassigned report ##{subject_id}" <>
subject_actor_nickname(log, " (on user ", ")") <>
" from a user"
end
def get_log_entry_message( def get_log_entry_message(
%ModerationLog{ %ModerationLog{
data: %{ data: %{

View file

@ -374,10 +374,18 @@ defmodule Pleroma.Object do
voters = [actor | object.data["voters"] || []] |> Enum.uniq() voters = [actor | object.data["voters"] || []] |> Enum.uniq()
voters_count =
if Map.has_key?(object.data, "votersCount") do
object.data["votersCount"] + 1
else
length(voters)
end
data = data =
object.data object.data
|> Map.put(key, options) |> Map.put(key, options)
|> Map.put("voters", voters) |> Map.put("voters", voters)
|> Map.put("votersCount", voters_count)
object object
|> Object.change(%{data: data}) |> Object.change(%{data: data})

View file

@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do
@keep_resp_headers @resp_cache_headers ++ @keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++ ~w(content-length content-type content-disposition content-encoding) ++
~w(content-range accept-ranges vary) ~w(content-range accept-ranges vary)
@default_cache_control_header "public, max-age=1209600" @default_cache_control_header "public, max-age=1209600, immutable"
@valid_resp_codes [200, 206, 304] @valid_resp_codes [200, 206, 304]
@max_read_duration :timer.seconds(30) @max_read_duration :timer.seconds(30)
@max_body_length :infinity @max_body_length :infinity

View file

@ -4,6 +4,9 @@
defmodule Pleroma.ReverseProxy.Client.Hackney do defmodule Pleroma.ReverseProxy.Client.Hackney do
@behaviour Pleroma.ReverseProxy.Client @behaviour Pleroma.ReverseProxy.Client
@redirect_limit 5
require Logger
# In-app redirect handler to avoid Hackney redirect bugs: # In-app redirect handler to avoid Hackney redirect bugs:
# - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney) # - https://github.com/benoitc/hackney/issues/527 (relative/protocol-less redirects can crash Hackney)
@ -31,26 +34,15 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
if opts[:follow_redirect] != false do if opts[:follow_redirect] != false do
{_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end) {_state, req_opts} = Access.get_and_update(opts, :follow_redirect, fn a -> {a, false} end)
env = %{method: method, headers: headers, body: body, req_opts: req_opts}
res = :hackney.request(method, url, headers, body, req_opts) res = :hackney.request(method, url, headers, body, req_opts)
case res do case res do
{:ok, code, resp_headers, _client} when code in @redirect_statuses -> {:ok, code, resp_headers, _client} when code in @redirect_statuses ->
:hackney.request( redirect(url, resp_headers, env, @redirect_limit)
method,
absolute_redirect_url(url, resp_headers),
headers,
body,
req_opts
)
{:ok, code, resp_headers} when code in @redirect_statuses -> {:ok, code, resp_headers} when code in @redirect_statuses ->
:hackney.request( redirect(url, resp_headers, env, @redirect_limit)
method,
absolute_redirect_url(url, resp_headers),
headers,
body,
req_opts
)
_ -> _ ->
res res
@ -71,4 +63,32 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true @impl true
def close(ref), do: :hackney.close(ref) def close(ref), do: :hackney.close(ref)
defp redirect(url, resp_headers, env, limit) when limit == 0 do
new_url = absolute_redirect_url(url, resp_headers)
Logger.debug(
"#{__MODULE__}: Handling redirect #{url} -> #{new_url}; redirect limit was reached - returning response after final redirect"
)
:hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
end
defp redirect(url, resp_headers, env, limit) do
new_url = absolute_redirect_url(url, resp_headers)
Logger.debug("#{__MODULE__}: handling redirect #{url} -> #{new_url}; limit = #{limit}")
res = :hackney.request(env.method, new_url, env.headers, env.body, env.req_opts)
case res do
{:ok, code, new_resp_headers, _client} when code in @redirect_statuses ->
redirect(new_url, new_resp_headers, env, limit - 1)
{:ok, code, new_resp_headers} when code in @redirect_statuses ->
redirect(new_url, new_resp_headers, env, limit - 1)
_ ->
res
end
end
end end

View file

@ -1,11 +1,28 @@
defmodule Pleroma.Search do defmodule Pleroma.Search do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Workers.SearchIndexingWorker alias Pleroma.Workers.SearchIndexingWorker
def add_to_index(%Pleroma.Activity{id: activity_id}) do @spec add_to_index(Activity.t()) :: {:ok, Oban.Job.t() | :noop} | {:error, Oban.Job.changeset()}
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id}) def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do
|> Oban.insert() with {_, true} <- {:indexable, indexable?(activity)},
{_, "public"} <- {:visibility, Visibility.get_visibility(object)} do
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
|> Oban.insert()
else
_ -> {:ok, :noop}
end
end end
def add_to_index(%Activity{id: activity_id}) do
case Activity.get_by_id_with_object(activity_id) do
%Activity{} = preloaded -> add_to_index(preloaded)
_ -> {:ok, :noop}
end
end
@spec remove_from_index(Object.t()) :: {:ok, Oban.Job.t()} | {:error, Oban.Job.changeset()}
def remove_from_index(%Pleroma.Object{id: object_id}) do def remove_from_index(%Pleroma.Object{id: object_id}) do
SearchIndexingWorker.new(%{"op" => "remove_from_index", "object" => object_id}) SearchIndexingWorker.new(%{"op" => "remove_from_index", "object" => object_id})
|> Oban.insert() |> Oban.insert()
@ -20,4 +37,44 @@ defmodule Pleroma.Search do
search_module = Pleroma.Config.get([Pleroma.Search, :module]) search_module = Pleroma.Config.get([Pleroma.Search, :module])
search_module.healthcheck_endpoints() search_module.healthcheck_endpoints()
end end
def object_to_search_data(%Object{} = object) do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <-
FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
trimmed <- String.trim(scrubbed) do
trimmed
end
# Make sure we have a non-empty string
if content != "" do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
id: object.id,
content: content,
ap: data["id"],
published: published |> DateTime.to_unix()
}
end
end
defp indexable?(%Activity{
data: %{"type" => "Create"},
object: %Object{
data: %{"content" => content, "published" => published, "type" => "Note"}
}
})
when not is_nil(content) and content not in ["", "."] and not is_nil(published),
do: true
defp indexable?(_), do: false
end end

View file

@ -4,6 +4,8 @@ defmodule Pleroma.Search.Meilisearch do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config.Getting, as: Config alias Pleroma.Config.Getting, as: Config
alias Pleroma.Object
alias Pleroma.Search
import Pleroma.Search.DatabaseSearch import Pleroma.Search.DatabaseSearch
import Ecto.Query import Ecto.Query
@ -118,66 +120,24 @@ defmodule Pleroma.Search.Meilisearch do
end end
end end
def object_to_search_data(object) do
# Only index public or unlisted Notes
if not is_nil(object) and object.data["type"] == "Note" and
not is_nil(object.data["content"]) and
not is_nil(object.data["published"]) and
(Pleroma.Constants.as_public() in object.data["to"] or
Pleroma.Constants.as_public() in object.data["cc"]) and
object.data["content"] not in ["", "."] do
data = object.data
content_str =
case data["content"] do
[nil | rest] -> to_string(rest)
str -> str
end
content =
with {:ok, scrubbed} <-
FastSanitize.Sanitizer.scrub(content_str, Pleroma.HTML.Scrubber.SearchIndexing),
trimmed <- String.trim(scrubbed) do
trimmed
end
# Make sure we have a non-empty string
if content != "" do
{:ok, published, _} = DateTime.from_iso8601(data["published"])
%{
id: object.id,
content: content,
ap: data["id"],
published: published |> DateTime.to_unix()
}
end
end
end
@impl true @impl true
def add_to_index(activity) do def add_to_index(%Activity{object: %Object{} = object} = activity) do
maybe_search_data = object_to_search_data(activity.object) search_data = Search.object_to_search_data(object)
if activity.data["type"] == "Create" and maybe_search_data do result =
result = meili_put(
meili_put( "/indexes/objects/documents",
"/indexes/objects/documents", [search_data]
[maybe_search_data] )
)
with {:ok, %{"status" => "enqueued"}} <- result do with {:ok, %{"status" => "enqueued"}} <- result do
# Added successfully # Added successfully
:ok
else
_ ->
# There was an error, report it
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
{:error, result}
end
else
# The post isn't something we can search, that's ok
:ok :ok
else
_ ->
# There was an error, report it
Logger.error("Failed to add activity #{activity.id} to index: #{inspect(result)}")
{:error, result}
end end
end end

View file

@ -4,11 +4,12 @@ defmodule Pleroma.Search.QdrantSearch do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config.Getting, as: Config alias Pleroma.Config.Getting, as: Config
alias Pleroma.Object
alias Pleroma.Search
alias __MODULE__.OpenAIClient alias __MODULE__.OpenAIClient
alias __MODULE__.QdrantClient alias __MODULE__.QdrantClient
import Pleroma.Search.Meilisearch, only: [object_to_search_data: 1]
import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3] import Pleroma.Search.DatabaseSearch, only: [maybe_fetch: 3]
@impl true @impl true
@ -82,23 +83,18 @@ defmodule Pleroma.Search.QdrantSearch do
end end
@impl true @impl true
def add_to_index(activity) do def add_to_index(%Activity{object: %Object{} = object} = activity) do
# This will only index public or unlisted notes search_data = Search.object_to_search_data(object)
maybe_search_data = object_to_search_data(activity.object)
if activity.data["type"] == "Create" and maybe_search_data do with {:ok, embedding} <- get_embedding(search_data.content),
with {:ok, embedding} <- get_embedding(maybe_search_data.content), {:ok, %{status: 200}} <-
{:ok, %{status: 200}} <- QdrantClient.put(
QdrantClient.put( "/collections/posts/points",
"/collections/posts/points", build_index_payload(activity, embedding)
build_index_payload(activity, embedding) ) do
) do
:ok
else
e -> {:error, e}
end
else
:ok :ok
else
e -> {:error, e}
end end
end end

View file

@ -93,7 +93,7 @@ defmodule Pleroma.Upload do
def store(upload, opts \\ []) do def store(upload, opts \\ []) do
opts = get_opts(opts) opts = get_opts(opts)
with {:ok, upload} <- prepare_upload(upload, opts), with {:ok, %__MODULE__{} = upload} <- prepare_upload(upload, opts),
upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = get_description(upload), description = get_description(upload),

View file

@ -342,7 +342,7 @@ defmodule Pleroma.User.Backup do
dir, dir,
"outbox", "outbox",
fn a -> fn a ->
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do with {:ok, activity} <- Transmogrifier.prepare_activity(a.data) do
{:ok, Map.delete(activity, "@context")} {:ok, Map.delete(activity, "@context")}
end end
end end

View file

@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do
do: exists?(unquote(relationship_type), source, target) do: exists?(unquote(relationship_type), source, target)
# `def get_block_expire_date/2`, `def get_mute_expire_date/2`, # `def get_block_expire_date/2`, `def get_mute_expire_date/2`,
# `def get_reblog_mute_expire_date/2`, `def get_notification_mute_exists?/2`, # `def get_reblog_mute_expire_date/2`, `def get_notification_mute_expire_date/2`,
# `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2` # `def get_inverse_subscription_expire_date/2`, `def get_inverse_endorsement_expire_date/2`
def unquote(:"get_#{relationship_type}_expire_date")(source, target), def unquote(:"get_#{relationship_type}_expire_date")(source, target),
do: get_expire_date(unquote(relationship_type), source, target) do: get_expire_date(unquote(relationship_type), source, target)

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Utils do
dir dir
|> File.ls!() |> File.ls!()
|> Enum.map(&Path.join(dir, &1)) |> Enum.map(&Path.join(dir, &1))
|> Kernel.ParallelCompiler.compile() |> Kernel.ParallelCompiler.compile(return_diagnostics: true)
end end
@doc """ @doc """

View file

@ -1003,6 +1003,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_state(query, _), do: query defp restrict_state(query, _), do: query
defp restrict_assigned_account(query, %{assigned_account: assigned_account}) do
from(activity in query,
where: fragment("?->>'assigned_account' = ?", activity.data, ^assigned_account)
)
end
defp restrict_assigned_account(query, _), do: query
defp restrict_favorited_by(query, %{favorited_by: ap_id}) do defp restrict_favorited_by(query, %{favorited_by: ap_id}) do
from( from(
[_activity, object] in query, [_activity, object] in query,
@ -1471,6 +1479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_actor(opts) |> restrict_actor(opts)
|> restrict_type(opts) |> restrict_type(opts)
|> restrict_state(opts) |> restrict_state(opts)
|> restrict_assigned_account(opts)
|> restrict_favorited_by(opts) |> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_opts) |> restrict_blocked(restrict_blocked_opts)
|> restrict_blockers_visibility(opts) |> restrict_blockers_visibility(opts)
@ -1609,6 +1618,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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 normalize_also_known_as(urls) when is_list(urls), do: urls
defp normalize_also_known_as(url) when is_binary(url), do: [url]
defp normalize_also_known_as(nil), do: []
defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do defp maybe_put_description(map, %{"name" => description}) when is_binary(description) do
Map.put(map, "name", description) Map.put(map, "name", description)
end end
@ -1684,7 +1697,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
featured_address: featured_address, featured_address: featured_address,
bio: data["summary"] || "", bio: data["summary"] || "",
actor_type: actor_type, actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []), also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
public_key: public_key, public_key: public_key,
inbox: data["inbox"], inbox: data["inbox"],
shared_inbox: shared_inbox, shared_inbox: shared_inbox,

View file

@ -332,21 +332,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do
@spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()} @spec announce(User.t(), Object.t(), keyword()) :: {:ok, map(), keyword()}
def announce(actor, object, options \\ []) do def announce(actor, object, options \\ []) do
public? = Keyword.get(options, :public, false) visibility = Keyword.get(options, :visibility, "public")
to = {to, cc} =
cond do if actor.ap_id == Relay.ap_id() do
actor.ap_id == Relay.ap_id() -> {[actor.follower_address], []}
[actor.follower_address] else
Pleroma.Web.CommonAPI.Utils.get_to_and_cc_for_visibility(
public? and Visibility.local_public?(object) -> visibility,
[actor.follower_address, object.data["actor"], Utils.as_local_public()] actor.follower_address,
nil,
public? -> [object.data["actor"]]
[actor.follower_address, object.data["actor"], Pleroma.Constants.as_public()] )
true ->
[actor.follower_address, object.data["actor"]]
end end
{:ok, {:ok,
@ -355,6 +352,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
"actor" => actor.ap_id, "actor" => actor.ap_id,
"object" => object.data["id"], "object" => object.data["id"],
"to" => to, "to" => to,
"cc" => cc,
"context" => object.data["context"], "context" => object.data["context"],
"type" => "Announce", "type" => "Announce",
"published" => Utils.make_date() "published" => Utils.make_date()

View file

@ -28,6 +28,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
end end
field(:closed, ObjectValidators.DateTime) field(:closed, ObjectValidators.DateTime)
field(:votersCount, :integer)
field(:voters, {:array, ObjectValidators.ObjectID}, default: []) field(:voters, {:array, ObjectValidators.ObjectID}, default: [])
field(:nonAnonymous, :boolean) field(:nonAnonymous, :boolean)
embeds_many(:anyOf, QuestionOptionsValidator) embeds_many(:anyOf, QuestionOptionsValidator)

View file

@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Determine if an activity can be represented by running it through Transmogrifier. Determine if an activity can be represented by running it through Transmogrifier.
""" """
def representable?(%Activity{} = activity) do def representable?(%Activity{} = activity) do
with {:ok, _data} <- @transmogrifier_impl.prepare_outgoing(activity.data) do with {:ok, _data} <- @transmogrifier_impl.prepare_activity(activity.data) do
true true
else else
_e -> _e ->
@ -102,14 +102,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Logger.debug("Federating #{ap_id} to #{inbox}") Logger.debug("Federating #{ap_id} to #{inbox}")
uri = %{path: path} = URI.parse(inbox) uri = %{path: path} = URI.parse(inbox)
{:ok, data} = @transmogrifier_impl.prepare_outgoing(activity.data) {:ok, data} = @transmogrifier_impl.prepare_activity(activity.data)
{actor, data} = {actor, data} =
with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
{actor, data} {actor, data}
else else
{:actor_changed?, true} -> {:actor_changed?, true} ->
# If prepare_outgoing changes the actor, re-get it from the db # If prepare_activity changes the actor, re-get it from the db
new_actor = User.get_cached_by_ap_id(data["actor"]) new_actor = User.get_cached_by_ap_id(data["actor"])
{new_actor, data} {new_actor, data}
end end

View file

@ -783,7 +783,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def set_replies(obj_data), do: obj_data def set_replies(obj_data), do: obj_data
# Prepares the object of an outgoing create activity. defp set_voters_count(%{"voters" => [_ | _] = voters} = obj) do
Map.merge(obj, %{"votersCount" => length(voters)})
end
defp set_voters_count(obj), do: obj
# Prepares and sanitizes the object for federation.
def prepare_object(object) do def prepare_object(object) do
object object
|> add_hashtags |> add_hashtags
@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri |> set_reply_to_uri
|> set_quote_url |> set_quote_url
|> set_replies |> set_replies
|> set_voters_count
|> CommonFixes.maybe_add_content_map() |> CommonFixes.maybe_add_content_map()
|> strip_internal_fields |> strip_internal_fields
|> strip_internal_tags |> strip_internal_tags
@ -824,7 +831,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# internal -> Mastodon # internal -> Mastodon
# """ # """
def prepare_outgoing(%{"type" => activity_type, "object" => object_id} = data) def prepare_activity(%{"type" => activity_type, "object" => object_id} = data)
when activity_type in ["Create", "Listen"] do when activity_type in ["Create", "Listen"] do
object = object =
object_id object_id
@ -840,7 +847,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.updatable_object_types() do when objtype in Pleroma.Constants.updatable_object_types() do
data = data =
data data
@ -851,7 +858,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data) def prepare_activity(%{"type" => "Update", "object" => %{"type" => objtype} = object} = data)
when objtype in Pleroma.Constants.actor_types() do when objtype in Pleroma.Constants.actor_types() do
object = object =
object object
@ -868,11 +875,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data} {:ok, data}
end end
def prepare_outgoing(%{"type" => "Update", "object" => %{}} = data) do def prepare_activity(%{"type" => "Update", "object" => %{}} = data) do
raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}" raise "Requested to serve an Update for non-updateable object type: #{inspect(data)}"
end end
def prepare_outgoing(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do def prepare_activity(%{"type" => "Announce", "actor" => ap_id, "object" => object_id} = data) do
object = object =
object_id object_id
|> Object.normalize(fetch: false) |> Object.normalize(fetch: false)
@ -895,7 +902,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs,
# because of course it does. # because of course it does.
def prepare_outgoing(%{"type" => "Accept"} = data) do def prepare_activity(%{"type" => "Accept"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do with follow_activity <- Activity.normalize(data["object"]) do
object = %{ object = %{
"actor" => follow_activity.actor, "actor" => follow_activity.actor,
@ -913,7 +920,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def prepare_outgoing(%{"type" => "Reject"} = data) do def prepare_activity(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do with follow_activity <- Activity.normalize(data["object"]) do
object = %{ object = %{
"actor" => follow_activity.actor, "actor" => follow_activity.actor,
@ -931,7 +938,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def prepare_outgoing(%{"type" => "Flag"} = data) do def prepare_activity(%{"type" => "Flag"} = data) do
with {:ok, stripped_activity} <- Utils.strip_report_status_data(data), with {:ok, stripped_activity} <- Utils.strip_report_status_data(data),
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity), stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
@ -939,7 +946,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end end
end end
def prepare_outgoing(%{"type" => _type} = data) do def prepare_activity(%{"type" => _type} = data) do
data = data =
data data
|> strip_internal_fields |> strip_internal_fields

View file

@ -7,5 +7,5 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier.API do
Behaviour for the subset of Transmogrifier used by Publisher. Behaviour for the subset of Transmogrifier used by Publisher.
""" """
@callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()} @callback prepare_activity(map()) :: {:ok, map()} | {:error, term()}
end end

View file

@ -863,6 +863,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do
def update_report_state(_, _), do: {:error, "Unsupported state"} def update_report_state(_, _), do: {:error, "Unsupported state"}
def assign_report_to_account(%Activity{} = activity, nil = _account) do
new_data = Map.delete(activity.data, "assigned_account")
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(%Activity{} = activity, account) do
new_data = Map.put(activity.data, "assigned_account", account)
activity
|> Changeset.change(data: new_data)
|> Repo.update()
end
def assign_report_to_account(activity_ids, account) do
activities_num = length(activity_ids)
from(a in Activity, where: a.id in ^activity_ids)
|> update(set: [data: fragment("jsonb_set(data, '{assigned_account}', ?)", ^account)])
|> Repo.update_all([])
|> case do
{^activities_num, _} -> :ok
_ -> {:error, activity_ids}
end
end
def strip_report_status_data(%Activity{} = activity) do def strip_report_status_data(%Activity{} = activity) do
with {:ok, new_data} <- strip_report_status_data(activity.data) do with {:ok, new_data} <- strip_report_status_data(activity.data) do
{:ok, %{activity | data: new_data}} {:ok, %{activity | data: new_data}}

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
end end
def render("object.json", %{object: %Activity{} = activity}) do def render("object.json", %{object: %Activity{} = activity}) do
{:ok, ap_data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, ap_data} = Transmogrifier.prepare_activity(activity.data)
ap_data ap_data
end end

View file

@ -35,32 +35,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
def render("endpoints.json", _), do: %{} def render("endpoints.json", _), do: %{}
def render("service.json", %{user: user}) do def render("service.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys) Map.merge(common_actor_fields(user), %{
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
endpoints = render("endpoints.json", %{user: user})
%{
"id" => user.ap_id,
"type" => "Application", "type" => "Application",
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"name" => "Pleroma", "name" => "Pleroma",
"summary" => "summary" =>
"An internal service actor for this Pleroma instance. No user-serviceable parts inside.", "An internal service actor for this Pleroma instance. No user-serviceable parts inside.",
"url" => user.ap_id,
"manuallyApprovesFollowers" => false, "manuallyApprovesFollowers" => false,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"invisible" => User.invisible?(user) "invisible" => User.invisible?(user)
} })
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
@ -77,13 +59,8 @@ defmodule Pleroma.Web.ActivityPub.UserView do
end end
def render("user.json", %{user: user}) do def render("user.json", %{user: user}) do
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
user = User.sanitize_html(user) user = User.sanitize_html(user)
endpoints = render("endpoints.json", %{user: user})
emoji_tags = Transmogrifier.take_emoji_tags(user) emoji_tags = Transmogrifier.take_emoji_tags(user)
fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue")) fields = Enum.map(user.fields, &Map.put(&1, "type", "PropertyValue"))
@ -102,25 +79,9 @@ defmodule Pleroma.Web.ActivityPub.UserView do
do: Date.to_iso8601(user.birthday), do: Date.to_iso8601(user.birthday),
else: nil else: nil
%{ Map.merge(common_actor_fields(user), %{
"id" => user.ap_id,
"type" => user.actor_type,
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"featured" => "#{user.ap_id}/collections/featured", "featured" => "#{user.ap_id}/collections/featured",
"preferredUsername" => user.nickname, "preferredUsername" => user.nickname,
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.is_locked,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
},
"endpoints" => endpoints,
"attachment" => fields, "attachment" => fields,
"tag" => emoji_tags, "tag" => emoji_tags,
# Note: key name is indeed "discoverable" (not an error) # Note: key name is indeed "discoverable" (not an error)
@ -130,7 +91,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
"vcard:bday" => birthday, "vcard:bday" => birthday,
"webfinger" => "acct:#{User.full_nickname(user)}", "webfinger" => "acct:#{User.full_nickname(user)}",
"published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at) "published" => Pleroma.Web.CommonAPI.Utils.to_masto_date(user.inserted_at)
} })
|> Map.merge( |> Map.merge(
maybe_make_image( maybe_make_image(
&User.avatar_url/2, &User.avatar_url/2,
@ -283,7 +244,7 @@ defmodule Pleroma.Web.ActivityPub.UserView do
}) do }) do
collection = collection =
Enum.map(activities, fn activity -> Enum.map(activities, fn activity ->
{:ok, data} = Transmogrifier.prepare_outgoing(activity.data) {:ok, data} = Transmogrifier.prepare_activity(activity.data)
data data
end) end)
@ -309,6 +270,33 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(Utils.make_json_ld_header()) |> Map.merge(Utils.make_json_ld_header())
end end
defp common_actor_fields(%User{} = user) do
endpoints = render("endpoints.json", %{user: user})
{:ok, _, public_key} = Keys.keys_from_pem(user.keys)
public_key = :public_key.pem_entry_encode(:SubjectPublicKeyInfo, public_key)
public_key = :public_key.pem_encode([public_key])
%{
"id" => user.ap_id,
"type" => user.actor_type,
"following" => "#{user.ap_id}/following",
"followers" => "#{user.ap_id}/followers",
"inbox" => "#{user.ap_id}/inbox",
"outbox" => "#{user.ap_id}/outbox",
"name" => user.name,
"summary" => user.bio,
"url" => user.ap_id,
"manuallyApprovesFollowers" => user.is_locked,
"endpoints" => endpoints,
"publicKey" => %{
"id" => "#{user.ap_id}#main-key",
"owner" => user.ap_id,
"publicKeyPem" => public_key
}
}
end
defp maybe_put_total_items(map, false, _total), do: map defp maybe_put_total_items(map, false, _total), do: map
defp maybe_put_total_items(map, true, total) do defp maybe_put_total_items(map, true, total) do

View file

@ -174,6 +174,8 @@ defmodule Pleroma.Web.AdminAPI.ConfigController do
end end
end end
defp whitelisted_config?(":pleroma", ":database_config_whitelist"), do: false
defp whitelisted_config?(group, key) do defp whitelisted_config?(group, key) do
if whitelisted_configs = Config.get(:database_config_whitelist) do if whitelisted_configs = Config.get(:database_config_whitelist) do
Enum.any?(whitelisted_configs, fn Enum.any?(whitelisted_configs, fn

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ModerationLog alias Pleroma.ModerationLog
alias Pleroma.ReportNote alias Pleroma.ReportNote
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report alias Pleroma.Web.AdminAPI.Report
@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["admin:write:reports"]} %{scopes: ["admin:write:reports"]}
when action in [:update, :notes_create, :notes_delete] when action in [:update, :assign_account, :notes_create, :notes_delete]
) )
action_fallback(AdminAPI.FallbackController) action_fallback(AdminAPI.FallbackController)
@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
end end
end end
def assign_account(
%{
assigns: %{user: admin},
private: %{open_api_spex: %{body_params: %{reports: reports}}}
} = conn,
_
) do
result = Enum.map(reports, &do_assign_account(&1, admin))
if Enum.any?(result, &Map.has_key?(&1, :error)) do
json_response(conn, :bad_request, result)
else
json_response(conn, :no_content, "")
end
end
def notes_create( def notes_create(
%{ %{
assigns: %{user: user}, assigns: %{user: user},
@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
_ -> json_response(conn, :bad_request, "") _ -> json_response(conn, :bad_request, "")
end end
end end
defp do_assign_account(%{assigned_account: nil, id: id}, admin) do
with {:ok, activity} <- CommonAPI.assign_report_to_account(id, nil),
report <- Activity.get_by_id_with_user_actor(activity.id) do
ModerationLog.insert_log(%{
action: "report_unassigned",
actor: admin,
subject: activity,
subject_actor: report.user_actor
})
activity
else
{:error, message} ->
%{id: id, error: message}
end
end
defp do_assign_account(%{assigned_account: assigned_account, id: id}, admin) do
with %User{id: account} = user <- User.get_cached_by_nickname(assigned_account),
{:ok, activity} <- CommonAPI.assign_report_to_account(id, account),
report <- Activity.get_by_id_with_user_actor(activity.id) do
ModerationLog.insert_log(%{
action: "report_assigned",
actor: admin,
subject: activity,
subject_actor: report.user_actor,
assigned_account: user.nickname
})
activity
else
{:error, message} ->
%{id: id, error: message}
end
end
end end

View file

@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do
user = User.get_cached_by_ap_id(actor) user = User.get_cached_by_ap_id(actor)
account = User.get_cached_by_ap_id(account_ap_id) account = User.get_cached_by_ap_id(account_ap_id)
assigned_account =
if Map.has_key?(report.data, "assigned_account") do
User.get_cached_by_id(report.data["assigned_account"])
end
statuses = statuses =
status_ap_ids status_ap_ids
|> Enum.reject(&is_nil(&1)) |> Enum.reject(&is_nil(&1))
@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do
Activity.get_by_ap_id_with_object(act) Activity.get_by_ap_id_with_object(act)
end) end)
%{report: report, user: user, account: account, statuses: statuses} %{
report: report,
user: user,
account: account,
statuses: statuses,
assigned_account: assigned_account
}
end end
defp make_fake_activity(act, user) do defp make_fake_activity(act, user) do

View file

@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
} }
end end
def render("show.json", %{report: report, user: user, account: account, statuses: statuses}) do def render("show.json", %{
report: report,
user: user,
account: account,
statuses: statuses,
assigned_account: assigned_account
}) do
created_at = Utils.to_masto_date(report.data["published"]) created_at = Utils.to_masto_date(report.data["published"])
content = content =
@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
nil nil
end end
assigned_account =
if assigned_account do
merge_account_views(assigned_account)
end
%{ %{
id: report.id, id: report.id,
account: merge_account_views(account), account: merge_account_views(account),
@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}), }),
state: report.data["state"], state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}), notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes}),
rules: rules(Map.get(report.data, "rules", nil)) rules: rules(Map.get(report.data, "rules", nil)),
assigned_account: assigned_account
} }
end end

View file

@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts) OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
end end
defp cast_and_validate(spec, operation, conn, content_type, false = _strict, cast_opts) do defp cast_and_validate(
spec,
operation,
%Conn{} = conn,
content_type,
false = _strict,
cast_opts
) do
case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do case OpenApiSpex.cast_and_validate(spec, operation, conn, content_type) do
{:ok, conn} -> {:ok, conn} ->
{:ok, conn} {:ok, conn}

View file

@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
:query, :query,
%Schema{type: :integer, default: 50}, %Schema{type: :integer, default: 50},
"Number number of log entries per page" "Number number of log entries per page"
),
Operation.parameter(
:assigned_account,
:query,
%Schema{type: :string},
"Filter by assigned account ID"
) )
| admin_api_params() | admin_api_params()
], ],
@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
} }
end end
def assign_account_operation do
%Operation{
tags: ["Report management"],
summary: "Assign account to specified reports",
operationId: "AdminAPI.ReportController.assign_account",
security: [%{"oAuth" => ["admin:write:reports"]}],
parameters: admin_api_params(),
requestBody: request_body("Parameters", assign_account_request(), required: true),
responses: %{
204 => no_content_response(),
400 => Operation.response("Bad Request", "application/json", update_400_response()),
403 => Operation.response("Forbidden", "application/json", ApiError)
}
}
end
def notes_create_operation do def notes_create_operation do
%Operation{ %Operation{
tags: ["Report management"], tags: ["Report management"],
@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
hint: %Schema{type: :string, nullable: true} hint: %Schema{type: :string, nullable: true}
} }
} }
} },
assigned_account:
account_admin()
|> Map.put(:nullable, true)
} }
} }
end end
@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
} }
end end
defp assign_account_request do
%Schema{
type: :object,
required: [:reports],
properties: %{
reports: %Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
id: %Schema{allOf: [FlakeID], description: "Required, report ID"},
assigned_account: %Schema{
type: :string,
description: "User nickname",
nullable: true
}
}
},
example: %{
"reports" => [
%{"id" => "123", "assigned_account" => "pleroma"}
]
}
}
}
}
end
defp update_400_response do defp update_400_response do
%Schema{ %Schema{
type: :array, type: :array,

View file

@ -342,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
max_pinned_statuses: %Schema{ max_pinned_statuses: %Schema{
type: :integer, type: :integer,
description: "The maximum number of pinned statuses for each account." description: "The maximum number of pinned statuses for each account."
},
max_profile_fields: %Schema{
type: :integer,
description: "The maximum number of custom profile fields allowed to be set."
},
profile_field_name_limit: %Schema{
type: :integer,
description: "The maximum size of a profile field name, in characters."
},
profile_field_value_limit: %Schema{
type: :integer,
description: "The maximum size of a profile field value, in characters."
} }
} }
}, },

View file

@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
summary: "Create a list", summary: "Create a list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.", description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.create", operationId: "ListController.create",
requestBody: create_update_request(), requestBody: create_request(),
security: [%{"oAuth" => ["write:lists"]}], security: [%{"oAuth" => ["write:lists"]}],
responses: %{ responses: %{
200 => Operation.response("List", "application/json", List), 200 => Operation.response("List", "application/json", List),
@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
description: "Change the title of a list", description: "Change the title of a list",
operationId: "ListController.update", operationId: "ListController.update",
parameters: [id_param()], parameters: [id_param()],
requestBody: create_update_request(), requestBody: update_request(),
security: [%{"oAuth" => ["write:lists"]}], security: [%{"oAuth" => ["write:lists"]}],
responses: %{ responses: %{
200 => Operation.response("List", "application/json", List), 200 => Operation.response("List", "application/json", List),
@ -164,14 +164,18 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
) )
end end
defp create_update_request do defp create_request do
request_body( request_body(
"Parameters", "Parameters",
%Schema{ %Schema{
description: "POST body for creating or updating a List", description: "POST body for creating a List",
type: :object, type: :object,
properties: %{ properties: %{
title: %Schema{type: :string, description: "List title"} title: %Schema{type: :string, description: "List title"},
exclusive: %Schema{
type: :boolean,
description: "Whether members of the list should be removed from the “Home” feed"
}
}, },
required: [:title] required: [:title]
}, },
@ -179,6 +183,24 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
) )
end end
defp update_request do
request_body(
"Parameters",
%Schema{
description: "PUT body for updating a List",
type: :object,
properties: %{
title: %Schema{type: :string, description: "List title"},
exclusive: %Schema{
type: :boolean,
description: "Whether members of the list should be removed from the “Home” feed"
}
}
},
required: true
)
end
defp add_remove_accounts_request(required) when is_boolean(required) do defp add_remove_accounts_request(required) when is_boolean(required) do
request_body( request_body(
"Parameters", "Parameters",

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.ApiError
@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
%Operation{ %Operation{
tags: ["Custom emojis"], tags: ["Custom emojis"],
summary: "List all custom emojis", summary: "List all custom emojis",
operationId: "UtilController.emoji", operationId: "PleromaAPI.UtilController.emoji",
parameters: [], parameters: [],
responses: %{ responses: %{
200 => 200 =>
@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
%Operation{ %Operation{
tags: ["Others"], tags: ["Others"],
summary: "Dump frontend configurations", summary: "Dump frontend configurations",
operationId: "UtilController.frontend_configurations", operationId: "PleromaAPI.UtilController.frontend_configurations",
parameters: [], parameters: [],
responses: %{ responses: %{
200 => 200 =>
@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Change account password", summary: "Change account password",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.change_password", operationId: "PleromaAPI.UtilController.change_password",
requestBody: request_body("Parameters", change_password_request(), required: true), requestBody: request_body("Parameters", change_password_request(), required: true),
responses: %{ responses: %{
200 => 200 =>
@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Change account email", summary: "Change account email",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.change_email", operationId: "PleromaAPI.UtilController.change_email",
requestBody: request_body("Parameters", change_email_request(), required: true), requestBody: request_body("Parameters", change_email_request(), required: true),
responses: %{ responses: %{
200 => 200 =>
@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Settings"], tags: ["Settings"],
summary: "Update Notification Settings", summary: "Update Notification Settings",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.update_notification_settings", operationId: "PleromaAPI.UtilController.update_notification_settings",
parameters: [ parameters: [
Operation.parameter( Operation.parameter(
:block_from_strangers, :block_from_strangers,
@ -173,7 +173,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Disable Account", summary: "Disable Account",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.disable_account", operationId: "PleromaAPI.UtilController.disable_account",
parameters: [ parameters: [
Operation.parameter(:password, :query, :string, "Password") Operation.parameter(:password, :query, :string, "Password")
], ],
@ -193,7 +193,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Delete Account", summary: "Delete Account",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.delete_account", operationId: "PleromaAPI.UtilController.delete_account",
parameters: [ parameters: [
Operation.parameter(:password, :query, :string, "Password") Operation.parameter(:password, :query, :string, "Password")
], ],
@ -212,7 +212,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
def captcha_operation do def captcha_operation do
%Operation{ %Operation{
summary: "Get a captcha", summary: "Get a captcha",
operationId: "UtilController.captcha", operationId: "PleromaAPI.UtilController.captcha",
tags: ["Others"], tags: ["Others"],
parameters: [], parameters: [],
responses: %{ responses: %{
@ -226,7 +226,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Move account", summary: "Move account",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.move_account", operationId: "PleromaAPI.UtilController.move_account",
requestBody: request_body("Parameters", move_account_request(), required: true), requestBody: request_body("Parameters", move_account_request(), required: true),
responses: %{ responses: %{
200 => 200 =>
@ -262,7 +262,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "List account aliases", summary: "List account aliases",
security: [%{"oAuth" => ["read:accounts"]}], security: [%{"oAuth" => ["read:accounts"]}],
operationId: "UtilController.list_aliases", operationId: "PleromaAPI.UtilController.list_aliases",
responses: %{ responses: %{
200 => 200 =>
Operation.response("Success", "application/json", %Schema{ Operation.response("Success", "application/json", %Schema{
@ -286,7 +286,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Add an alias to this account", summary: "Add an alias to this account",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.add_alias", operationId: "PleromaAPI.UtilController.add_alias",
requestBody: request_body("Parameters", add_alias_request(), required: true), requestBody: request_body("Parameters", add_alias_request(), required: true),
responses: %{ responses: %{
200 => 200 =>
@ -326,7 +326,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"], tags: ["Account credentials"],
summary: "Delete an alias from this account", summary: "Delete an alias from this account",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.delete_alias", operationId: "PleromaAPI.UtilController.delete_alias",
requestBody: request_body("Parameters", delete_alias_request(), required: true), requestBody: request_body("Parameters", delete_alias_request(), required: true),
responses: %{ responses: %{
200 => 200 =>
@ -366,7 +366,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Others"], tags: ["Others"],
summary: "Quick status check on the instance", summary: "Quick status check on the instance",
security: [%{"oAuth" => ["write:accounts"]}], security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.healthcheck", operationId: "PleromaAPI.UtilController.healthcheck",
parameters: [], parameters: [],
responses: %{ responses: %{
200 => Operation.response("Healthy", "application/json", %Schema{type: :object}), 200 => Operation.response("Healthy", "application/json", %Schema{type: :object}),
@ -376,52 +376,6 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
} }
end end
def remote_subscribe_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Remote Subscribe",
operationId: "UtilController.remote_subscribe",
parameters: [],
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
}
end
def remote_interaction_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Remote interaction",
operationId: "UtilController.remote_interaction",
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
responses: %{
200 =>
Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
}
}
end
defp remote_interaction_request do
%Schema{
title: "RemoteInteractionRequest",
description: "POST body for remote interaction",
type: :object,
required: [:ap_id, :profile],
properties: %{
ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
profile: %Schema{type: :string, description: "Remote profile webfinger"}
}
}
end
def show_subscribe_form_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Show remote subscribe form",
operationId: "UtilController.show_subscribe_form",
parameters: [],
responses: %{200 => Operation.response("Web Page", "test/html", %Schema{type: :string})}
}
end
defp delete_account_request do defp delete_account_request do
%Schema{ %Schema{
title: "AccountDeleteRequest", title: "AccountDeleteRequest",

View file

@ -0,0 +1,99 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.RemoteInteractionOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
import Pleroma.Web.ApiSpec.Helpers
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def remote_interaction_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Remote interaction",
operationId: "RemoteInteractionController.remote_interaction",
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
responses: %{
200 =>
Operation.response("Remote interaction URL", "application/json", %Schema{type: :object})
}
}
end
defp remote_interaction_request do
%Schema{
title: "RemoteInteractionRequest",
description: "POST body for remote interaction",
type: :object,
required: [:ap_id, :profile],
properties: %{
ap_id: %Schema{type: :string, description: "Profile or status ActivityPub ID"},
profile: %Schema{type: :string, description: "Remote profile webfinger"}
}
}
end
def follow_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Display follow form",
operationId: "RemoteInteractionController.follow",
parameters: [],
responses: %{
200 => Operation.response("Web Page", "text/html", %Schema{type: :string}),
302 => Operation.response("Redirect to the status page", nil, nil)
}
}
end
def do_follow_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Perform follow activity",
operationId: "RemoteInteractionController.do_follow",
parameters: [],
responses: %{
200 => Operation.response("Web page", "text/html", %Schema{type: :string}),
302 => Operation.response("Redirect to the account page", nil, nil)
}
}
end
def authorize_interaction_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Authorize remote interaction",
operationId: "RemoteInteractionController.authorize_interaction",
parameters: [],
responses: %{
302 => Operation.response("Redirect to remote_interaction path", nil, nil)
}
}
end
def show_subscribe_form_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Show remote subscribe form",
operationId: "RemoteInteractionController.show_subscribe_form",
parameters: [],
responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
}
end
def remote_subscribe_operation do
%Operation{
tags: ["Remote interaction"],
summary: "Remote Subscribe",
operationId: "RemoteInteractionController.remote_subscribe",
parameters: [],
responses: %{200 => Operation.response("Web Page", "text/html", %Schema{type: :string})}
}
end
end

View file

@ -17,11 +17,11 @@ defmodule Pleroma.Web.ApiSpec.RenderError do
def call(conn, errors) do def call(conn, errors) do
errors = errors =
Enum.map(errors, fn Enum.map(errors, fn
%{name: nil, reason: :invalid_enum} = err -> %OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err ->
%OpenApiSpex.Cast.Error{err | name: err.value} %{err | name: err.value}
%{name: nil} = err -> %OpenApiSpex.Cast.Error{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)} %{err | name: List.last(err.path)}
err -> err ->
err err

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
acct: %Schema{type: :string}, acct: %Schema{type: :string},
avatar_static: %Schema{type: :string, format: :uri}, avatar_static: %Schema{type: :string, format: :uri},
avatar: %Schema{type: :string, format: :uri}, avatar: %Schema{type: :string, format: :uri},
avatar_description: %Schema{type: :string},
bot: %Schema{type: :boolean}, bot: %Schema{type: :boolean},
created_at: %Schema{type: :string, format: "date-time"}, created_at: %Schema{type: :string, format: "date-time"},
display_name: %Schema{type: :string}, display_name: %Schema{type: :string},
@ -31,6 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
following_count: %Schema{type: :integer}, following_count: %Schema{type: :integer},
header_static: %Schema{type: :string, format: :uri}, header_static: %Schema{type: :string, format: :uri},
header: %Schema{type: :string, format: :uri}, header: %Schema{type: :string, format: :uri},
header_description: %Schema{type: :string},
id: FlakeID, id: FlakeID,
locked: %Schema{type: :boolean}, locked: %Schema{type: :boolean},
note: %Schema{type: :string, format: :html}, note: %Schema{type: :string, format: :html},
@ -111,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
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}, avatar_description: %Schema{type: :string, deprecated: true},
header_description: %Schema{type: :string} header_description: %Schema{type: :string, deprecated: true}
} }
}, },
source: %Schema{ source: %Schema{

View file

@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
requested: %Schema{type: :boolean}, requested: %Schema{type: :boolean},
showing_reblogs: %Schema{type: :boolean}, showing_reblogs: %Schema{type: :boolean},
subscribing: %Schema{type: :boolean}, subscribing: %Schema{type: :boolean},
notifying: %Schema{type: :boolean} notifying: %Schema{type: :boolean},
mute_expires_at: %Schema{type: :string, format: "date-time", nullable: true},
block_expires_at: %Schema{type: :string, format: "date-time", nullable: true}
}, },
example: %{ example: %{
"blocked_by" => false, "blocked_by" => false,

View file

@ -15,12 +15,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
properties: %{ properties: %{
id: FlakeID, id: FlakeID,
name: %Schema{type: :string, description: "Folder name"}, name: %Schema{type: :string, description: "Folder name"},
emoji: %Schema{type: :string, description: "Folder emoji", nullable: true} emoji: %Schema{type: :string, description: "Folder emoji", nullable: true},
emoji_url: %Schema{
type: :string,
description: "URL of the folder emoji if it's a custom emoji, null otherwise",
nullable: true
}
}, },
example: %{ example: %{
"id" => "9toJCu5YZW7O7gfvH6", "id" => "9toJCu5YZW7O7gfvH6",
"name" => "Read later", "name" => "Read later",
"emoji" => nil "emoji" => nil,
"emoji_url" => nil
} }
}) })
end end

View file

@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do
type: :object, type: :object,
properties: %{ properties: %{
id: %Schema{type: :string, description: "The internal database ID of the list"}, id: %Schema{type: :string, description: "The internal database ID of the list"},
title: %Schema{type: :string, description: "The user-defined title of the list"} title: %Schema{type: :string, description: "The user-defined title of the list"},
exclusive: %Schema{
type: :boolean,
description: "Whether members of the list should be removed from the “Home” feed"
}
}, },
example: %{ example: %{
"id" => "12249", "id" => "12249",

View file

@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI 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),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)}, {_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params), visibility = announce_visibility(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public), {:ok, announce, _} <- Builder.announce(user, object, visibility: visibility),
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do {:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
{:ok, activity} {:ok, activity}
else else
@ -407,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
defp public_announce?(_, %{visibility: visibility}) def announce_visibility(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct}, when visibility in ~w{public unlisted private direct local},
do: visibility in ~w(public unlisted) do: visibility
defp public_announce?(object, _) do def announce_visibility(object, _), do: Visibility.get_visibility(object)
Visibility.public?(object)
end
@spec get_visibility(map(), map() | nil, Participation.t() | nil) :: @spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
{String.t() | nil, String.t() | nil} {String.t() | nil, String.t() | nil}
@ -709,6 +707,22 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def assign_report_to_account(activity_ids, user) when is_list(activity_ids) do
case Utils.assign_report_to_account(activity_ids, user) do
:ok -> {:ok, activity_ids}
_ -> {:error, dgettext("errors", "Could not assign account")}
end
end
def assign_report_to_account(activity_id, user) do
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
Utils.assign_report_to_account(activity, user)
else
nil -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not assign account")}
end
end
@spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()} @spec update_activity_scope(String.t(), map()) :: {:ok, any()} | {:error, any()}
def update_activity_scope(activity_id, opts \\ %{}) do def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id), with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),

View file

@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> validate() |> validate()
end end
defp listen_object(draft) do defp listen_object(%__MODULE__{} = draft) do
object = object =
draft.params draft.params
|> Map.take([:album, :artist, :title, :length]) |> Map.take([:album, :artist, :title, :length])
@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("cc", draft.cc) |> Map.put("cc", draft.cc)
|> Map.put("actor", draft.user.ap_id) |> Map.put("actor", draft.user.ap_id)
%__MODULE__{draft | object: object} %{draft | object: object}
end end
defp put_params(draft, params) do defp put_params(%__MODULE__{} = draft, params) do
params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id]) params = Map.put_new(params, :in_reply_to_status_id, params[:in_reply_to_id])
%__MODULE__{draft | params: params} %{draft | params: params}
end end
defp status(%{params: %{status: status}} = draft) do defp status(%__MODULE__{params: %{status: status}} = draft) do
%__MODULE__{draft | status: String.trim(status)} %{draft | status: String.trim(status)}
end end
defp summary(%{params: params} = draft) do defp summary(%__MODULE__{params: params} = draft) do
%__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")} %{draft | summary: Map.get(params, :spoiler_text, "")}
end end
defp full_payload(%{status: status, summary: summary} = draft) do defp full_payload(%__MODULE__{status: status, summary: summary} = draft) do
full_payload = String.trim(status <> summary) full_payload = String.trim(status <> summary)
case Utils.validate_character_limit(full_payload, draft.attachments) do case Utils.validate_character_limit(full_payload, draft.attachments) do
:ok -> %__MODULE__{draft | full_payload: full_payload} :ok -> %{draft | full_payload: full_payload}
{:error, message} -> add_error(draft, message) {:error, message} -> add_error(draft, message)
end end
end end
defp attachments(%{params: params} = draft) do defp attachments(%__MODULE__{params: params} = draft) do
attachments = Utils.attachments_from_ids(params, draft.user) attachments = Utils.attachments_from_ids(params, draft.user)
draft = %__MODULE__{draft | attachments: attachments} draft = %{draft | attachments: attachments}
case Utils.validate_attachments_count(attachments) do case Utils.validate_attachments_count(attachments) do
:ok -> draft :ok -> draft
@ -134,9 +134,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end end
end end
defp in_reply_to(%{params: %{in_reply_to_status_id: ""}} = draft), do: draft defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: ""}} = draft), do: draft
defp in_reply_to(%{params: %{in_reply_to_status_id: id}} = draft) when is_binary(id) do defp in_reply_to(%__MODULE__{params: %{in_reply_to_status_id: id}} = draft)
when is_binary(id) do
# If a post was deleted all its activities (except the newly added Delete) are purged too, # If a post was deleted all its activities (except the newly added Delete) are purged too,
# thus lookup by Create db ID will yield nil just as if it never existed in the first place. # thus lookup by Create db ID will yield nil just as if it never existed in the first place.
# #
@ -148,7 +149,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
with %Activity{} = activity <- Activity.get_by_id(id), with %Activity{} = activity <- Activity.get_by_id(id),
true <- Visibility.visible_for_user?(activity, draft.user), true <- Visibility.visible_for_user?(activity, draft.user),
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do {_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
%__MODULE__{draft | in_reply_to: activity} %{draft | in_reply_to: activity}
else else
nil -> nil ->
add_error(draft, dgettext("errors", "Cannot reply to a deleted status")) add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
@ -166,40 +167,43 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end end
end end
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do defp in_reply_to(
%__MODULE__{draft | in_reply_to: in_reply_to} %__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft
) do
%{draft | in_reply_to: in_reply_to}
end end
defp in_reply_to(draft), do: draft defp in_reply_to(draft), do: draft
defp quote_post(%{params: %{quoted_status_id: id}} = draft) when not_empty_string(id) do defp quote_post(%__MODULE__{params: %{quoted_status_id: id}} = draft)
when not_empty_string(id) do
case Activity.get_by_id_with_object(id) do case Activity.get_by_id_with_object(id) do
%Activity{} = activity -> %Activity{} = activity ->
%__MODULE__{draft | quote_post: activity} %{draft | quote_post: activity}
_ -> _ ->
draft draft
end end
end end
defp quote_post(%{params: %{quote_id: id}} = draft) when not_empty_string(id) do defp quote_post(%__MODULE__{params: %{quote_id: id}} = draft) when not_empty_string(id) do
quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)}) quote_post(%{draft | params: Map.put(draft.params, :quoted_status_id, id)})
end end
defp quote_post(draft), do: draft defp quote_post(draft), do: draft
defp in_reply_to_conversation(draft) do defp in_reply_to_conversation(%__MODULE__{} = draft) do
in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id]) in_reply_to_conversation = Participation.get(draft.params[:in_reply_to_conversation_id])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} %{draft | in_reply_to_conversation: in_reply_to_conversation}
end end
defp visibility(%{params: params} = draft) do defp visibility(%__MODULE__{params: params} = draft) do
case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do case CommonAPI.get_visibility(params, draft.in_reply_to, draft.in_reply_to_conversation) do
{visibility, "direct"} when visibility != "direct" -> {visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct")) add_error(draft, dgettext("errors", "The message visibility must be direct"))
{visibility, _} -> {visibility, _} ->
%__MODULE__{draft | visibility: visibility} %{draft | visibility: visibility}
end end
end end
@ -215,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
false false
end end
defp quoting_visibility(%{quote_post: %Activity{}} = draft) do defp quoting_visibility(%__MODULE__{quote_post: %Activity{}} = draft) do
with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false), with %Object{} = object <- Object.normalize(draft.quote_post, fetch: false),
true <- can_quote?(draft, object, Visibility.get_visibility(object)) do true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
draft draft
@ -226,24 +230,24 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
defp quoting_visibility(draft), do: draft defp quoting_visibility(draft), do: draft
defp expires_at(draft) do defp expires_at(%__MODULE__{} = draft) do
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at} {:ok, expires_at} -> %{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message) {:error, message} -> add_error(draft, message)
end end
end end
defp poll(draft) do defp poll(%__MODULE__{} = draft) do
case Utils.make_poll_data(draft.params) do case Utils.make_poll_data(draft.params) do
{:ok, {poll, poll_emoji}} -> {:ok, {poll, poll_emoji}} ->
%__MODULE__{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)} %{draft | extra: poll, emoji: Map.merge(draft.emoji, poll_emoji)}
{:error, message} -> {:error, message} ->
add_error(draft, message) add_error(draft, message)
end end
end end
defp content(%{mentions: mentions} = draft) do defp content(%__MODULE__{mentions: mentions} = draft) do
{content_html, mentioned_users, tags} = Utils.make_content_html(draft) {content_html, mentioned_users, tags} = Utils.make_content_html(draft)
mentioned_ap_ids = mentioned_ap_ids =
@ -254,25 +258,25 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Kernel.++(mentioned_ap_ids) |> Kernel.++(mentioned_ap_ids)
|> Utils.get_addressed_users(draft.params[:to]) |> Utils.get_addressed_users(draft.params[:to])
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags} %{draft | content_html: content_html, mentions: mentions, tags: tags}
end end
defp to_and_cc(draft) do defp to_and_cc(%__MODULE__{} = draft) do
{to, cc} = Utils.get_to_and_cc(draft) {to, cc} = Utils.get_to_and_cc(draft)
%__MODULE__{draft | to: to, cc: cc} %{draft | to: to, cc: cc}
end end
defp context(draft) do defp context(%__MODULE__{} = draft) do
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation) context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
%__MODULE__{draft | context: context} %{draft | context: context}
end end
defp sensitive(draft) do defp sensitive(%__MODULE__{} = draft) do
sensitive = draft.params[:sensitive] sensitive = draft.params[:sensitive]
%__MODULE__{draft | sensitive: sensitive} %{draft | sensitive: sensitive}
end end
defp language(draft) do defp language(%__MODULE__{} = draft) do
language = language =
with language <- draft.params[:language], with language <- draft.params[:language],
true <- good_locale_code?(language) do true <- good_locale_code?(language) do
@ -281,10 +285,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
_ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary) _ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
end end
%__MODULE__{draft | language: language} %{draft | language: language}
end end
defp object(draft) do defp object(%__MODULE__{} = draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji) emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
# Sometimes people create posts with subject containing emoji, # Sometimes people create posts with subject containing emoji,
@ -325,15 +329,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("generator", draft.params[:generator]) |> Map.put("generator", draft.params[:generator])
|> Map.put("language", draft.language) |> Map.put("language", draft.language)
%__MODULE__{draft | object: object} %{draft | object: object}
end end
defp preview?(draft) do defp preview?(%__MODULE__{} = draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview]) preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%__MODULE__{draft | preview?: preview?} %{draft | preview?: preview?}
end end
defp changes(draft) do defp changes(%__MODULE__{} = draft) do
direct? = draft.visibility == "direct" direct? = draft.visibility == "direct"
additional = %{"cc" => draft.cc, "directMessage" => direct?} additional = %{"cc" => draft.cc, "directMessage" => direct?}
@ -353,14 +357,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
} }
|> Utils.maybe_add_list_data(draft.user, draft.visibility) |> Utils.maybe_add_list_data(draft.user, draft.visibility)
%__MODULE__{draft | changes: changes} %{draft | changes: changes}
end end
defp with_valid(%{valid?: true} = draft, func), do: func.(draft) defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft defp with_valid(draft, _func), do: draft
defp add_error(draft, message) do defp add_error(%__MODULE__{} = draft, message) do
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]} %{draft | valid?: false, errors: [message | draft.errors]}
end end
defp validate(%{valid?: true} = draft), do: {:ok, draft} defp validate(%{valid?: true} = draft), do: {:ok, draft}

View file

@ -75,48 +75,70 @@ defmodule Pleroma.Web.CommonAPI.Utils do
{Enum.map(participation.recipients, & &1.ap_id), []} {Enum.map(participation.recipients, & &1.ap_id), []}
end end
def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do def get_to_and_cc(%{visibility: visibility} = draft) do
to = # If the OP is a DM already, add the implicit actor
case visibility do mentions =
"public" -> [Pleroma.Constants.as_public() | draft.mentions] if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
"local" -> [Utils.as_local_public() | draft.mentions] Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions])
else
draft.mentions
end end
cc = [draft.user.follower_address] get_to_and_cc_for_visibility(
visibility,
if draft.in_reply_to do draft.user.follower_address,
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} draft.in_reply_to && draft.in_reply_to.data["actor"],
else mentions
{to, cc} )
end
end end
def get_to_and_cc(%{visibility: "unlisted"} = draft) do def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do
to = [draft.user.follower_address | draft.mentions] scope_addr = Pleroma.Constants.as_public()
cc = [Pleroma.Constants.as_public()]
if draft.in_reply_to do to =
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc} if parent_actor,
else do: Enum.uniq([parent_actor, scope_addr | mentions]),
{to, cc} else: [scope_addr | mentions]
end
{to, [follower_collection]}
end end
def get_to_and_cc(%{visibility: "private"} = draft) do def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do
{to, cc} = get_to_and_cc(struct(draft, visibility: "direct")) recipients =
{[draft.user.follower_address | to], cc} if parent_actor,
do: Enum.uniq([parent_actor | mentions]),
else: mentions
to = [
Utils.as_local_public()
| Enum.filter(recipients, fn addr ->
String.starts_with?(addr, Pleroma.Web.Endpoint.url() <> "/")
end)
]
{to, [follower_collection]}
end end
def get_to_and_cc(%{visibility: "direct"} = draft) do def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do
# If the OP is a DM already, add the implicit actor. to =
if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do if parent_actor,
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []} do: Enum.uniq([parent_actor, follower_collection | mentions]),
else else: [follower_collection | mentions]
{draft.mentions, []}
end {to, [Pleroma.Constants.as_public()]}
end end
def get_to_and_cc(%{visibility: {:list, _}, mentions: mentions}), do: {mentions, []} def get_to_and_cc_for_visibility("private", follower_collection, _, mentions) do
{[follower_collection | mentions], []}
end
def get_to_and_cc_for_visibility("direct", _, _, mentions) do
{mentions, []}
end
def get_to_and_cc_for_visibility({:list, _}, _, _, mentions) do
{mentions, []}
end
def get_addressed_users(_, to) when is_list(to) do def get_addressed_users(_, to) when is_list(to) do
User.get_ap_ids_by_nicknames(to) User.get_ap_ids_by_nicknames(to)

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