Merge develop into Phoenix upstream migration

This commit is contained in:
Lain Soykaf 2026-05-11 22:13:46 +04:00
commit 216a00f73f
No known key found for this signature in database
222 changed files with 7177 additions and 1890 deletions

View file

@ -234,6 +234,61 @@ defmodule Mix.Tasks.Pleroma.Config do
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()
def migrate_to_db(file_path \\ nil) 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, "ALTER SEQUENCE config_id_seq RESTART;")
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

View file

@ -226,7 +226,12 @@ defmodule Mix.Tasks.Pleroma.Database do
DELETE FROM hashtags AS ht
WHERE NOT EXISTS (
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()

View file

@ -22,7 +22,7 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do
else
{_, 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"
end

View file

@ -72,7 +72,7 @@ defmodule Mix.Tasks.Pleroma.Search.Meilisearch do
query,
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.chunk_every(chunk_size)
|> Stream.transform(0, fn objects, acc ->

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Activity.HTML do
def invalidate_cache_for(activity_id) do
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)
end

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Bookmark do
schema "bookmarks" do
belongs_to(:user, User, 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()
end
@ -38,7 +38,7 @@ defmodule Pleroma.Bookmark do
|> validate_required([:user_id, :activity_id])
|> unique_constraint(:activity_id, name: :bookmarks_user_id_activity_id_index)
|> 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]
)
end
@ -76,11 +76,4 @@ defmodule Pleroma.Bookmark do
|> Repo.one()
|> Repo.delete()
end
def set_folder(bookmark, folder_id) do
bookmark
|> cast(%{folder_id: folder_id}, [:folder_id])
|> validate_required([:folder_id])
|> Repo.update()
end
end

View file

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

View file

@ -10,6 +10,7 @@ defmodule Pleroma.ConfigDB do
import Pleroma.Web.Gettext
alias __MODULE__
alias Pleroma.EctoType.Config.RateLimit
alias Pleroma.Repo
@type t :: %__MODULE__{}
@ -60,8 +61,59 @@ defmodule Pleroma.ConfigDB do
|> cast(params, [:key, :group, :value])
|> validate_required([:key, :group, :value])
|> unique_constraint(:key, name: :config_group_key_index)
|> validate_rate_limit()
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
%ConfigDB{}
|> changeset(params)

View file

@ -22,13 +22,14 @@ defmodule Pleroma.Constants do
"generator",
"rules",
"language",
"voters"
"voters",
"assigned_account"
]
)
const(static_only_files,
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,

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

@ -127,6 +127,13 @@ defmodule Pleroma.Formatter do
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
end
def markdown_to_html(text, opts) do
Earmark.as_html!(
text,
%Earmark.Options{compact_output: true, smartypants: false} |> Map.merge(opts)
)
end
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
@ -135,6 +142,10 @@ defmodule Pleroma.Formatter do
HTML.filter_tags(text)
end
def html_escape(text, "text/x.misskeymarkdown") do
HTML.filter_tags(text)
end
def html_escape(text, "text/plain") do
Regex.split(@link_regex, text, include_captures: true)
|> Enum.map_every(2, fn chunk ->

View file

@ -75,8 +75,8 @@ defmodule Pleroma.Frontend do
end
defp download_build(frontend_info, dest) do
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]}")
url = String.replace(frontend_info["build_url"], "${ref}", frontend_info["ref"])
Logger.info("Downloading pre-built bundle for #{frontend_info["name"]} from #{url}")
with {:ok, %{status: 200, body: zip_body}} <-
Pleroma.HTTP.get(url, [], pool: :media, recv_timeout: 120_000) do

View file

@ -21,10 +21,13 @@ defmodule Pleroma.Gopher.Server do
def init([ip, port]) do
Logger.info("Starting gopher server on #{port}")
Process.flag(:trap_exit, true)
listener = :gopher
{:ok, _pid} =
:ranch.start_listener(
:gopher,
listener,
:ranch_tcp,
%{
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

View file

@ -6,8 +6,9 @@ defmodule Pleroma.HTTP.AdapterHelper.Hackney do
@behaviour Pleroma.HTTP.AdapterHelper
@defaults [
follow_redirect: true,
force_redirect: true
follow_redirect: false,
force_redirect: false,
with_body: true
]
@spec options(keyword(), URI.t()) :: keyword()

View file

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

View file

@ -78,7 +78,7 @@ defmodule Pleroma.Marker do
defp get_marker(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}
end
end

View file

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

View file

@ -132,11 +132,18 @@ defmodule Pleroma.ModerationLog do
end
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 =
attrs
|> prepare_log_data
|> 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)})
insert_log_entry_with_message(%ModerationLog{data: data})
@ -441,6 +448,35 @@ defmodule Pleroma.ModerationLog do
" with '#{state}' state"
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(
%ModerationLog{
data: %{

View file

@ -372,12 +372,28 @@ defmodule Pleroma.Object do
option
end)
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
existing_voters = object.data["voters"] || []
voters = [actor | existing_voters] |> Enum.uniq()
new_voter? = actor not in existing_voters
existing_voters_count = object.data["votersCount"]
voters_count =
cond do
is_integer(existing_voters_count) and new_voter? ->
existing_voters_count + 1
is_integer(existing_voters_count) ->
existing_voters_count
true ->
length(voters)
end
data =
object.data
|> Map.put(key, options)
|> Map.put("voters", voters)
|> Map.put("votersCount", voters_count)
object
|> Object.change(%{data: data})

View file

@ -5,7 +5,10 @@
defmodule Pleroma.ReleaseTasks do
@repo Pleroma.Repo
def run(args) do
# TODO: Kept for some backwards compatibility with buggy pleroma_ctl,
# if a mismatch between pleroma_ctl and Pleroma accidentaly happens.
# Remove in the future.
def run(args) when is_binary(args) do
[task | args] = String.split(args)
case task do
@ -16,6 +19,20 @@ defmodule Pleroma.ReleaseTasks do
end
end
# HACK: Script arguments need to be received as a list, otherwise (quoted) arguments with
# whitespace will be broken. Previously the broken string form above was used,
# escaping in the shell does not help.
def run(args) when is_list(args) do
[task | args] = args
case task do
"migrate" -> migrate(args)
"create" -> create()
"rollback" -> rollback(args)
task -> mix_task(task, args)
end
end
def find_module(task) do
module_name =
task

View file

@ -12,7 +12,7 @@ defmodule Pleroma.ReverseProxy do
@keep_resp_headers @resp_cache_headers ++
~w(content-length content-type content-disposition content-encoding) ++
~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]
@max_read_duration :timer.seconds(30)
@max_body_length :infinity

View file

@ -4,6 +4,26 @@
defmodule Pleroma.ReverseProxy.Client.Hackney do
@behaviour Pleroma.ReverseProxy.Client
@redirect_limit 5
require Logger
# 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/273 (redirects not followed when using HTTP proxy)
#
# Based on a redirect handler from Pleb, slightly modified to work with Hackney:
# https://declin.eu/objects/d4f38e62-5429-4614-86d1-e8fc16e6bf33
@redirect_statuses [301, 302, 303, 307, 308]
defp absolute_redirect_url(original_url, resp_headers) do
location =
Enum.find(resp_headers, fn {header, _location} ->
String.downcase(header) == "location"
end)
URI.merge(original_url, elem(location, 1))
|> URI.to_string()
end
@impl true
def request(method, url, headers, body, opts \\ []) do
@ -12,7 +32,24 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
path
end)
:hackney.request(method, url, headers, body, opts)
if opts[:follow_redirect] != false do
{_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)
case res do
{:ok, code, resp_headers, _client} when code in @redirect_statuses ->
redirect(url, resp_headers, env, @redirect_limit)
{:ok, code, resp_headers} when code in @redirect_statuses ->
redirect(url, resp_headers, env, @redirect_limit)
_ ->
res
end
else
:hackney.request(method, url, headers, body, opts)
end
end
@impl true
@ -26,4 +63,32 @@ defmodule Pleroma.ReverseProxy.Client.Hackney do
@impl true
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

View file

@ -1,11 +1,28 @@
defmodule Pleroma.Search do
alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Workers.SearchIndexingWorker
def add_to_index(%Pleroma.Activity{id: activity_id}) do
SearchIndexingWorker.new(%{"op" => "add_to_index", "activity" => activity_id})
|> Oban.insert()
@spec add_to_index(Activity.t()) :: {:ok, Oban.Job.t() | :noop} | {:error, Oban.Job.changeset()}
def add_to_index(%Activity{id: activity_id, object: %Object{} = object} = activity) do
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
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
SearchIndexingWorker.new(%{"op" => "remove_from_index", "object" => object_id})
|> Oban.insert()
@ -20,4 +37,44 @@ defmodule Pleroma.Search do
search_module = Pleroma.Config.get([Pleroma.Search, :module])
search_module.healthcheck_endpoints()
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

View file

@ -4,6 +4,8 @@ defmodule Pleroma.Search.Meilisearch do
alias Pleroma.Activity
alias Pleroma.Config.Getting, as: Config
alias Pleroma.Object
alias Pleroma.Search
import Pleroma.Search.DatabaseSearch
import Ecto.Query
@ -118,66 +120,24 @@ defmodule Pleroma.Search.Meilisearch do
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
def add_to_index(activity) do
maybe_search_data = object_to_search_data(activity.object)
def add_to_index(%Activity{object: %Object{} = object} = activity) do
search_data = Search.object_to_search_data(object)
if activity.data["type"] == "Create" and maybe_search_data do
result =
meili_put(
"/indexes/objects/documents",
[maybe_search_data]
)
result =
meili_put(
"/indexes/objects/documents",
[search_data]
)
with {:ok, %{"status" => "enqueued"}} <- result do
# 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
with {:ok, %{"status" => "enqueued"}} <- result do
# 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
end

View file

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

View file

@ -93,7 +93,7 @@ defmodule Pleroma.Upload do
def store(upload, opts \\ []) do
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}"},
{:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload),
description = get_description(upload),

View file

@ -342,7 +342,7 @@ defmodule Pleroma.User.Backup do
dir,
"outbox",
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")}
end
end

View file

@ -45,7 +45,7 @@ defmodule Pleroma.UserRelationship do
do: exists?(unquote(relationship_type), source, target)
# `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 unquote(:"get_#{relationship_type}_expire_date")(source, target),
do: get_expire_date(unquote(relationship_type), source, target)

View file

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

View file

@ -1003,6 +1003,14 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
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
from(
[_activity, object] in query,
@ -1471,6 +1479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_actor(opts)
|> restrict_type(opts)
|> restrict_state(opts)
|> restrict_assigned_account(opts)
|> restrict_favorited_by(opts)
|> restrict_blocked(restrict_blocked_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(_), 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
Map.put(map, "name", description)
end
@ -1664,44 +1677,80 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
show_birthday = !!birthday
# if WebFinger request was already done, we probably have acct, otherwise
# we request WebFinger here
nickname = additional[:nickname_from_acct] || generate_nickname(data)
with {:ok, nickname} <- nickname_from_actor(data, additional) do
{:ok,
%{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: normalize_also_known_as(data["alsoKnownAs"]),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
birthday: birthday,
show_birthday: show_birthday,
pinned_objects: pinned_objects,
nickname: nickname
}}
end
end
%{
ap_id: data["id"],
uri: get_actor_url(data["url"]),
banner: normalize_image(data["image"]),
fields: fields,
emoji: emojis,
is_locked: is_locked,
is_discoverable: is_discoverable,
invisible: invisible,
avatar: normalize_image(data["icon"]),
name: data["name"],
follower_address: data["followers"],
following_address: data["following"],
featured_address: featured_address,
bio: data["summary"] || "",
actor_type: actor_type,
also_known_as: Map.get(data, "alsoKnownAs", []),
public_key: public_key,
inbox: data["inbox"],
shared_inbox: shared_inbox,
accepts_chat_messages: accepts_chat_messages,
birthday: birthday,
show_birthday: show_birthday,
pinned_objects: pinned_objects,
nickname: nickname
}
defp nickname_from_actor(data, additional) do
generated = generated_nickname(data)
case additional[:nickname_from_acct] do
^generated when is_binary(generated) ->
{:ok, generated}
acct when is_binary(acct) ->
with ^acct <- webfinger_nickname(data) do
{:ok, acct}
else
_ -> {:error, {:webfinger_actor_mismatch, acct, data["id"]}}
end
_ ->
{:ok, generate_nickname(data)}
end
end
defp generated_nickname(%{"preferredUsername" => username, "id" => ap_id})
when is_binary(username) and is_binary(ap_id) do
case URI.parse(ap_id) do
%URI{host: host} when is_binary(host) -> "#{username}@#{host}"
_ -> nil
end
end
defp generated_nickname(_), do: nil
defp webfinger_nickname(data) do
with generated when is_binary(generated) <- generated_nickname(data),
{:ok, %{"subject" => "acct:" <> acct, "ap_id" => ap_id}} <- WebFinger.finger(generated),
true <- ap_id == data["id"] do
acct
end
end
defp generate_nickname(%{"preferredUsername" => username} = data) when is_binary(username) do
generated = "#{username}@#{URI.parse(data["id"]).host}"
generated = generated_nickname(data)
if Config.get([WebFinger, :update_nickname_on_user_fetch]) do
case WebFinger.finger(generated) do
{:ok, %{"subject" => "acct:" <> acct}} -> acct
case webfinger_nickname(data) do
acct when is_binary(acct) -> acct
_ -> generated
end
else
@ -1781,9 +1830,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data, additional \\ []) do
with {:ok, data} <- MRF.filter(data) do
{:ok, object_to_user_data(data, additional)}
with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data, additional) do
{:ok, data}
else
{:error, _} = e -> e
e -> {:error, e}
end
end

View file

@ -348,7 +348,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
def inbox(%{assigns: %{valid_signature: false}} = conn, params) do
Federator.incoming_ap_doc(%{
Federator.incoming_failed_signature_ap_doc(%{
method: conn.method,
req_headers: conn.req_headers,
request_path: conn.request_path,

View file

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

View file

@ -27,7 +27,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do
end
defp fetch(url) do
http_client_opts = Pleroma.Config.get([:media_proxy, :proxy_opts, :http], pool: :media)
# This module uses Tesla (Pleroma.HTTP) to fetch the MediaProxy URL.
# Redirect following is handled by Tesla middleware, so we must not enable
# adapter-level redirect logic (Hackney can crash on relative redirects when proxied).
http_client_opts =
[:media_proxy, :proxy_opts, :http]
|> Pleroma.Config.get(pool: :media)
|> Keyword.drop([:follow_redirect, :force_redirect])
HTTP.get(url, [], http_client_opts)
end

View file

@ -6,9 +6,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
use Ecto.Schema
alias Pleroma.EctoType.ActivityPub.ObjectValidators
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI.Utils
import Ecto.Changeset
@ -26,6 +29,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
end
field(:replies, {:array, ObjectValidators.ObjectID}, default: [])
field(:source, :map)
end
def cast_and_apply(data) do
@ -80,6 +84,113 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
def fix_attachments(data), do: data
defp remote_mention_resolver(
%{"id" => ap_id, "tag" => tags},
"@" <> nickname = mention,
buffer,
opts,
acc
)
when is_binary(ap_id) and is_list(tags) do
initial_host =
ap_id
|> URI.parse()
|> Map.get(:host)
with mention_tag when not is_nil(mention_tag) <-
Enum.find(tags, &mention_tag?(&1, mention, initial_host)),
href when is_binary(href) <- mention_tag["href"],
%User{} = user <- User.get_cached_by_ap_id(href) do
link = Pleroma.Formatter.mention_from_user(user, opts)
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
else
_ -> {buffer, acc}
end
end
defp remote_mention_resolver(_object, _mention, buffer, _opts, acc), do: {buffer, acc}
defp mention_tag?(%{"type" => "Mention", "name" => name}, mention, initial_host)
when is_binary(name) do
name == mention || mention == "#{name}@#{initial_host}"
end
defp mention_tag?(_tag, _mention, _initial_host), do: false
defp scrub_content(%{"content" => content} = object) when is_binary(content) do
Map.put(object, "content", HTML.filter_tags(content))
end
defp scrub_content(object), do: object
defp mfm_parse_limit do
min(Pleroma.Config.get([:instance, :limit]), Pleroma.Config.get([:instance, :remote_limit]))
end
defp normalize_source(%{"source" => source} = object) when is_binary(source) do
object
|> Map.put("source", %{"content" => source})
|> normalize_source()
end
defp normalize_source(%{"source" => source} = object) when is_map(source) do
source =
case source["content"] do
content when is_binary(content) ->
if String.length(content) <= mfm_parse_limit() do
source
else
Map.delete(source, "content")
end
nil ->
source
_ ->
Map.delete(source, "content")
end
Map.put(object, "source", source)
end
defp normalize_source(object), do: object
defp fix_misskey_content(%{"htmlMfm" => true, "content" => content} = object)
when is_binary(content) do
Map.put(object, "content", HTML.filter_tags(content))
end
defp fix_misskey_content(%{"htmlMfm" => true} = object), do: object
defp fix_misskey_content(
%{"source" => %{"mediaType" => "text/x.misskeymarkdown", "content" => content}} = object
)
when is_binary(content) do
mention_handler = fn nick, buffer, opts, acc ->
remote_mention_resolver(object, nick, buffer, opts, acc)
end
{linked, _mentions, _tags} =
Utils.format_input(content, "text/x.misskeymarkdown", mention_handler: mention_handler)
Map.put(object, "content", linked)
end
defp fix_misskey_content(%{"source" => %{"mediaType" => "text/x.misskeymarkdown"}} = object),
do: scrub_content(object)
defp fix_misskey_content(%{"_misskey_content" => content} = object) when is_binary(content) do
object
|> Map.put("source", %{
"content" => content,
"mediaType" => "text/x.misskeymarkdown"
})
|> Map.delete("_misskey_content")
|> fix_misskey_content()
end
defp fix_misskey_content(object), do: object
defp fix(data) do
data
|> CommonFixes.fix_actor()
@ -88,6 +199,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|> fix_tag()
|> fix_replies()
|> fix_attachments()
|> normalize_source()
|> fix_misskey_content()
|> CommonFixes.fix_quote_url()
|> CommonFixes.fix_likes()
|> Transmogrifier.fix_emoji()

View file

@ -32,6 +32,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
quote bind_quoted: binding() do
field(:content, :string)
field(:contentMap, ObjectValidators.ContentLanguageMap)
field(:htmlMfm, :boolean)
field(:published, ObjectValidators.DateTime)
field(:updated, ObjectValidators.DateTime)

View file

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

View file

@ -75,15 +75,40 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator do
end
end
# For remote Updates, verify the host is the same.
# For remote Updates, verify the Actor is the same
def validate_updating_rights_remote(cng) do
with actor = get_field(cng, :actor),
object = get_field(cng, :object),
{:ok, object_id} <- ObjectValidators.ObjectID.cast(object),
actor_uri <- URI.parse(actor),
object_uri <- URI.parse(object_id),
true <- actor_uri.host == object_uri.host do
cng
entity <-
Object.normalize(object_id, fetch: false) || User.get_cached_by_ap_id(object_id) do
case entity do
# Actor must own Object to update it
%Object{} ->
if actor == entity.data["actor"] do
cng
else
cng
|> add_error(:object, "Can't be updated by this actor")
end
# Actor must only be allowed to update itself
%User{} ->
if actor == entity.ap_id do
cng
else
cng
|> add_error(:object, "Can't be updated by this actor")
end
nil ->
cng
|> add_error(:object, "Can't be updated by this actor")
_ ->
cng
|> add_error(:object, "Update is neither for Object or Actor")
end
else
_e ->
cng

View file

@ -79,7 +79,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Determine if an activity can be represented by running it through Transmogrifier.
"""
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
else
_e ->
@ -102,14 +102,14 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
Logger.debug("Federating #{ap_id} to #{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} =
with {_, false} <- {:actor_changed?, data["actor"] != activity.data["actor"]} do
{actor, data}
else
{: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, data}
end

View file

@ -783,7 +783,13 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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
object
|> add_hashtags
@ -795,6 +801,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> set_reply_to_uri
|> set_quote_url
|> set_replies
|> set_voters_count
|> CommonFixes.maybe_add_content_map()
|> strip_internal_fields
|> strip_internal_tags
@ -824,7 +831,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
# 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
object =
object_id
@ -840,7 +847,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
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
data =
data
@ -851,7 +858,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
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
object =
object
@ -868,11 +875,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, data}
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)}"
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_id
|> 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,
# 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
object = %{
"actor" => follow_activity.actor,
@ -913,7 +920,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def prepare_outgoing(%{"type" => "Reject"} = data) do
def prepare_activity(%{"type" => "Reject"} = data) do
with follow_activity <- Activity.normalize(data["object"]) do
object = %{
"actor" => follow_activity.actor,
@ -931,7 +938,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
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),
stripped_activity <- Utils.maybe_anonymize_reporter(stripped_activity),
stripped_activity <- Map.merge(stripped_activity, Utils.make_json_ld_header()) do
@ -939,7 +946,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
end
end
def prepare_outgoing(%{"type" => _type} = data) do
def prepare_activity(%{"type" => _type} = data) do
data =
data
|> 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.
"""
@callback prepare_outgoing(map()) :: {:ok, map()} | {:error, term()}
@callback prepare_activity(map()) :: {:ok, map()} | {:error, term()}
end

View file

@ -120,7 +120,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
"https://www.w3.org/ns/activitystreams",
"#{Endpoint.url()}/schemas/litepub-0.1.jsonld",
%{
"@language" => get_language(data)
"@language" => get_language(data),
"htmlMfm" => "https://w3id.org/fep/c16b#htmlMfm"
}
]
}
@ -863,6 +864,34 @@ defmodule Pleroma.Web.ActivityPub.Utils do
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
with {:ok, new_data} <- strip_report_status_data(activity.data) do
{:ok, %{activity | data: new_data}}

View file

@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectView do
end
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
end

View file

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

View file

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

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
alias Pleroma.Activity
alias Pleroma.ModerationLog
alias Pleroma.ReportNote
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.Report
@ -24,7 +25,7 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
plug(
OAuthScopesPlug,
%{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)
@ -79,6 +80,22 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
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(
%{
assigns: %{user: user},
@ -131,4 +148,40 @@ defmodule Pleroma.Web.AdminAPI.ReportController do
_ -> json_response(conn, :bad_request, "")
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

View file

@ -13,6 +13,11 @@ defmodule Pleroma.Web.AdminAPI.Report do
user = User.get_cached_by_ap_id(actor)
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 =
status_ap_ids
|> Enum.reject(&is_nil(&1))
@ -26,7 +31,13 @@ defmodule Pleroma.Web.AdminAPI.Report do
Activity.get_by_ap_id_with_object(act)
end)
%{report: report, user: user, account: account, statuses: statuses}
%{
report: report,
user: user,
account: account,
statuses: statuses,
assigned_account: assigned_account
}
end
defp make_fake_activity(act, user) do

View file

@ -26,7 +26,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}
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"])
content =
@ -36,6 +42,11 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
nil
end
assigned_account =
if assigned_account do
merge_account_views(assigned_account)
end
%{
id: report.id,
account: merge_account_views(account),
@ -49,7 +60,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
}),
state: report.data["state"],
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

View file

@ -106,7 +106,14 @@ defmodule Pleroma.Web.ApiSpec.CastAndValidate do
OpenApiSpex.cast_and_validate(spec, operation, conn, content_type, cast_opts)
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
{:ok, conn} ->
{:ok, conn}

View file

@ -123,7 +123,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
name: %Schema{type: :string, description: "Application Name"},
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
redirect_uris: %Schema{
type: :string,
oneOf: [
%Schema{type: :string},
%Schema{type: :array, items: %Schema{type: :string}}
],
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
@ -141,7 +144,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
},
example: %{
"name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"redirect_uris" => ["https://myapp.com/auth/callback"],
"website" => "https://myapp.com/",
"scopes" => ["read", "write"],
"trusted" => true
@ -157,7 +160,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
name: %Schema{type: :string, description: "Application Name"},
scopes: %Schema{type: :array, items: %Schema{type: :string}, description: "oAuth scopes"},
redirect_uris: %Schema{
type: :string,
oneOf: [
%Schema{type: :string},
%Schema{type: :array, items: %Schema{type: :string}}
],
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
@ -175,7 +181,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.OAuthAppOperation do
},
example: %{
"name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"redirect_uris" => ["https://myapp.com/auth/callback"],
"website" => "https://myapp.com/",
"scopes" => ["read", "write"],
"trusted" => true

View file

@ -53,6 +53,12 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
:query,
%Schema{type: :integer, default: 50},
"Number number of log entries per page"
),
Operation.parameter(
:assigned_account,
:query,
%Schema{type: :string},
"Filter by assigned account ID"
)
| admin_api_params()
],
@ -103,6 +109,22 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
}
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
%Operation{
tags: ["Report management"],
@ -186,7 +208,10 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
hint: %Schema{type: :string, nullable: true}
}
}
}
},
assigned_account:
account_admin()
|> Map.put(:nullable, true)
}
}
end
@ -242,6 +267,34 @@ defmodule Pleroma.Web.ApiSpec.Admin.ReportOperation do
}
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
%Schema{
type: :array,

View file

@ -97,7 +97,10 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
oneOf: [
%Schema{type: :string},
%Schema{type: :array, items: %Schema{type: :string}}
],
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},

View file

@ -57,6 +57,22 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
end
def domain_blocks_operation do
%Operation{
tags: ["Instance misc"],
summary: "Retrieve instance domain blocks",
operationId: "InstanceController.domain_blocks",
responses: %{
200 =>
Operation.response(
"Array of domain blocks",
"application/json",
array_of_domain_blocks()
)
}
}
end
def translation_languages_operation do
%Operation{
tags: ["Instance misc"],
@ -326,6 +342,18 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
max_pinned_statuses: %Schema{
type: :integer,
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."
}
}
},
@ -420,4 +448,19 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
}
}
end
defp array_of_domain_blocks do
%Schema{
type: :array,
items: %Schema{
type: :object,
properties: %{
domain: %Schema{type: :string},
digest: %Schema{type: :string},
severity: %Schema{type: :string},
comment: %Schema{type: :string}
}
}
}
end
end

View file

@ -36,7 +36,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
summary: "Create a list",
description: "Fetch the list with the given ID. Used for verifying the title of a list.",
operationId: "ListController.create",
requestBody: create_update_request(),
requestBody: create_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("List", "application/json", List),
@ -68,7 +68,7 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
description: "Change the title of a list",
operationId: "ListController.update",
parameters: [id_param()],
requestBody: create_update_request(),
requestBody: update_request(),
security: [%{"oAuth" => ["write:lists"]}],
responses: %{
200 => Operation.response("List", "application/json", List),
@ -164,14 +164,18 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
)
end
defp create_update_request do
defp create_request do
request_body(
"Parameters",
%Schema{
description: "POST body for creating or updating a List",
description: "POST body for creating a List",
type: :object,
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]
},
@ -179,6 +183,24 @@ defmodule Pleroma.Web.ApiSpec.ListOperation do
)
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
request_body(
"Parameters",

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
defmodule Pleroma.Web.ApiSpec.PleromaUtilOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.ApiError
@ -19,7 +19,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
%Operation{
tags: ["Custom emojis"],
summary: "List all custom emojis",
operationId: "UtilController.emoji",
operationId: "PleromaAPI.UtilController.emoji",
parameters: [],
responses: %{
200 =>
@ -48,7 +48,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
%Operation{
tags: ["Others"],
summary: "Dump frontend configurations",
operationId: "UtilController.frontend_configurations",
operationId: "PleromaAPI.UtilController.frontend_configurations",
parameters: [],
responses: %{
200 =>
@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Change account password",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.change_password",
operationId: "PleromaAPI.UtilController.change_password",
requestBody: request_body("Parameters", change_password_request(), required: true),
responses: %{
200 =>
@ -106,7 +106,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Change account email",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.change_email",
operationId: "PleromaAPI.UtilController.change_email",
requestBody: request_body("Parameters", change_email_request(), required: true),
responses: %{
200 =>
@ -141,7 +141,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Settings"],
summary: "Update Notification Settings",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.update_notification_settings",
operationId: "PleromaAPI.UtilController.update_notification_settings",
parameters: [
Operation.parameter(
:block_from_strangers,
@ -173,7 +173,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Disable Account",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.disable_account",
operationId: "PleromaAPI.UtilController.disable_account",
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
@ -193,7 +193,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Delete Account",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.delete_account",
operationId: "PleromaAPI.UtilController.delete_account",
parameters: [
Operation.parameter(:password, :query, :string, "Password")
],
@ -212,7 +212,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
def captcha_operation do
%Operation{
summary: "Get a captcha",
operationId: "UtilController.captcha",
operationId: "PleromaAPI.UtilController.captcha",
tags: ["Others"],
parameters: [],
responses: %{
@ -226,7 +226,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Move account",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.move_account",
operationId: "PleromaAPI.UtilController.move_account",
requestBody: request_body("Parameters", move_account_request(), required: true),
responses: %{
200 =>
@ -262,7 +262,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "List account aliases",
security: [%{"oAuth" => ["read:accounts"]}],
operationId: "UtilController.list_aliases",
operationId: "PleromaAPI.UtilController.list_aliases",
responses: %{
200 =>
Operation.response("Success", "application/json", %Schema{
@ -286,7 +286,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Add an alias to this account",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.add_alias",
operationId: "PleromaAPI.UtilController.add_alias",
requestBody: request_body("Parameters", add_alias_request(), required: true),
responses: %{
200 =>
@ -326,7 +326,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Account credentials"],
summary: "Delete an alias from this account",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.delete_alias",
operationId: "PleromaAPI.UtilController.delete_alias",
requestBody: request_body("Parameters", delete_alias_request(), required: true),
responses: %{
200 =>
@ -366,7 +366,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
tags: ["Others"],
summary: "Quick status check on the instance",
security: [%{"oAuth" => ["write:accounts"]}],
operationId: "UtilController.healthcheck",
operationId: "PleromaAPI.UtilController.healthcheck",
parameters: [],
responses: %{
200 => Operation.response("Healthy", "application/json", %Schema{type: :object}),
@ -376,52 +376,6 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
}
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
%Schema{
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
errors =
Enum.map(errors, fn
%{name: nil, reason: :invalid_enum} = err ->
%OpenApiSpex.Cast.Error{err | name: err.value}
%OpenApiSpex.Cast.Error{name: nil, reason: :invalid_enum} = err ->
%{err | name: err.value}
%{name: nil} = err ->
%OpenApiSpex.Cast.Error{err | name: List.last(err.path)}
%OpenApiSpex.Cast.Error{name: nil} = err ->
%{err | name: List.last(err.path)}
err ->
err

View file

@ -21,6 +21,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
acct: %Schema{type: :string},
avatar_static: %Schema{type: :string, format: :uri},
avatar: %Schema{type: :string, format: :uri},
avatar_description: %Schema{type: :string},
bot: %Schema{type: :boolean},
created_at: %Schema{type: :string, format: "date-time"},
display_name: %Schema{type: :string},
@ -31,6 +32,7 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
following_count: %Schema{type: :integer},
header_static: %Schema{type: :string, format: :uri},
header: %Schema{type: :string, format: :uri},
header_description: %Schema{type: :string},
id: FlakeID,
locked: %Schema{type: :boolean},
note: %Schema{type: :string, format: :html},
@ -111,8 +113,8 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do
nullable: true,
description: "Favicon image of the user's instance"
},
avatar_description: %Schema{type: :string},
header_description: %Schema{type: :string}
avatar_description: %Schema{type: :string, deprecated: true},
header_description: %Schema{type: :string, deprecated: true}
}
},
source: %Schema{

View file

@ -26,7 +26,9 @@ defmodule Pleroma.Web.ApiSpec.Schemas.AccountRelationship do
requested: %Schema{type: :boolean},
showing_reblogs: %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: %{
"blocked_by" => false,

View file

@ -15,12 +15,18 @@ defmodule Pleroma.Web.ApiSpec.Schemas.BookmarkFolder do
properties: %{
id: FlakeID,
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: %{
"id" => "9toJCu5YZW7O7gfvH6",
"name" => "Read later",
"emoji" => nil
"emoji" => nil,
"emoji_url" => nil
}
})
end

View file

@ -13,7 +13,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.List do
type: :object,
properties: %{
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: %{
"id" => "12249",

View file

@ -222,8 +222,8 @@ defmodule Pleroma.Web.CommonAPI do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
object = %Object{} <- Object.normalize(activity, fetch: false),
{_, nil} <- {:existing_announce, Utils.get_existing_announce(user.ap_id, object)},
public = public_announce?(object, params),
{:ok, announce, _} <- Builder.announce(user, object, public: public),
visibility = announce_visibility(object, params),
{:ok, announce, _} <- Builder.announce(user, object, visibility: visibility),
{:ok, activity, _} <- Pipeline.common_pipeline(announce, local: true) do
{:ok, activity}
else
@ -407,13 +407,11 @@ defmodule Pleroma.Web.CommonAPI do
end
end
defp public_announce?(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct},
do: visibility in ~w(public unlisted)
def announce_visibility(_, %{visibility: visibility})
when visibility in ~w{public unlisted private direct local},
do: visibility
defp public_announce?(object, _) do
Visibility.public?(object)
end
def announce_visibility(object, _), do: Visibility.get_visibility(object)
@spec get_visibility(map(), map() | nil, Participation.t() | nil) ::
{String.t() | nil, String.t() | nil}
@ -709,6 +707,22 @@ defmodule Pleroma.Web.CommonAPI do
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()}
def update_activity_scope(activity_id, opts \\ %{}) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),

View file

@ -88,7 +88,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> validate()
end
defp listen_object(draft) do
defp listen_object(%__MODULE__{} = draft) do
object =
draft.params
|> Map.take([:album, :artist, :title, :length])
@ -99,34 +99,34 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("cc", draft.cc)
|> Map.put("actor", draft.user.ap_id)
%__MODULE__{draft | object: object}
%{draft | object: object}
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])
%__MODULE__{draft | params: params}
%{draft | params: params}
end
defp status(%{params: %{status: status}} = draft) do
%__MODULE__{draft | status: String.trim(status)}
defp status(%__MODULE__{params: %{status: status}} = draft) do
%{draft | status: String.trim(status)}
end
defp summary(%{params: params} = draft) do
%__MODULE__{draft | summary: Map.get(params, :spoiler_text, "")}
defp summary(%__MODULE__{params: params} = draft) do
%{draft | summary: Map.get(params, :spoiler_text, "")}
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)
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)
end
end
defp attachments(%{params: params} = draft) do
defp attachments(%__MODULE__{params: params} = draft) do
attachments = Utils.attachments_from_ids(params, draft.user)
draft = %__MODULE__{draft | attachments: attachments}
draft = %{draft | attachments: attachments}
case Utils.validate_attachments_count(attachments) do
:ok -> draft
@ -134,9 +134,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
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,
# 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),
true <- Visibility.visible_for_user?(activity, draft.user),
{_, type} when type in ["Create", "Announce"] <- {:type, activity.data["type"]} do
%__MODULE__{draft | in_reply_to: activity}
%{draft | in_reply_to: activity}
else
nil ->
add_error(draft, dgettext("errors", "Cannot reply to a deleted status"))
@ -166,40 +167,43 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
end
defp in_reply_to(%{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft) do
%__MODULE__{draft | in_reply_to: in_reply_to}
defp in_reply_to(
%__MODULE__{params: %{in_reply_to_status_id: %Activity{} = in_reply_to}} = draft
) do
%{draft | in_reply_to: in_reply_to}
end
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
%Activity{} = activity ->
%__MODULE__{draft | quote_post: activity}
%{draft | quote_post: activity}
_ ->
draft
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)})
end
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])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}
%{draft | in_reply_to_conversation: in_reply_to_conversation}
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
{visibility, "direct"} when visibility != "direct" ->
add_error(draft, dgettext("errors", "The message visibility must be direct"))
{visibility, _} ->
%__MODULE__{draft | visibility: visibility}
%{draft | visibility: visibility}
end
end
@ -215,7 +219,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
false
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),
true <- can_quote?(draft, object, Visibility.get_visibility(object)) do
draft
@ -226,24 +230,24 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
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
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
{:ok, expires_at} -> %{draft | expires_at: expires_at}
{:error, message} -> add_error(draft, message)
end
end
defp poll(draft) do
defp poll(%__MODULE__{} = draft) do
case Utils.make_poll_data(draft.params) do
{: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} ->
add_error(draft, message)
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)
mentioned_ap_ids =
@ -254,25 +258,25 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Kernel.++(mentioned_ap_ids)
|> 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
defp to_and_cc(draft) do
defp to_and_cc(%__MODULE__{} = draft) do
{to, cc} = Utils.get_to_and_cc(draft)
%__MODULE__{draft | to: to, cc: cc}
%{draft | to: to, cc: cc}
end
defp context(draft) do
defp context(%__MODULE__{} = draft) do
context = Utils.make_context(draft.in_reply_to, draft.in_reply_to_conversation)
%__MODULE__{draft | context: context}
%{draft | context: context}
end
defp sensitive(draft) do
defp sensitive(%__MODULE__{} = draft) do
sensitive = draft.params[:sensitive]
%__MODULE__{draft | sensitive: sensitive}
%{draft | sensitive: sensitive}
end
defp language(draft) do
defp language(%__MODULE__{} = draft) do
language =
with language <- draft.params[:language],
true <- good_locale_code?(language) do
@ -281,10 +285,10 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
_ -> LanguageDetector.detect(draft.content_html <> " " <> draft.summary)
end
%__MODULE__{draft | language: language}
%{draft | language: language}
end
defp object(draft) do
defp object(%__MODULE__{} = draft) do
emoji = Map.merge(Pleroma.Emoji.Formatter.get_emoji_map(draft.full_payload), draft.emoji)
# Sometimes people create posts with subject containing emoji,
@ -313,6 +317,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
emoji = Map.merge(emoji, summary_emoji)
media_type = Utils.get_content_type(draft.params[:content_type])
{:ok, note_data, _meta} = Builder.note(draft)
object =
@ -320,20 +325,24 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|> Map.put("emoji", emoji)
|> Map.put("source", %{
"content" => draft.status,
"mediaType" => Utils.get_content_type(draft.params[:content_type])
"mediaType" => media_type
})
|> maybe_put("htmlMfm", true, media_type == "text/x.misskeymarkdown")
|> Map.put("generator", draft.params[:generator])
|> Map.put("language", draft.language)
%__MODULE__{draft | object: object}
%{draft | object: object}
end
defp preview?(draft) do
defp maybe_put(map, key, value, true), do: Map.put(map, key, value)
defp maybe_put(map, _key, _value, _condition), do: map
defp preview?(%__MODULE__{} = draft) do
preview? = Pleroma.Web.Utils.Params.truthy_param?(draft.params[:preview])
%__MODULE__{draft | preview?: preview?}
%{draft | preview?: preview?}
end
defp changes(draft) do
defp changes(%__MODULE__{} = draft) do
direct? = draft.visibility == "direct"
additional = %{"cc" => draft.cc, "directMessage" => direct?}
@ -353,14 +362,14 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
}
|> Utils.maybe_add_list_data(draft.user, draft.visibility)
%__MODULE__{draft | changes: changes}
%{draft | changes: changes}
end
defp with_valid(%{valid?: true} = draft, func), do: func.(draft)
defp with_valid(draft, _func), do: draft
defp add_error(draft, message) do
%__MODULE__{draft | valid?: false, errors: [message | draft.errors]}
defp add_error(%__MODULE__{} = draft, message) do
%{draft | valid?: false, errors: [message | draft.errors]}
end
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), []}
end
def get_to_and_cc(%{visibility: visibility} = draft) when visibility in ["public", "local"] do
to =
case visibility do
"public" -> [Pleroma.Constants.as_public() | draft.mentions]
"local" -> [Utils.as_local_public() | draft.mentions]
def get_to_and_cc(%{visibility: visibility} = draft) do
# If the OP is a DM already, add the implicit actor
mentions =
if visibility == "direct" && draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions])
else
draft.mentions
end
cc = [draft.user.follower_address]
if draft.in_reply_to do
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
get_to_and_cc_for_visibility(
visibility,
draft.user.follower_address,
draft.in_reply_to && draft.in_reply_to.data["actor"],
mentions
)
end
def get_to_and_cc(%{visibility: "unlisted"} = draft) do
to = [draft.user.follower_address | draft.mentions]
cc = [Pleroma.Constants.as_public()]
def get_to_and_cc_for_visibility("public", follower_collection, parent_actor, mentions) do
scope_addr = Pleroma.Constants.as_public()
if draft.in_reply_to do
{Enum.uniq([draft.in_reply_to.data["actor"] | to]), cc}
else
{to, cc}
end
to =
if parent_actor,
do: Enum.uniq([parent_actor, scope_addr | mentions]),
else: [scope_addr | mentions]
{to, [follower_collection]}
end
def get_to_and_cc(%{visibility: "private"} = draft) do
{to, cc} = get_to_and_cc(struct(draft, visibility: "direct"))
{[draft.user.follower_address | to], cc}
def get_to_and_cc_for_visibility("local", follower_collection, parent_actor, mentions) do
recipients =
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
def get_to_and_cc(%{visibility: "direct"} = draft) do
# If the OP is a DM already, add the implicit actor.
if draft.in_reply_to && Visibility.direct?(draft.in_reply_to) do
{Enum.uniq([draft.in_reply_to.data["actor"] | draft.mentions]), []}
else
{draft.mentions, []}
end
def get_to_and_cc_for_visibility("unlisted", follower_collection, parent_actor, mentions) do
to =
if parent_actor,
do: Enum.uniq([parent_actor, follower_collection | mentions]),
else: [follower_collection | mentions]
{to, [Pleroma.Constants.as_public()]}
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
User.get_ap_ids_by_nicknames(to)
@ -300,6 +322,14 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.linkify(options)
end
def format_input(text, "text/x.misskeymarkdown", options) do
text
|> Formatter.markdown_to_html(%{breaks: true})
|> safe_mfm_to_html()
|> Formatter.linkify(options)
|> Formatter.html_escape("text/x.misskeymarkdown")
end
def format_input(text, "text/markdown", options) do
text
|> Formatter.mentions_escape(options)
@ -308,6 +338,16 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|> Formatter.html_escape("text/html")
end
defp safe_mfm_to_html(html) do
html
|> MfmParser.Parser.parse()
|> MfmParser.Encoder.to_html()
rescue
_ -> html
catch
_, _ -> html
end
def format_naive_asctime(date) do
date |> DateTime.from_naive!("Etc/UTC") |> format_asctime
end

View file

@ -20,6 +20,7 @@ defmodule Pleroma.Web.EmbedController do
conn
|> delete_resp_header("x-frame-options")
|> delete_resp_header("content-security-policy")
|> put_layout({Pleroma.Web.LayoutView, :embed})
|> render("show.html",
activity: activity,
author: User.sanitize_html(author),

View file

@ -46,8 +46,10 @@ defmodule Pleroma.Web.Endpoint do
plug(Pleroma.Web.Plugs.HTTPSecurityPlug)
plug(Pleroma.Web.Plugs.UploadedMedia)
@static_cache_control "public, max-age=1209600"
@static_cache_control "public, max-age=1209600, immutable"
@static_cache_disabled "public, no-cache"
# cache for a day
@favicon_cache_control "public, max=age=86400, immutable"
# InstanceStatic needs to be before Plug.Static to be able to override shipped-static files
# If you're adding new paths to `only:` you'll need to configure them in InstanceStatic as well
@ -64,6 +66,15 @@ defmodule Pleroma.Web.Endpoint do
}
)
plug(Pleroma.Web.Plugs.FaviconPlug,
at: "/",
only: ["favicon.png"],
cache_control_for_etags: @favicon_cache_control,
headers: %{
"cache-control" => @favicon_cache_control
}
)
plug(Pleroma.Web.Plugs.InstanceStatic,
at: "/",
gzip: true,

View file

@ -29,9 +29,18 @@ defmodule Pleroma.Web.Fallback.RedirectController do
)
end
def live_dashboard(conn, _params) do
def live_dashboard(conn, %{"path" => path}) do
query_params = conn.query_string
redirect_path =
if query_params == "" do
"/pleroma/live_dashboard/#{path}"
else
"/pleroma/live_dashboard/#{path}?#{query_params}"
end
conn
|> redirect(to: "/pleroma/live_dashboard")
|> redirect(to: redirect_path)
end
def redirector(conn, _params, code \\ 200) do

View file

@ -11,6 +11,7 @@ defmodule Pleroma.Web.Federator do
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Workers.PublisherWorker
alias Pleroma.Workers.ReceiverWorker
alias Pleroma.Workers.SignatureRetryWorker
require Logger
@ -35,12 +36,21 @@ defmodule Pleroma.Web.Federator do
end
# Client API
def incoming_ap_doc(%{params: params, req_headers: req_headers}) do
ReceiverWorker.new(
def incoming_failed_signature_ap_doc(%{
method: method,
params: params,
req_headers: req_headers,
request_path: request_path,
query_string: query_string
}) do
SignatureRetryWorker.new(
%{
"op" => "incoming_ap_doc",
"op" => "incoming_failed_signature_ap_doc",
"method" => method,
"req_headers" => req_headers,
"params" => params,
"request_path" => request_path,
"query_string" => query_string,
"timeout" => :timer.seconds(20)
},
priority: 2

View file

@ -26,7 +26,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
alias Pleroma.Web.OAuth.OAuthController
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.Registration
alias Pleroma.Web.Utils.Params
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
@ -111,8 +111,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
_params
) do
with :ok <- validate_email_param(params),
:ok <- TwitterAPI.validate_captcha(app, params),
{:ok, user} <- TwitterAPI.register_user(params),
:ok <- Registration.validate_captcha(app, params),
{:ok, user} <- Registration.register_user(params),
{_, {:ok, token}} <-
{:login, OAuthController.login(user, app, app.scopes)} do
OAuthController.after_token_exchange(conn, %{user: user, token: token})

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.Registration
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
@ -17,7 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.AuthController do
def password_reset(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
TwitterAPI.password_reset(nickname_or_email)
Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end

View file

@ -7,7 +7,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:skip_auth when action in [:show, :show2, :peers])
plug(:skip_auth)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.InstanceOperation
@ -31,6 +31,11 @@ defmodule Pleroma.Web.MastodonAPI.InstanceController do
render(conn, "rules.json")
end
@doc "GET /api/v1/instance/domain_blocks"
def domain_blocks(conn, _params) do
render(conn, "domain_blocks.json")
end
@doc "GET /api/v1/instance/translation_languages"
def translation_languages(conn, _params) do
render(conn, "translation_languages.json")

View file

@ -28,27 +28,27 @@ defmodule Pleroma.Web.MastodonAPI.ListController do
# POST /api/v1/lists
def create(
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
%{assigns: %{user: user}, private: %{open_api_spex: %{body_params: params}}} =
conn,
_
) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(params, user) do
render(conn, "show.json", list: list)
end
end
# GET /api/v1/lists/:idOB
# GET /api/v1/lists/:id
def show(%{assigns: %{list: list}} = conn, _) do
render(conn, "show.json", list: list)
end
# PUT /api/v1/lists/:id
def update(
%{assigns: %{list: list}, private: %{open_api_spex: %{body_params: %{title: title}}}} =
%{assigns: %{list: list}, private: %{open_api_spex: %{body_params: params}}} =
conn,
_
) do
with {:ok, list} <- Pleroma.List.rename(list, title) do
with {:ok, list} <- Pleroma.List.update(list, params) do
render(conn, "show.json", list: list)
end
end

View file

@ -81,10 +81,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
# Note: scope not present in Mastodon: read:bookmarks
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
# Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]

View file

@ -45,6 +45,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> User.followed_hashtags()
|> Enum.map(& &1.id)
excluded_list_members =
user
|> Pleroma.List.get_exclusive_list_members()
params =
params
|> Map.put(:type, ["Create", "Announce"])
@ -58,7 +62,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.delete(:local)
activities =
[user.ap_id | User.following(user)]
[user.ap_id | User.following(user) -- excluded_list_members]
|> ActivityPub.fetch_activities(params)
|> Enum.reverse()

View file

@ -96,6 +96,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
followed_by = FollowingRelationship.following?(target, reading_user)
following = FollowingRelationship.following?(reading_user, target)
blocking =
UserRelationship.exists?(
user_relationships,
:block,
reading_user,
target,
&User.blocks_user?(&1, &2)
)
muting =
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
)
requested =
cond do
following -> false
@ -116,14 +134,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
id: to_string(target.id),
following: following,
followed_by: followed_by,
blocking:
UserRelationship.exists?(
user_relationships,
:block,
reading_user,
target,
&User.blocks_user?(&1, &2)
),
blocking: blocking,
blocked_by:
UserRelationship.exists?(
user_relationships,
@ -132,14 +143,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
reading_user,
&User.blocks_user?(&1, &2)
),
muting:
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
),
block_expires_at: nil,
muting: muting,
muting_notifications:
UserRelationship.exists?(
user_relationships,
@ -148,6 +153,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
target,
&User.muted_notifications?(&1, &2)
),
mute_expires_at: nil,
subscribing: subscribing,
notifying: subscribing,
requested: requested,
@ -174,6 +180,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
&User.endorses?(&1, &2)
)
}
|> maybe_put_mute_expires_at(target, reading_user, %{mutes: muting})
|> maybe_put_block_expires_at(target, reading_user, %{blocks: blocking})
end
def render("relationships.json", %{user: user, targets: targets} = opts) do
@ -292,8 +300,10 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
note: user.bio,
url: user.uri || user.ap_id,
avatar: avatar,
avatar_description: avatar_description,
avatar_static: avatar_static,
header: header,
header_description: header_description,
header_static: header_static,
emojis: emojis,
fields: user.fields,
@ -343,8 +353,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|> maybe_put_unread_conversation_count(user, opts[:for])
|> maybe_put_unread_notification_count(user, opts[:for])
|> maybe_put_email_address(user, opts[:for])
|> maybe_put_mute_expires_at(user, opts[:for], opts)
|> maybe_put_block_expires_at(user, opts[:for], opts)
|> maybe_put_mute_expires_at(user, opts[:for], opts, relationship)
|> maybe_put_block_expires_at(user, opts[:for], opts, relationship)
|> maybe_show_birthday(user, opts[:for])
end
@ -472,25 +482,47 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
defp maybe_put_email_address(data, _, _), do: data
defp maybe_put_mute_expires_at(data, %User{} = user, target, %{mutes: true}) do
defp maybe_put_mute_expires_at(data, target, user, opts, relationship \\ nil)
defp maybe_put_mute_expires_at(data, _target, _user, %{mutes: true}, %{
mute_expires_at: mute_expires_at
}) do
Map.put(data, :mute_expires_at, mute_expires_at)
end
defp maybe_put_mute_expires_at(data, %User{} = target, user, %{mutes: true}, _relationship) do
Map.put(
data,
:mute_expires_at,
UserRelationship.get_mute_expire_date(target, user)
UserRelationship.get_mute_expire_date(user, target)
)
end
defp maybe_put_mute_expires_at(data, _, _, _), do: data
defp maybe_put_mute_expires_at(data, _, _, _, _), do: data
defp maybe_put_block_expires_at(data, %User{} = user, target, %{blocks: true}) do
defp maybe_put_block_expires_at(data, target, user, opts, relationship \\ nil)
defp maybe_put_block_expires_at(data, _target, _user, %{blocks: true}, %{
block_expires_at: block_expires_at
}) do
Map.put(data, :block_expires_at, block_expires_at)
end
defp maybe_put_block_expires_at(
data,
%User{} = target,
%User{} = user,
%{blocks: true},
_relationship
) do
Map.put(
data,
:block_expires_at,
UserRelationship.get_block_expire_date(target, user)
UserRelationship.get_block_expire_date(user, target)
)
end
defp maybe_put_block_expires_at(data, _, _, _), do: data
defp maybe_put_block_expires_at(data, _, _, _, _), do: data
defp maybe_show_birthday(data, %User{id: user_id} = user, %User{id: user_id}) do
data

View file

@ -5,11 +5,18 @@
defmodule Pleroma.Web.MastodonAPI.InstanceView do
use Pleroma.Web, :view
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF
@mastodon_api_level "2.7.2"
@block_severities %{
federated_timeline_removal: "silence",
reject: "suspend"
}
def render("show.json", _) do
instance = Config.get(:instance)
@ -90,6 +97,53 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
}
end
def render("domain_blocks.json", _) do
if Config.get([:mrf, :transparency]) do
exclusions = Config.get([:mrf, :transparency_exclusions]) |> MRF.instance_list_from_tuples()
domain_blocks =
Config.get(:mrf_simple)
|> Enum.map(fn {rule, instances} ->
instances
|> Enum.map(fn
{host, reason} when not_empty_string(host) and not_empty_string(reason) ->
{host, reason}
{host, _reason} when not_empty_string(host) ->
{host, ""}
host when not_empty_string(host) ->
{host, ""}
_ ->
nil
end)
|> Enum.reject(&is_nil/1)
|> Enum.reject(fn {host, _} ->
host in exclusions or not Map.has_key?(@block_severities, rule)
end)
|> Enum.map(fn {host, reason} ->
domain_block = %{
domain: host,
digest: :crypto.hash(:sha256, host) |> Base.encode16(case: :lower),
severity: Map.get(@block_severities, rule)
}
if not_empty_string(reason) do
Map.put(domain_block, :comment, reason)
else
domain_block
end
end)
end)
|> List.flatten()
domain_blocks
else
[]
end
end
def render("translation_languages.json", _) do
with true <- Pleroma.Language.Translation.configured?(),
{:ok, languages} <- Pleroma.Language.Translation.languages_matrix() do
@ -249,6 +303,15 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
defp configuration2 do
configuration()
|> put_in([:accounts, :max_pinned_statuses], Config.get([:instance, :max_pinned_statuses], 0))
|> put_in([:accounts, :max_profile_fields], Config.get([:instance, :max_account_fields]))
|> put_in(
[:accounts, :profile_field_name_limit],
Config.get([:instance, :account_field_name_length])
)
|> put_in(
[:accounts, :profile_field_value_limit],
Config.get([:instance, :account_field_value_length])
)
|> put_in([:statuses, :characters_reserved_per_url], 0)
|> Map.merge(%{
urls: %{

View file

@ -13,7 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.ListView do
def render("show.json", %{list: list}) do
%{
id: to_string(list.id),
title: list.title
title: list.title,
exclusive: list.exclusive
}
end
end

View file

@ -71,6 +71,8 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end)
end
defp voters_count(%{data: %{"votersCount" => voters}}) when is_integer(voters), do: voters
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
length(voters)
end

View file

@ -31,9 +31,32 @@ defmodule Pleroma.Web.OAuth.App do
@spec changeset(t(), map()) :: Ecto.Changeset.t()
def changeset(struct, params) do
params = normalize_redirect_uris_param(params)
cast(struct, params, [:client_name, :redirect_uris, :scopes, :website, :trusted, :user_id])
end
defp normalize_redirect_uris_param(%{} = params) do
case params do
%{redirect_uris: redirect_uris} when is_list(redirect_uris) ->
Map.put(params, :redirect_uris, normalize_redirect_uris(redirect_uris))
%{"redirect_uris" => redirect_uris} when is_list(redirect_uris) ->
Map.put(params, "redirect_uris", normalize_redirect_uris(redirect_uris))
_ ->
params
end
end
defp normalize_redirect_uris(redirect_uris) when is_list(redirect_uris) do
redirect_uris
|> Enum.filter(&is_binary/1)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
|> Enum.join("\n")
end
@spec register_changeset(t(), map()) :: Ecto.Changeset.t()
def register_changeset(struct, params \\ %{}) do
changeset =

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordController do
defmodule Pleroma.Web.OAuth.PasswordController do
@moduledoc """
The module contains functions for password reset.
"""
@ -16,7 +16,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.TwitterAPI.TwitterAPI
alias Pleroma.Web.Registration
plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request)
@ -24,7 +24,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordController do
def request(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
TwitterAPI.password_reset(nickname_or_email)
Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.PasswordView do
defmodule Pleroma.Web.OAuth.PasswordView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
alias Pleroma.Web.Gettext

View file

@ -2,13 +2,13 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.Controller do
defmodule Pleroma.Web.OAuth.TokenController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.OAuth.TokenView
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.TwitterAPI.TokenView
require Logger

View file

@ -2,12 +2,12 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TokenView do
defmodule Pleroma.Web.OAuth.TokenView do
use Pleroma.Web, :view
def render("index.json", %{tokens: tokens}) do
tokens
|> render_many(Pleroma.Web.TwitterAPI.TokenView, "show.json")
|> render_many(Pleroma.Web.OAuth.TokenView, "show.json")
|> Enum.filter(&Enum.any?/1)
end

View file

@ -10,10 +10,8 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderController do
plug(Pleroma.Web.ApiSpec.CastAndValidate)
# Note: scope not present in Mastodon: read:bookmarks
plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :index)
# Note: scope not present in Mastodon: write:bookmarks
plug(
OAuthScopesPlug,
%{scopes: ["write:bookmarks"]} when action in [:create, :update, :delete]

View file

@ -0,0 +1,52 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.PasswordController do
@moduledoc """
The module contains functions for password reset.
"""
use Pleroma.Web, :controller
require Logger
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
alias Pleroma.PasswordResetToken
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.Registration
plug(Pleroma.Web.Plugs.RateLimiter, [name: :request] when action == :request)
@doc "POST /auth/password"
def request(conn, params) do
nickname_or_email = params["email"] || params["nickname"]
Registration.password_reset(nickname_or_email)
json_response(conn, :no_content, "")
end
def reset(conn, %{"token" => token}) do
with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}),
false <- PasswordResetToken.expired?(token),
%User{} = user <- User.get_cached_by_id(token.user_id) do
render(conn, "reset.html", %{
token: token,
user: user
})
else
_e -> render(conn, "invalid_token.html")
end
end
def do_reset(conn, %{"data" => data}) do
with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do
render(conn, "reset_success.html")
else
_e -> render(conn, "reset_failed.html")
end
end
end

View file

@ -0,0 +1,59 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.TokenController do
use Pleroma.Web, :controller
alias Pleroma.User
alias Pleroma.Web.OAuth.Token
alias Pleroma.Web.PleromaAPI.TokenView
alias Pleroma.Web.Plugs.OAuthScopesPlug
require Logger
plug(:skip_auth when action == :confirm_email)
plug(:skip_plug, OAuthScopesPlug when action in [:oauth_tokens, :revoke_token])
action_fallback(:errors)
def confirm_email(conn, %{"user_id" => uid, "token" => token}) do
with %User{} = user <- User.get_cached_by_id(uid),
true <- user.local and !user.is_confirmed and user.confirmation_token == token,
{:ok, _} <- User.confirm(user) do
redirect(conn, to: "/")
end
end
def oauth_tokens(%{assigns: %{user: user}} = conn, _params) do
with oauth_tokens <- Token.get_user_tokens(user) do
conn
|> put_view(TokenView)
|> render("index.json", %{tokens: oauth_tokens})
end
end
def revoke_token(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
Token.delete_user_token(user, id)
json_reply(conn, 201, "")
end
defp errors(conn, {:param_cast, _}) do
conn
|> put_status(400)
|> json("Invalid parameters")
end
defp errors(conn, _) do
conn
|> put_status(500)
|> json("Something went wrong")
end
defp json_reply(conn, status, json) do
conn
|> put_resp_content_type("application/json")
|> send_resp(status, json)
end
end

View file

@ -2,12 +2,11 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.UtilController do
defmodule Pleroma.Web.PleromaAPI.UtilController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Emoji
alias Pleroma.Healthcheck
@ -17,19 +16,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
alias Pleroma.Web.Auth.WrapperAuthenticator, as: Authenticator
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.WebFinger
plug(
Pleroma.Web.ApiSpec.CastAndValidate,
[replace_params: false]
when action != :remote_subscribe and action != :show_subscribe_form
)
plug(
Pleroma.Web.Plugs.FederatingPlug
when action == :remote_subscribe
when action == :show_subscribe_form
)
plug(Pleroma.Web.ApiSpec.CastAndValidate, replace_params: false)
plug(
OAuthScopesPlug,
@ -54,125 +42,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
]
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.TwitterUtilOperation
def show_subscribe_form(conn, %{"nickname" => nick}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - user not found",
"Could not find user"
)
})
end
end
def show_subscribe_form(conn, %{"status_id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity),
%User{} = user <- User.get_cached_by_ap_id(activity.actor),
avatar = User.avatar_url(user) do
conn
|> render("status_interact.html", %{
status_link: ap_id,
status_id: id,
nickname: user.nickname,
avatar: avatar,
error: false
})
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - status not found",
"Could not find status"
)
})
end
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
show_subscribe_form(conn, %{"nickname" => nick})
end
def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
show_subscribe_form(conn, %{"status_id" => id})
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_interaction(
%{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
_params
) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
else
_e -> json(conn, %{error: "Couldn't find user"})
end
end
defp get_ap_id(activity) do
object = Pleroma.Object.normalize(activity, fetch: false)
case object do
%{data: %{"id" => ap_id}} -> {:ok, ap_id}
_ -> {:no_ap_id, nil}
end
end
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaUtilOperation
def frontend_configurations(conn, _params) do
render(conn, "frontend_configurations.json")

View file

@ -9,11 +9,13 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
alias Pleroma.Emoji
def render("show.json", %{folder: %BookmarkFolder{} = folder}) do
{emoji, emoji_url} = get_emoji(folder.emoji)
%{
id: folder.id |> to_string(),
name: folder.name,
emoji: folder.emoji,
emoji_url: get_emoji_url(folder.emoji)
emoji: emoji,
emoji_url: emoji_url
}
end
@ -21,20 +23,15 @@ defmodule Pleroma.Web.PleromaAPI.BookmarkFolderView do
render_many(folders, __MODULE__, "show.json", Map.delete(opts, :folders))
end
defp get_emoji_url(nil) do
nil
end
defp get_emoji(nil), do: {nil, nil}
defp get_emoji_url(emoji) do
defp get_emoji(emoji) do
if Emoji.unicode?(emoji) do
nil
{emoji, nil}
else
emoji = Emoji.get(emoji)
if emoji != nil do
Emoji.local_url(emoji.file)
else
nil
case Emoji.get(emoji) do
nil -> {nil, nil}
emoji_data -> {emoji, Emoji.local_url(emoji_data.file)}
end
end
end

View file

@ -0,0 +1,13 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.PleromaAPI.UtilView do
use Pleroma.Web, :view
alias Pleroma.Config
def render("frontend_configurations.json", _) do
Config.get(:frontend_configurations, %{})
|> Enum.into(%{})
end
end

View file

@ -0,0 +1,71 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2026 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.FaviconPlug do
@behaviour Plug
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration for instance favicon.
Serves default or custom favicon.png with cacheable cache-control.
"""
import Plug.Conn, only: [put_resp_header: 3, send_resp: 3, halt: 1]
require Logger
def init(opts) do
opts
|> Keyword.put(:from, "__unconfigured_favicon_static_plug")
|> Plug.Static.init()
end
def call(%{request_path: "/favicon.png"} = conn, opts) do
case find_favicon_dir() do
{:ok, dir} ->
call_static(conn, opts, dir)
:error ->
# Favicon should always be available and this should never occur.
# If it does, halt the pipeline before having unintended side-effects.
Logger.error("No favicon.png found! Is the default favicon deleted?")
conn
|> send_resp(404, "Not found")
|> halt()
end
end
def call(conn, _) do
conn
end
defp find_favicon_dir do
instance_dir = Pleroma.Config.get([:instance, :static_dir], "instance/static")
instance_path = Path.join(instance_dir, "favicon.png")
priv_dir = Application.app_dir(:pleroma, "priv/static")
priv_path = Path.join(priv_dir, "favicon.png")
cond do
File.exists?(instance_path) -> {:ok, instance_dir}
File.exists?(priv_path) -> {:ok, priv_dir}
true -> :error
end
end
defp call_static(conn, opts, from) do
opts =
opts
|> Map.put(:from, from)
|> Map.put(:content_types, false)
conn = set_content_type(conn)
Plug.Static.call(conn, opts)
end
defp set_content_type(conn) do
put_resp_header(conn, "content-type", "image/png")
end
end

View file

@ -4,7 +4,7 @@
defmodule Pleroma.Web.Plugs.InstanceStatic do
require Pleroma.Constants
import Plug.Conn, only: [put_resp_header: 3]
import Plug.Conn, only: [put_resp_header: 3, put_status: 2, send_resp: 3, halt: 1]
@moduledoc """
This is a shim to call `Plug.Static` but with runtime `from` configuration.
@ -51,10 +51,37 @@ defmodule Pleroma.Web.Plugs.InstanceStatic do
|> Map.put(:from, from)
|> Map.put(:content_types, false)
conn = set_content_type(conn, conn.request_path)
conn =
conn
|> set_content_type(conn.request_path)
|> Plug.Static.call(opts)
# Call Plug.Static with our sanitized content-type
Plug.Static.call(conn, opts)
if conn.halted do
conn
else
path = String.trim_leading(conn.request_path, "/")
if not File.exists?(file_path(path)) do
conn
|> put_status(:not_found)
|> send_404()
|> halt()
else
conn
end
end
end
defp send_404(conn) do
if String.ends_with?(String.downcase(conn.request_path), ".json") do
conn
|> put_resp_header("content-type", "application/json")
|> send_resp(404, Jason.encode!(%{error: "not found"}))
else
conn
|> put_resp_header("content-type", "text/plain")
|> send_resp(404, "Not found")
end
end
defp set_content_type(conn, "/emoji/" <> filepath) do

View file

@ -32,8 +32,8 @@ defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlug do
# remove me once testsuite uses mapped capabilities instead of what we do now
{:user, nil} ->
Logger.debug("Failed to map identity from signature (lookup failure)")
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{actor}")
conn
Logger.debug("key_id=#{inspect(key_id_from_conn(conn))}, actor=#{inspect(actor)}")
assign(conn, :valid_signature, false)
end
end

View file

@ -67,6 +67,8 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
import Plug.Conn
alias Pleroma.Config
alias Pleroma.Config.Holder
alias Pleroma.EctoType.Config.RateLimit
alias Pleroma.User
alias Pleroma.Web.Plugs.RateLimiter.LimiterSupervisor
@ -143,7 +145,7 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
def action_settings(plug_opts) do
with limiter_name when is_atom(limiter_name) <- plug_opts[:name],
limits when not is_nil(limits) <- Config.get([:rate_limit, limiter_name]) do
{:ok, limits} <- fetch_and_normalize_limits(limiter_name) do
bucket_name_root = Keyword.get(plug_opts, :bucket_name, limiter_name)
%{
@ -151,6 +153,72 @@ defmodule Pleroma.Web.Plugs.RateLimiter do
limits: limits,
opts: plug_opts
}
else
:disabled -> nil
end
end
defp fetch_and_normalize_limits(limiter_name) do
limits = Config.get([:rate_limit, limiter_name])
case normalize_limits(limits) do
{:ok, limits} ->
{:ok, limits}
:disabled ->
:disabled
:error ->
default_limits =
Holder.default_config(:pleroma, :rate_limit)
|> get_default_limits(limiter_name)
case normalize_limits(default_limits) do
{:ok, normalized_limits} ->
warn_invalid_limits_once(limiter_name, limits)
{:ok, normalized_limits}
_ ->
warn_invalid_limits_once(limiter_name, limits)
:disabled
end
end
end
defp get_default_limits(%{} = rate_limit, limiter_name), do: Map.get(rate_limit, limiter_name)
defp get_default_limits(rate_limit, limiter_name) when is_list(rate_limit) do
if Keyword.keyword?(rate_limit) do
Keyword.get(rate_limit, limiter_name)
else
nil
end
end
defp get_default_limits(_, _), do: nil
@invalid_limits_warned_key {__MODULE__, :invalid_limits_warned}
defp warn_invalid_limits_once(limiter_name, limits) do
warned = :persistent_term.get(@invalid_limits_warned_key, MapSet.new())
if MapSet.member?(warned, limiter_name) do
:ok
else
:persistent_term.put(@invalid_limits_warned_key, MapSet.put(warned, limiter_name))
Logger.warning(
"Invalid rate limiter config for #{inspect(limiter_name)}: #{inspect(limits)}. Falling back to defaults or disabling this limiter."
)
end
end
defp normalize_limits(nil), do: :disabled
defp normalize_limits(limits) do
case RateLimit.cast(limits) do
{:ok, normalized_limits} -> {:ok, normalized_limits}
:error -> :error
end
end

View file

@ -17,7 +17,7 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
# no slashes
@path "media"
@default_cache_control_header "public, max-age=1209600"
@default_cache_control_header "public, max-age=1209600, immutable"
def init(_opts) do
static_plug_opts =

View file

@ -5,9 +5,9 @@
defmodule Pleroma.Web.Preload.Providers.Instance do
alias Pleroma.Web.MastodonAPI.InstanceView
alias Pleroma.Web.Nodeinfo.Nodeinfo
alias Pleroma.Web.PleromaAPI.UtilView
alias Pleroma.Web.Plugs.InstanceStatic
alias Pleroma.Web.Preload.Providers.Provider
alias Pleroma.Web.TwitterAPI.UtilView
@behaviour Provider
@instance_url "/api/v1/instance"

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPI do
defmodule Pleroma.Web.Registration do
import Pleroma.Web.Gettext
alias Pleroma.Emails.Mailer

View file

@ -2,7 +2,7 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionController do
use Pleroma.Web, :controller
require Logger
@ -14,18 +14,27 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
alias Pleroma.Web.Auth.TOTPAuthenticator
alias Pleroma.Web.Auth.WrapperAuthenticator
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.WebFinger
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
plug(
Pleroma.Web.ApiSpec.CastAndValidate,
[replace_params: false]
when action == :remote_interaction
)
plug(Pleroma.Web.Plugs.FederatingPlug)
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
Pleroma.Web.Plugs.OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
when action in [:do_follow]
when action == :do_follow
)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.RemoteInteractionOperation
# GET /ostatus_subscribe
#
def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
@ -125,7 +134,7 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
#
def authorize_interaction(conn, %{"uri" => uri}) do
conn
|> redirect(to: Routes.remote_follow_path(conn, :follow, %{acct: uri}))
|> redirect(to: Routes.remote_interaction_path(conn, :follow, %{acct: uri}))
end
defp handle_follow_error(conn, {:mfa_token, followee, _} = _) do
@ -162,4 +171,122 @@ defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Something went wrong."})
end
def show_subscribe_form(conn, %{"nickname" => nick}) do
with %User{} = user <- User.get_cached_by_nickname(nick),
avatar = User.avatar_url(user) do
conn
|> render("subscribe.html", %{nickname: nick, avatar: avatar, error: false})
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - user not found",
"Could not find user"
)
})
end
end
def show_subscribe_form(conn, %{"status_id" => id}) do
with %Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity),
%User{} = user <- User.get_cached_by_ap_id(activity.actor),
avatar = User.avatar_url(user) do
conn
|> render("status_interact.html", %{
status_link: ap_id,
status_id: id,
nickname: user.nickname,
avatar: avatar,
error: false
})
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - status not found",
"Could not find status"
)
})
end
end
def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do
show_subscribe_form(conn, %{"nickname" => nick})
end
def remote_subscribe(conn, %{"status_id" => id, "profile" => _}) do
show_subscribe_form(conn, %{"status_id" => id})
end
def remote_subscribe(conn, %{"user" => %{"nickname" => nick, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%User{ap_id: ap_id} <- User.get_cached_by_nickname(nick) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "subscribe.html", %{
nickname: nick,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"remote follow error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_subscribe(conn, %{"status" => %{"status_id" => id, "profile" => profile}}) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile),
%Activity{} = activity <- Activity.get_by_id(id),
{:ok, ap_id} <- get_ap_id(activity) do
conn
|> Phoenix.Controller.redirect(external: String.replace(template, "{uri}", ap_id))
else
_e ->
render(conn, "status_interact.html", %{
status_id: id,
avatar: nil,
error:
Pleroma.Web.Gettext.dpgettext(
"static_pages",
"status interact error message - unknown error",
"Something went wrong."
)
})
end
end
def remote_interaction(
%{private: %{open_api_spex: %{body_params: %{ap_id: ap_id, profile: profile}}}} = conn,
_params
) do
with {:ok, %{"subscribe_address" => template}} <- WebFinger.finger(profile) do
conn
|> json(%{url: String.replace(template, "{uri}", ap_id)})
else
_e -> json(conn, %{error: "Couldn't find user"})
end
end
defp get_ap_id(activity) do
object = Pleroma.Object.normalize(activity, fetch: false)
case object do
%{data: %{"id" => ap_id}} -> {:ok, ap_id}
_ -> {:no_ap_id, nil}
end
end
end

View file

@ -2,9 +2,11 @@
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
defmodule Pleroma.Web.RemoteInteraction.RemoteInteractionView do
use Pleroma.Web, :view
import Phoenix.HTML
import Phoenix.HTML.Form
import Phoenix.HTML.Link
alias Pleroma.Web.Gettext
def avatar_url(user) do

View file

@ -226,23 +226,28 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
scope "/api/v1/pleroma", Pleroma.Web.TwitterAPI do
scope "/api/v1/pleroma", Pleroma.Web.OAuth do
pipe_through(:pleroma_api)
get("/password_reset/:token", PasswordController, :reset, as: :reset_password)
post("/password_reset", PasswordController, :do_reset, as: :reset_password)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
post("/remote_interaction", UtilController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:pleroma_api)
get("/emoji", UtilController, :emoji)
get("/captcha", UtilController, :captcha)
get("/healthcheck", UtilController, :healthcheck)
get("/federation_status", InstancesController, :show)
end
scope "/api/v1/pleroma", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_api)
post("/remote_interaction", RemoteInteractionController, :remote_interaction)
end
scope "/api/v1/pleroma", Pleroma.Web do
pipe_through(:pleroma_api)
post("/uploader_callback/:upload_path", UploaderController, :callback)
@ -395,6 +400,7 @@ defmodule Pleroma.Web.Router do
get("/reports", ReportController, :index)
get("/reports/:id", ReportController, :show)
patch("/reports", ReportController, :update)
post("/reports/assign_account", ReportController, :assign_account)
post("/reports/:id/notes", ReportController, :notes_create)
delete("/reports/:report_id/notes/:id", ReportController, :notes_delete)
end
@ -483,18 +489,18 @@ defmodule Pleroma.Web.Router do
end
end
scope "/", Pleroma.Web.TwitterAPI do
scope "/", Pleroma.Web.RemoteInteraction do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/main/ostatus", UtilController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
post("/main/ostatus", RemoteInteractionController, :remote_subscribe)
get("/main/ostatus", RemoteInteractionController, :show_subscribe_form)
get("/ostatus_subscribe", RemoteInteractionController, :follow)
post("/ostatus_subscribe", RemoteInteractionController, :do_follow)
get("/authorize_interaction", RemoteFollowController, :authorize_interaction)
get("/authorize_interaction", RemoteInteractionController, :authorize_interaction)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do
scope "/api/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:authenticated_api)
post("/change_email", UtilController, :change_email)
@ -814,6 +820,7 @@ defmodule Pleroma.Web.Router do
get("/instance", InstanceController, :show)
get("/instance/peers", InstanceController, :peers)
get("/instance/rules", InstanceController, :rules)
get("/instance/domain_blocks", InstanceController, :domain_blocks)
get("/instance/translation_languages", InstanceController, :translation_languages)
get("/statuses", StatusController, :index)
@ -851,7 +858,7 @@ defmodule Pleroma.Web.Router do
scope "/api", Pleroma.Web do
pipe_through(:config)
get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations)
get("/pleroma/frontend_configurations", PleromaAPI.UtilController, :frontend_configurations)
end
scope "/api", Pleroma.Web do
@ -859,7 +866,7 @@ defmodule Pleroma.Web.Router do
get(
"/account/confirm_email/:user_id/:token",
TwitterAPI.Controller,
OAuth.TokenController,
:confirm_email,
as: :confirm_email
)
@ -871,11 +878,11 @@ defmodule Pleroma.Web.Router do
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
scope "/api", Pleroma.Web, as: :authenticated_pleroma_api do
pipe_through(:authenticated_api)
get("/oauth_tokens", TwitterAPI.Controller, :oauth_tokens)
delete("/oauth_tokens/:id", TwitterAPI.Controller, :revoke_token)
get("/oauth_tokens", OAuth.TokenController, :oauth_tokens)
delete("/oauth_tokens/:id", OAuth.TokenController, :revoke_token)
end
scope "/", Pleroma.Web do
@ -1024,7 +1031,9 @@ defmodule Pleroma.Web.Router do
scope "/", Pleroma.Web do
pipe_through(:pleroma_html)
post("/auth/password", TwitterAPI.PasswordController, :request)
post("/auth/password", OAuth.PasswordController, :request)
get("/embed/:id", EmbedController, :show)
end
scope "/proxy/", Pleroma.Web do
@ -1086,7 +1095,7 @@ defmodule Pleroma.Web.Router do
get("/:maybe_nickname_or_id", RedirectController, :redirector_with_meta)
match(:*, "/api/pleroma/*path", LegacyPleromaApiRerouterPlug, [])
get("/api/*path", RedirectController, :api_not_implemented)
get("/phoenix/live_dashboard", RedirectController, :live_dashboard)
get("/phoenix/live_dashboard/*path", RedirectController, :live_dashboard)
get("/*path", RedirectController, :redirector_with_preload)
options("/*path", RedirectController, :empty)

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