Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into pleroma-meilisearch
This commit is contained in:
commit
0c5cc51983
746 changed files with 11830 additions and 5643 deletions
|
|
@ -1,113 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Mix.Tasks.Pleroma.Benchmark do
|
||||
import Mix.Pleroma
|
||||
use Mix.Task
|
||||
|
||||
def run(["search"]) do
|
||||
start_pleroma()
|
||||
|
||||
Benchee.run(%{
|
||||
"search" => fn ->
|
||||
Pleroma.Activity.search(nil, "cofe")
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def run(["tag"]) do
|
||||
start_pleroma()
|
||||
|
||||
Benchee.run(%{
|
||||
"tag" => fn ->
|
||||
%{"type" => "Create", "tag" => "cofe"}
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def run(["render_timeline", nickname | _] = args) do
|
||||
start_pleroma()
|
||||
user = Pleroma.User.get_by_nickname(nickname)
|
||||
|
||||
activities =
|
||||
%{}
|
||||
|> Map.put("type", ["Create", "Announce"])
|
||||
|> Map.put("blocking_user", user)
|
||||
|> Map.put("muting_user", user)
|
||||
|> Map.put("user", user)
|
||||
|> Map.put("limit", 4096)
|
||||
|> Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities()
|
||||
|> Enum.reverse()
|
||||
|
||||
inputs = %{
|
||||
"1 activity" => Enum.take_random(activities, 1),
|
||||
"10 activities" => Enum.take_random(activities, 10),
|
||||
"20 activities" => Enum.take_random(activities, 20),
|
||||
"40 activities" => Enum.take_random(activities, 40),
|
||||
"80 activities" => Enum.take_random(activities, 80)
|
||||
}
|
||||
|
||||
inputs =
|
||||
if Enum.at(args, 2) == "extended" do
|
||||
Map.merge(inputs, %{
|
||||
"200 activities" => Enum.take_random(activities, 200),
|
||||
"500 activities" => Enum.take_random(activities, 500),
|
||||
"2000 activities" => Enum.take_random(activities, 2000),
|
||||
"4096 activities" => Enum.take_random(activities, 4096)
|
||||
})
|
||||
else
|
||||
inputs
|
||||
end
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
"Standart rendering" => fn activities ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
},
|
||||
inputs: inputs
|
||||
)
|
||||
end
|
||||
|
||||
def run(["adapters"]) do
|
||||
start_pleroma()
|
||||
|
||||
:ok =
|
||||
Pleroma.Gun.Conn.open(
|
||||
"https://httpbin.org/stream-bytes/1500",
|
||||
:gun_connections
|
||||
)
|
||||
|
||||
Process.sleep(1_500)
|
||||
|
||||
Benchee.run(
|
||||
%{
|
||||
"Without conn and without pool" => fn ->
|
||||
{:ok, %Tesla.Env{}} =
|
||||
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [],
|
||||
pool: :no_pool,
|
||||
receive_conn: false
|
||||
)
|
||||
end,
|
||||
"Without conn and with pool" => fn ->
|
||||
{:ok, %Tesla.Env{}} =
|
||||
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], receive_conn: false)
|
||||
end,
|
||||
"With reused conn and without pool" => fn ->
|
||||
{:ok, %Tesla.Env{}} =
|
||||
Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500", [], pool: :no_pool)
|
||||
end,
|
||||
"With reused conn and with pool" => fn ->
|
||||
{:ok, %Tesla.Env{}} = Pleroma.HTTP.get("https://httpbin.org/stream-bytes/1500")
|
||||
end
|
||||
},
|
||||
parallel: 10
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -266,12 +266,20 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
config_dir = Path.dirname(config_path)
|
||||
psql_dir = Path.dirname(psql_path)
|
||||
|
||||
# Note: Distros requiring group read (0o750) on those directories should
|
||||
# pre-create the directories.
|
||||
[config_dir, psql_dir, static_dir, uploads_dir]
|
||||
|> Enum.reject(&File.exists?/1)
|
||||
|> Enum.map(&File.mkdir_p!/1)
|
||||
|> Enum.each(fn dir ->
|
||||
File.mkdir_p!(dir)
|
||||
File.chmod!(dir, 0o700)
|
||||
end)
|
||||
|
||||
shell_info("Writing config to #{config_path}.")
|
||||
|
||||
# Sadly no fchmod(2) equivalent in Elixir…
|
||||
File.touch!(config_path)
|
||||
File.chmod!(config_path, 0o640)
|
||||
File.write(config_path, result_config)
|
||||
shell_info("Writing the postgres script to #{psql_path}.")
|
||||
File.write(psql_path, result_psql)
|
||||
|
|
@ -290,8 +298,7 @@ defmodule Mix.Tasks.Pleroma.Instance do
|
|||
else
|
||||
shell_error(
|
||||
"The task would have overwritten the following files:\n" <>
|
||||
(Enum.map(will_overwrite, &"- #{&1}\n") |> Enum.join("")) <>
|
||||
"Rerun with `--force` to overwrite them."
|
||||
Enum.map_join(will_overwrite, &"- #{&1}\n") <> "Rerun with `--force` to overwrite them."
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,70 @@ defmodule Mix.Tasks.Pleroma.OpenapiSpec do
|
|||
def run([path]) do
|
||||
# Load Pleroma application to get version info
|
||||
Application.load(:pleroma)
|
||||
spec = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!()
|
||||
File.write(path, spec)
|
||||
|
||||
spec_json = Pleroma.Web.ApiSpec.spec(server_specific: false) |> Jason.encode!()
|
||||
# to get rid of the structs
|
||||
spec_regened = spec_json |> Jason.decode!()
|
||||
|
||||
check_specs!(spec_regened)
|
||||
|
||||
File.write(path, spec_json)
|
||||
end
|
||||
|
||||
defp check_specs!(spec) do
|
||||
with :ok <- check_specs(spec) do
|
||||
:ok
|
||||
else
|
||||
{_, errors} ->
|
||||
IO.puts(IO.ANSI.format([:red, :bright, "Spec check failed, errors:"]))
|
||||
Enum.map(errors, &IO.puts/1)
|
||||
|
||||
raise "Spec check failed"
|
||||
end
|
||||
end
|
||||
|
||||
def check_specs(spec) do
|
||||
errors =
|
||||
spec["paths"]
|
||||
|> Enum.flat_map(fn {path, %{} = endpoints} ->
|
||||
Enum.map(
|
||||
endpoints,
|
||||
fn {method, endpoint} ->
|
||||
with :ok <- check_endpoint(spec, endpoint) do
|
||||
:ok
|
||||
else
|
||||
error ->
|
||||
"#{endpoint["operationId"]} (#{method} #{path}): #{error}"
|
||||
end
|
||||
end
|
||||
)
|
||||
|> Enum.reject(fn res -> res == :ok end)
|
||||
end)
|
||||
|
||||
if errors == [] do
|
||||
:ok
|
||||
else
|
||||
{:error, errors}
|
||||
end
|
||||
end
|
||||
|
||||
defp check_endpoint(spec, endpoint) do
|
||||
valid_tags = available_tags(spec)
|
||||
|
||||
with {_, [_ | _] = tags} <- {:tags, endpoint["tags"]},
|
||||
{_, []} <- {:unavailable, Enum.reject(tags, &(&1 in valid_tags))} do
|
||||
:ok
|
||||
else
|
||||
{:tags, _} ->
|
||||
"No tags specified"
|
||||
|
||||
{:unavailable, tags} ->
|
||||
"Tags #{inspect(tags)} not available. Please add it in \"x-tagGroups\" in Pleroma.Web.ApiSpec"
|
||||
end
|
||||
end
|
||||
|
||||
defp available_tags(spec) do
|
||||
spec["x-tagGroups"]
|
||||
|> Enum.flat_map(fn %{"tags" => tags} -> tags end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -209,7 +209,8 @@ defmodule Pleroma.Application do
|
|||
build_cachex("chat_message_id_idempotency_key",
|
||||
expiration: chat_message_id_idempotency_key_expiration(),
|
||||
limit: 500_000
|
||||
)
|
||||
),
|
||||
build_cachex("rel_me", limit: 2500)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.BBS.Authenticator do
|
||||
use Sshd.PasswordAuthenticator
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Plugs.AuthenticationPlug
|
||||
|
||||
def authenticate(username, password) do
|
||||
username = to_string(username)
|
||||
password = to_string(password)
|
||||
|
||||
with %User{} = user <- User.get_by_nickname(username) do
|
||||
AuthenticationPlug.checkpw(password, user.password_hash)
|
||||
else
|
||||
_e -> false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,246 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.BBS.Handler do
|
||||
use Sshd.ShellHandler
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.HTML
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
def on_shell(username, _pubkey, _ip, _port) do
|
||||
:ok = IO.puts("Welcome to #{Pleroma.Config.get([:instance, :name])}!")
|
||||
user = Pleroma.User.get_cached_by_nickname(to_string(username))
|
||||
Logger.debug("#{inspect(user)}")
|
||||
loop(run_state(user: user))
|
||||
end
|
||||
|
||||
def on_connect(username, ip, port, method) do
|
||||
Logger.debug(fn ->
|
||||
"""
|
||||
Incoming SSH shell #{inspect(self())} requested for #{username} from #{inspect(ip)}:#{inspect(port)} using #{inspect(method)}
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
def on_disconnect(username, ip, port) do
|
||||
Logger.debug(fn ->
|
||||
"Disconnecting SSH shell for #{username} from #{inspect(ip)}:#{inspect(port)}"
|
||||
end)
|
||||
end
|
||||
|
||||
defp loop(state) do
|
||||
self_pid = self()
|
||||
counter = state.counter
|
||||
prefix = state.prefix
|
||||
user = state.user
|
||||
|
||||
input = spawn(fn -> io_get(self_pid, prefix, counter, user.nickname) end)
|
||||
wait_input(state, input)
|
||||
end
|
||||
|
||||
def puts_activity(activity) do
|
||||
status = Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{activity: activity})
|
||||
|
||||
IO.puts("-- #{status.id} by #{status.account.display_name} (#{status.account.acct})")
|
||||
|
||||
status.content
|
||||
|> String.split("<br/>")
|
||||
|> Enum.map(&HTML.strip_tags/1)
|
||||
|> Enum.map(&HtmlEntities.decode/1)
|
||||
|> Enum.map(&IO.puts/1)
|
||||
end
|
||||
|
||||
def puts_notification(activity, user) do
|
||||
notification =
|
||||
Pleroma.Web.MastodonAPI.NotificationView.render("show.json", %{
|
||||
notification: activity,
|
||||
for: user
|
||||
})
|
||||
|
||||
IO.puts(
|
||||
"== (#{notification.type}) #{notification.status.id} by #{notification.account.display_name} (#{notification.account.acct})"
|
||||
)
|
||||
|
||||
notification.status.content
|
||||
|> String.split("<br/>")
|
||||
|> Enum.map(&HTML.strip_tags/1)
|
||||
|> Enum.map(&HtmlEntities.decode/1)
|
||||
|> (fn x ->
|
||||
case x do
|
||||
[content] ->
|
||||
"> " <> content
|
||||
|
||||
[head | _tail] ->
|
||||
# "> " <> hd <> "..."
|
||||
head
|
||||
|> String.slice(1, 80)
|
||||
|> (fn x -> "> " <> x <> "..." end).()
|
||||
end
|
||||
end).()
|
||||
|> IO.puts()
|
||||
|
||||
IO.puts("")
|
||||
end
|
||||
|
||||
def handle_command(state, "help") do
|
||||
IO.puts("Available commands:")
|
||||
IO.puts("help - This help")
|
||||
IO.puts("home - Show the home timeline")
|
||||
IO.puts("p <text> - Post the given text")
|
||||
IO.puts("r <id> <text> - Reply to the post with the given id")
|
||||
IO.puts("t <id> - Show a thread from the given id")
|
||||
IO.puts("n - Show notifications")
|
||||
IO.puts("n read - Mark all notifactions as read")
|
||||
IO.puts("f <id> - Favourites the post with the given id")
|
||||
IO.puts("R <id> - Repeat the post with the given id")
|
||||
IO.puts("quit - Quit")
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "r " <> text) do
|
||||
text = String.trim(text)
|
||||
[activity_id, rest] = String.split(text, " ", parts: 2)
|
||||
|
||||
with %Activity{} <- Activity.get_by_id(activity_id),
|
||||
{:ok, _activity} <-
|
||||
CommonAPI.post(user, %{status: rest, in_reply_to_status_id: activity_id}) do
|
||||
IO.puts("Replied!")
|
||||
else
|
||||
_e -> IO.puts("Could not reply...")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "t " <> activity_id) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
activities =
|
||||
ActivityPub.fetch_activities_for_context(activity.data["context"], %{
|
||||
blocking_user: user,
|
||||
user: user,
|
||||
exclude_id: activity.id
|
||||
})
|
||||
|
||||
case activities do
|
||||
[] ->
|
||||
activity_id
|
||||
|> Activity.get_by_id()
|
||||
|> puts_activity()
|
||||
|
||||
_ ->
|
||||
activities
|
||||
|> Enum.reverse()
|
||||
|> Enum.each(&puts_activity/1)
|
||||
end
|
||||
else
|
||||
_e -> IO.puts("Could not show this thread...")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "n read") do
|
||||
Pleroma.Notification.clear(user)
|
||||
IO.puts("All notifications were marked as read")
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "n") do
|
||||
user
|
||||
|> Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(%{})
|
||||
|> Enum.each(&puts_notification(&1, user))
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "p " <> text) do
|
||||
text = String.trim(text)
|
||||
|
||||
with {:ok, activity} <- CommonAPI.post(user, %{status: text}) do
|
||||
IO.puts("Posted! ID: #{activity.id}")
|
||||
else
|
||||
_e -> IO.puts("Could not post...")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(%{user: user} = state, "f " <> id) do
|
||||
id = String.trim(id)
|
||||
|
||||
with %Activity{} = activity <- Activity.get_by_id(id),
|
||||
{:ok, _activity} <- CommonAPI.favorite(user, activity) do
|
||||
IO.puts("Favourited!")
|
||||
else
|
||||
_e -> IO.puts("Could not Favourite...")
|
||||
end
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(state, "home") do
|
||||
user = state.user
|
||||
|
||||
params =
|
||||
%{}
|
||||
|> Map.put(:type, ["Create"])
|
||||
|> Map.put(:blocking_user, user)
|
||||
|> Map.put(:muting_user, user)
|
||||
|> Map.put(:user, user)
|
||||
|
||||
activities =
|
||||
[user.ap_id | Pleroma.User.following(user)]
|
||||
|> ActivityPub.fetch_activities(params)
|
||||
|
||||
Enum.each(activities, fn activity ->
|
||||
puts_activity(activity)
|
||||
end)
|
||||
|
||||
state
|
||||
end
|
||||
|
||||
def handle_command(state, command) do
|
||||
IO.puts("Unknown command '#{command}'")
|
||||
state
|
||||
end
|
||||
|
||||
defp wait_input(state, input) do
|
||||
receive do
|
||||
{:input, ^input, "quit\n"} ->
|
||||
IO.puts("Exiting...")
|
||||
|
||||
{:input, ^input, code} when is_binary(code) ->
|
||||
code = String.trim(code)
|
||||
|
||||
state = handle_command(state, code)
|
||||
|
||||
loop(%{state | counter: state.counter + 1})
|
||||
|
||||
{:input, ^input, {:error, :interrupted}} ->
|
||||
IO.puts("Caught Ctrl+C...")
|
||||
loop(%{state | counter: state.counter + 1})
|
||||
|
||||
{:input, ^input, msg} ->
|
||||
:ok = Logger.warn("received unknown message: #{inspect(msg)}")
|
||||
loop(%{state | counter: state.counter + 1})
|
||||
end
|
||||
end
|
||||
|
||||
defp run_state(opts) do
|
||||
%{prefix: "pleroma", counter: 1, user: opts[:user]}
|
||||
end
|
||||
|
||||
defp io_get(pid, prefix, counter, username) do
|
||||
prompt = prompt(prefix, counter, username)
|
||||
send(pid, {:input, self(), IO.gets(:stdio, prompt)})
|
||||
end
|
||||
|
||||
defp prompt(prefix, counter, username) do
|
||||
prompt = "#{username}@#{prefix}:#{counter}>"
|
||||
prompt <> " "
|
||||
end
|
||||
end
|
||||
|
|
@ -20,6 +20,20 @@ defmodule Pleroma.Config.ReleaseRuntimeProvider do
|
|||
|
||||
with_runtime_config =
|
||||
if File.exists?(config_path) do
|
||||
# <https://git.pleroma.social/pleroma/pleroma/-/issues/3135>
|
||||
%File.Stat{mode: mode} = File.lstat!(config_path)
|
||||
|
||||
if Bitwise.band(mode, 0o007) > 0 do
|
||||
raise "Configuration at #{config_path} has world-permissions, execute the following: chmod o= #{config_path}"
|
||||
end
|
||||
|
||||
if Bitwise.band(mode, 0o020) > 0 do
|
||||
raise "Configuration at #{config_path} has group-wise write permissions, execute the following: chmod g-w #{config_path}"
|
||||
end
|
||||
|
||||
# Note: Elixir doesn't provides a getuid(2)
|
||||
# so cannot forbid group-read only when config is owned by us
|
||||
|
||||
runtime_config = Config.Reader.read!(config_path)
|
||||
|
||||
with_defaults
|
||||
|
|
|
|||
|
|
@ -42,6 +42,18 @@ defmodule Pleroma.Constants do
|
|||
]
|
||||
)
|
||||
|
||||
const(status_object_types,
|
||||
do: [
|
||||
"Note",
|
||||
"Question",
|
||||
"Audio",
|
||||
"Video",
|
||||
"Event",
|
||||
"Article",
|
||||
"Page"
|
||||
]
|
||||
)
|
||||
|
||||
const(updatable_object_types,
|
||||
do: [
|
||||
"Note",
|
||||
|
|
@ -69,4 +81,21 @@ defmodule Pleroma.Constants do
|
|||
const(mime_regex,
|
||||
do: ~r/^[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+\/[^[:cntrl:] ()<>@,;:\\"\/\[\]?=]+(; .*)?$/
|
||||
)
|
||||
|
||||
const(upload_object_types, do: ["Document", "Image"])
|
||||
|
||||
const(activity_json_canonical_mime_type,
|
||||
do: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
|
||||
)
|
||||
|
||||
const(activity_json_mime_types,
|
||||
do: [
|
||||
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
|
||||
"application/activity+json"
|
||||
]
|
||||
)
|
||||
|
||||
const(public_streams,
|
||||
do: ["public", "public:local", "public:media", "public:local:media"]
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -27,3 +27,11 @@ defenum(Pleroma.DataMigration.State,
|
|||
failed: 4,
|
||||
manual: 5
|
||||
)
|
||||
|
||||
defenum(Pleroma.User.Backup.State,
|
||||
pending: 1,
|
||||
running: 2,
|
||||
complete: 3,
|
||||
failed: 4,
|
||||
invalid: 5
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,23 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.EctoType.ActivityPub.ObjectValidators.BareUri do
|
||||
use Ecto.Type
|
||||
|
||||
def type, do: :string
|
||||
|
||||
def cast(uri) when is_binary(uri) do
|
||||
case URI.parse(uri) do
|
||||
%URI{scheme: nil} -> :error
|
||||
%URI{} -> {:ok, uri}
|
||||
_ -> :error
|
||||
end
|
||||
end
|
||||
|
||||
def cast(_), do: :error
|
||||
|
||||
def dump(data), do: {:ok, data}
|
||||
|
||||
def load(data), do: {:ok, data}
|
||||
end
|
||||
|
|
@ -51,6 +51,8 @@ defmodule Pleroma.Emoji do
|
|||
@doc "Returns the path of the emoji `name`."
|
||||
@spec get(String.t()) :: String.t() | nil
|
||||
def get(name) do
|
||||
name = maybe_strip_name(name)
|
||||
|
||||
case :ets.lookup(@ets, name) do
|
||||
[{_, path}] -> path
|
||||
_ -> nil
|
||||
|
|
@ -139,6 +141,57 @@ defmodule Pleroma.Emoji do
|
|||
|
||||
def is_unicode_emoji?(_), do: false
|
||||
|
||||
@emoji_regex ~r/:[A-Za-z0-9_-]+(@.+)?:/
|
||||
|
||||
def is_custom_emoji?(s) when is_binary(s), do: Regex.match?(@emoji_regex, s)
|
||||
|
||||
def is_custom_emoji?(_), do: false
|
||||
|
||||
def maybe_strip_name(name) when is_binary(name), do: String.trim(name, ":")
|
||||
|
||||
def maybe_strip_name(name), do: name
|
||||
|
||||
def maybe_quote(name) when is_binary(name) do
|
||||
if is_unicode_emoji?(name) do
|
||||
name
|
||||
else
|
||||
if String.starts_with?(name, ":") do
|
||||
name
|
||||
else
|
||||
":#{name}:"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def maybe_quote(name), do: name
|
||||
|
||||
def emoji_url(%{"type" => "EmojiReact", "content" => _, "tag" => []}), do: nil
|
||||
|
||||
def emoji_url(%{"type" => "EmojiReact", "content" => emoji, "tag" => tags}) do
|
||||
emoji = maybe_strip_name(emoji)
|
||||
|
||||
tag =
|
||||
tags
|
||||
|> Enum.find(fn tag ->
|
||||
tag["type"] == "Emoji" && !is_nil(tag["name"]) && tag["name"] == emoji
|
||||
end)
|
||||
|
||||
if is_nil(tag) do
|
||||
nil
|
||||
else
|
||||
tag
|
||||
|> Map.get("icon")
|
||||
|> Map.get("url")
|
||||
end
|
||||
end
|
||||
|
||||
def emoji_url(_), do: nil
|
||||
|
||||
def emoji_name_with_instance(name, url) do
|
||||
url = url |> URI.parse() |> Map.get(:host)
|
||||
"#{name}@#{url}"
|
||||
end
|
||||
|
||||
emoji_qualification_map =
|
||||
emojis
|
||||
|> Enum.filter(&String.contains?(&1, "\uFE0F"))
|
||||
|
|
|
|||
|
|
@ -285,6 +285,7 @@ defmodule Pleroma.Emoji.Pack do
|
|||
|
||||
@spec load_pack(String.t()) :: {:ok, t()} | {:error, :file.posix()}
|
||||
def load_pack(name) do
|
||||
name = Path.basename(name)
|
||||
pack_file = Path.join([emoji_path(), name, "pack.json"])
|
||||
|
||||
with {:ok, _} <- File.stat(pack_file),
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ defmodule Pleroma.Formatter do
|
|||
end
|
||||
|
||||
def markdown_to_html(text) do
|
||||
Earmark.as_html!(text, %Earmark.Options{compact_output: true})
|
||||
Earmark.as_html!(text, %Earmark.Options{compact_output: true, smartypants: false})
|
||||
end
|
||||
|
||||
def html_escape({text, mentions, hashtags}, type) do
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Instances.Instance do
|
|||
|
||||
alias Pleroma.Instances
|
||||
alias Pleroma.Instances.Instance
|
||||
alias Pleroma.Maps
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Workers.BackgroundWorker
|
||||
|
|
@ -24,6 +25,14 @@ defmodule Pleroma.Instances.Instance do
|
|||
field(:favicon, :string)
|
||||
field(:favicon_updated_at, :naive_datetime)
|
||||
|
||||
embeds_one :metadata, Pleroma.Instances.Metadata, primary_key: false do
|
||||
field(:software_name, :string)
|
||||
field(:software_version, :string)
|
||||
field(:software_repository, :string)
|
||||
end
|
||||
|
||||
field(:metadata_updated_at, :utc_datetime)
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
||||
|
|
@ -31,11 +40,17 @@ defmodule Pleroma.Instances.Instance do
|
|||
|
||||
def changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at])
|
||||
|> cast(params, __schema__(:fields) -- [:metadata])
|
||||
|> cast_embed(:metadata, with: &metadata_changeset/2)
|
||||
|> validate_required([:host])
|
||||
|> unique_constraint(:host)
|
||||
end
|
||||
|
||||
def metadata_changeset(struct, params \\ %{}) do
|
||||
struct
|
||||
|> cast(params, [:software_name, :software_version, :software_repository])
|
||||
end
|
||||
|
||||
def filter_reachable([]), do: %{}
|
||||
|
||||
def filter_reachable(urls_or_hosts) when is_list(urls_or_hosts) do
|
||||
|
|
@ -198,6 +213,89 @@ defmodule Pleroma.Instances.Instance do
|
|||
end
|
||||
end
|
||||
|
||||
def get_or_update_metadata(%URI{host: host} = instance_uri) do
|
||||
existing_record = Repo.get_by(Instance, %{host: host})
|
||||
now = NaiveDateTime.utc_now()
|
||||
|
||||
if existing_record && existing_record.metadata_updated_at &&
|
||||
NaiveDateTime.diff(now, existing_record.metadata_updated_at) < 86_400 do
|
||||
existing_record.metadata
|
||||
else
|
||||
metadata = scrape_metadata(instance_uri)
|
||||
|
||||
if existing_record do
|
||||
existing_record
|
||||
|> changeset(%{metadata: metadata, metadata_updated_at: now})
|
||||
|> Repo.update()
|
||||
else
|
||||
%Instance{}
|
||||
|> changeset(%{host: host, metadata: metadata, metadata_updated_at: now})
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
metadata
|
||||
end
|
||||
end
|
||||
|
||||
defp get_nodeinfo_uri(well_known) do
|
||||
links = Map.get(well_known, "links", [])
|
||||
|
||||
nodeinfo21 =
|
||||
Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.1"))["href"]
|
||||
|
||||
nodeinfo20 =
|
||||
Enum.find(links, &(&1["rel"] == "http://nodeinfo.diaspora.software/ns/schema/2.0"))["href"]
|
||||
|
||||
cond do
|
||||
is_binary(nodeinfo21) -> {:ok, nodeinfo21}
|
||||
is_binary(nodeinfo20) -> {:ok, nodeinfo20}
|
||||
true -> {:error, :no_links}
|
||||
end
|
||||
end
|
||||
|
||||
defp scrape_metadata(%URI{} = instance_uri) do
|
||||
try do
|
||||
with {_, true} <- {:reachable, reachable?(instance_uri.host)},
|
||||
{:ok, %Tesla.Env{body: well_known_body}} <-
|
||||
instance_uri
|
||||
|> URI.merge("/.well-known/nodeinfo")
|
||||
|> to_string()
|
||||
|> Pleroma.HTTP.get([{"accept", "application/json"}]),
|
||||
{:ok, well_known_json} <- Jason.decode(well_known_body),
|
||||
{:ok, nodeinfo_uri} <- get_nodeinfo_uri(well_known_json),
|
||||
{:ok, %Tesla.Env{body: nodeinfo_body}} <-
|
||||
Pleroma.HTTP.get(nodeinfo_uri, [{"accept", "application/json"}]),
|
||||
{:ok, nodeinfo} <- Jason.decode(nodeinfo_body) do
|
||||
# Can extract more metadata from NodeInfo but need to be careful about it's size,
|
||||
# can't just dump the entire thing
|
||||
software = Map.get(nodeinfo, "software", %{})
|
||||
|
||||
%{
|
||||
software_name: software["name"],
|
||||
software_version: software["version"]
|
||||
}
|
||||
|> Maps.put_if_present(:software_repository, software["repository"])
|
||||
else
|
||||
{:reachable, false} ->
|
||||
Logger.debug(
|
||||
"Instance.scrape_metadata(\"#{to_string(instance_uri)}\") ignored unreachable host"
|
||||
)
|
||||
|
||||
nil
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.warn(
|
||||
"Instance.scrape_metadata(\"#{to_string(instance_uri)}\") error: #{inspect(e)}"
|
||||
)
|
||||
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Deletes all users from an instance in a background task, thus also deleting
|
||||
all of those users' activities and notifications.
|
||||
|
|
|
|||
|
|
@ -178,6 +178,7 @@ defmodule Pleroma.Notification do
|
|||
from([_n, a, o] in query,
|
||||
where:
|
||||
fragment("not(?->>'content' ~* ?)", o.data, ^regex) or
|
||||
fragment("?->>'content' is null", o.data) or
|
||||
fragment("?->>'actor' = ?", o.data, ^user.ap_id)
|
||||
)
|
||||
end
|
||||
|
|
@ -338,14 +339,6 @@ defmodule Pleroma.Notification do
|
|||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def destroy_multiple_from_types(%{id: user_id}, types) do
|
||||
from(n in Notification,
|
||||
where: n.user_id == ^user_id,
|
||||
where: n.type in ^types
|
||||
)
|
||||
|> Repo.delete_all()
|
||||
end
|
||||
|
||||
def dismiss(%Pleroma.Activity{} = activity) do
|
||||
Notification
|
||||
|> where([n], n.activity_id == ^activity.id)
|
||||
|
|
@ -559,7 +552,9 @@ defmodule Pleroma.Notification do
|
|||
end
|
||||
|
||||
def get_potential_receiver_ap_ids(%{data: %{"type" => "Flag", "actor" => actor}}) do
|
||||
(User.all_superusers() |> Enum.map(fn user -> user.ap_id end)) -- [actor]
|
||||
(User.all_users_with_privilege(:reports_manage_reports)
|
||||
|> Enum.map(fn user -> user.ap_id end)) --
|
||||
[actor]
|
||||
end
|
||||
|
||||
# Update activity: notify all who repeated this
|
||||
|
|
@ -685,7 +680,7 @@ defmodule Pleroma.Notification do
|
|||
cond do
|
||||
opts[:type] == "poll" -> false
|
||||
user.ap_id == actor -> false
|
||||
!User.following?(follower, user) -> true
|
||||
!User.following?(user, follower) -> true
|
||||
true -> false
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -425,4 +425,30 @@ defmodule Pleroma.Object do
|
|||
end
|
||||
|
||||
def object_data_hashtags(_), do: []
|
||||
|
||||
def get_emoji_reactions(object) do
|
||||
reactions = object.data["reactions"]
|
||||
|
||||
if is_list(reactions) or is_map(reactions) do
|
||||
reactions
|
||||
|> Enum.map(fn
|
||||
[_emoji, users, _maybe_url] = item when is_list(users) ->
|
||||
item
|
||||
|
||||
[emoji, users] when is_list(users) ->
|
||||
[emoji, users, nil]
|
||||
|
||||
# This case is here to process the Map situation, which will happen
|
||||
# only with the legacy two-value format.
|
||||
{emoji, users} when is_list(users) ->
|
||||
[emoji, users, nil]
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,77 +8,30 @@ defmodule Pleroma.Object.Fetcher do
|
|||
alias Pleroma.Maps
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.Signature
|
||||
alias Pleroma.Web.ActivityPub.InternalFetchActor
|
||||
alias Pleroma.Web.ActivityPub.MRF
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidator
|
||||
alias Pleroma.Web.ActivityPub.Pipeline
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.Federator
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
defp touch_changeset(changeset) do
|
||||
updated_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
|
||||
end
|
||||
|
||||
defp maybe_reinject_internal_fields(%{data: %{} = old_data}, new_data) do
|
||||
has_history? = fn
|
||||
%{"formerRepresentations" => %{"orderedItems" => list}} when is_list(list) -> true
|
||||
_ -> false
|
||||
end
|
||||
|
||||
internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
|
||||
|
||||
remote_history_exists? = has_history?.(new_data)
|
||||
|
||||
# If the remote history exists, we treat that as the only source of truth.
|
||||
new_data =
|
||||
if has_history?.(old_data) and not remote_history_exists? do
|
||||
Map.put(new_data, "formerRepresentations", old_data["formerRepresentations"])
|
||||
else
|
||||
new_data
|
||||
end
|
||||
|
||||
# If the remote does not have history information, we need to manage it ourselves
|
||||
new_data =
|
||||
if not remote_history_exists? do
|
||||
changed? =
|
||||
Pleroma.Constants.status_updatable_fields()
|
||||
|> Enum.any?(fn field -> Map.get(old_data, field) != Map.get(new_data, field) end)
|
||||
|
||||
%{updated_object: updated_object} =
|
||||
new_data
|
||||
|> Object.Updater.maybe_update_history(old_data,
|
||||
updated: changed?,
|
||||
use_history_in_new_object?: false
|
||||
)
|
||||
|
||||
updated_object
|
||||
else
|
||||
new_data
|
||||
end
|
||||
|
||||
Map.merge(new_data, internal_fields)
|
||||
end
|
||||
|
||||
defp maybe_reinject_internal_fields(_, new_data), do: new_data
|
||||
|
||||
@spec reinject_object(struct(), map()) :: {:ok, Object.t()} | {:error, any()}
|
||||
defp reinject_object(%Object{data: %{"type" => "Question"}} = object, new_data) do
|
||||
defp reinject_object(%Object{data: %{}} = object, new_data) do
|
||||
Logger.debug("Reinjecting object #{new_data["id"]}")
|
||||
|
||||
with data <- maybe_reinject_internal_fields(object, new_data),
|
||||
{:ok, data, _} <- ObjectValidator.validate(data, %{}),
|
||||
changeset <- Object.change(object, %{data: data}),
|
||||
changeset <- touch_changeset(changeset),
|
||||
{:ok, object} <- Repo.insert_or_update(changeset),
|
||||
{:ok, object} <- Object.set_cache(object) do
|
||||
{:ok, object}
|
||||
with {:ok, new_data, _} <- ObjectValidator.validate(new_data, %{}),
|
||||
{:ok, new_data} <- MRF.filter(new_data),
|
||||
{:ok, new_object, _} <-
|
||||
Object.Updater.do_update_and_invalidate_cache(
|
||||
object,
|
||||
new_data,
|
||||
_touch_changeset? = true
|
||||
) do
|
||||
{:ok, new_object}
|
||||
else
|
||||
e ->
|
||||
Logger.error("Error while processing object: #{inspect(e)}")
|
||||
|
|
@ -86,20 +39,11 @@ defmodule Pleroma.Object.Fetcher do
|
|||
end
|
||||
end
|
||||
|
||||
defp reinject_object(%Object{} = object, new_data) do
|
||||
Logger.debug("Reinjecting object #{new_data["id"]}")
|
||||
|
||||
with new_data <- Transmogrifier.fix_object(new_data),
|
||||
data <- maybe_reinject_internal_fields(object, new_data),
|
||||
changeset <- Object.change(object, %{data: data}),
|
||||
changeset <- touch_changeset(changeset),
|
||||
{:ok, object} <- Repo.insert_or_update(changeset),
|
||||
{:ok, object} <- Object.set_cache(object) do
|
||||
defp reinject_object(_, new_data) do
|
||||
with {:ok, object, _} <- Pipeline.common_pipeline(new_data, local: false) do
|
||||
{:ok, object}
|
||||
else
|
||||
e ->
|
||||
Logger.error("Error while processing object: #{inspect(e)}")
|
||||
{:error, e}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,9 @@
|
|||
defmodule Pleroma.Object.Updater do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Repo
|
||||
|
||||
def update_content_fields(orig_object_data, updated_object) do
|
||||
Pleroma.Constants.status_updatable_fields()
|
||||
|> Enum.reduce(
|
||||
|
|
@ -97,12 +100,14 @@ defmodule Pleroma.Object.Updater do
|
|||
end
|
||||
|
||||
defp maybe_update_poll(to_be_updated, updated_object) do
|
||||
choice_key = fn data ->
|
||||
if Map.has_key?(data, "anyOf"), do: "anyOf", else: "oneOf"
|
||||
choice_key = fn
|
||||
%{"anyOf" => [_ | _]} -> "anyOf"
|
||||
%{"oneOf" => [_ | _]} -> "oneOf"
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
with true <- to_be_updated["type"] == "Question",
|
||||
key <- choice_key.(updated_object),
|
||||
key when not is_nil(key) <- choice_key.(updated_object),
|
||||
true <- key == choice_key.(to_be_updated),
|
||||
orig_choices <- to_be_updated[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||
new_choices <- updated_object[key] |> Enum.map(&Map.drop(&1, ["replies"])),
|
||||
|
|
@ -237,4 +242,49 @@ defmodule Pleroma.Object.Updater do
|
|||
{:history_items, e} -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_touch_changeset(changeset, true) do
|
||||
updated_at =
|
||||
NaiveDateTime.utc_now()
|
||||
|> NaiveDateTime.truncate(:second)
|
||||
|
||||
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
|
||||
end
|
||||
|
||||
defp maybe_touch_changeset(changeset, _), do: changeset
|
||||
|
||||
def do_update_and_invalidate_cache(orig_object, updated_object, touch_changeset? \\ false) do
|
||||
orig_object_ap_id = updated_object["id"]
|
||||
orig_object_data = orig_object.data
|
||||
|
||||
%{
|
||||
updated_data: updated_object_data,
|
||||
updated: updated,
|
||||
used_history_in_new_object?: used_history_in_new_object?
|
||||
} = make_new_object_data_from_update_object(orig_object_data, updated_object)
|
||||
|
||||
changeset =
|
||||
orig_object
|
||||
|> Repo.preload(:hashtags)
|
||||
|> Object.change(%{data: updated_object_data})
|
||||
|> maybe_touch_changeset(touch_changeset?)
|
||||
|
||||
with {:ok, new_object} <- Repo.update(changeset),
|
||||
{:ok, _} <- Object.invalid_object_cache(new_object),
|
||||
{:ok, _} <- Object.set_cache(new_object),
|
||||
# The metadata/utils.ex uses the object id for the cache.
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
|
||||
if used_history_in_new_object? do
|
||||
with create_activity when not is_nil(create_activity) <-
|
||||
Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
|
||||
nil
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
{:ok, new_object, updated}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -40,7 +40,11 @@ defmodule Pleroma.ScheduledActivity do
|
|||
%{changes: %{params: %{"media_ids" => media_ids} = params}} = changeset
|
||||
)
|
||||
when is_list(media_ids) do
|
||||
media_attachments = Utils.attachments_from_ids(%{media_ids: media_ids})
|
||||
media_attachments =
|
||||
Utils.attachments_from_ids(
|
||||
%{media_ids: media_ids},
|
||||
User.get_cached_by_id(changeset.data.user_id)
|
||||
)
|
||||
|
||||
params =
|
||||
params
|
||||
|
|
|
|||
|
|
@ -38,9 +38,9 @@ defmodule Pleroma.Upload.Filter do
|
|||
{:ok, :noop} ->
|
||||
filter(rest, upload)
|
||||
|
||||
error ->
|
||||
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(error)}")
|
||||
error
|
||||
{:error, e} ->
|
||||
Logger.error("#{__MODULE__}: Filter #{filter} failed: #{inspect(e)}")
|
||||
{:error, e}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,7 +33,10 @@ defmodule Pleroma.Upload.Filter.Exiftool.ReadDescription do
|
|||
defp read_when_empty(_, file, tag) do
|
||||
try do
|
||||
{tag_content, 0} =
|
||||
System.cmd("exiftool", ["-b", "-s3", tag, file], stderr_to_stdout: true, parallelism: true)
|
||||
System.cmd("exiftool", ["-b", "-s3", tag, file],
|
||||
stderr_to_stdout: false,
|
||||
parallelism: true
|
||||
)
|
||||
|
||||
tag_content = String.trim(tag_content)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ defmodule Pleroma.Upload.Filter.Exiftool.StripLocation do
|
|||
# Formats not compatible with exiftool at this time
|
||||
def filter(%Pleroma.Upload{content_type: "image/heic"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/webp"}), do: {:ok, :noop}
|
||||
def filter(%Pleroma.Upload{content_type: "image/svg" <> _}), do: {:ok, :noop}
|
||||
|
||||
def filter(%Pleroma.Upload{tempfile: file, content_type: "image" <> _}) do
|
||||
try do
|
||||
|
|
|
|||
20
lib/pleroma/upload/filter/only_media.ex
Normal file
20
lib/pleroma/upload/filter/only_media.ex
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Upload.Filter.OnlyMedia do
|
||||
@behaviour Pleroma.Upload.Filter
|
||||
alias Pleroma.Upload
|
||||
|
||||
def filter(%Upload{content_type: content_type}) do
|
||||
[type, _subtype] = String.split(content_type, "/")
|
||||
|
||||
if type in ["image", "video", "audio"] do
|
||||
{:ok, :noop}
|
||||
else
|
||||
{:error, "Disallowed content-type: #{content_type}"}
|
||||
end
|
||||
end
|
||||
|
||||
def filter(_), do: {:ok, :noop}
|
||||
end
|
||||
|
|
@ -124,7 +124,6 @@ defmodule Pleroma.User do
|
|||
field(:domain_blocks, {:array, :string}, default: [])
|
||||
field(:is_active, :boolean, default: true)
|
||||
field(:no_rich_text, :boolean, default: false)
|
||||
field(:ap_enabled, :boolean, default: false)
|
||||
field(:is_moderator, :boolean, default: false)
|
||||
field(:is_admin, :boolean, default: false)
|
||||
field(:show_role, :boolean, default: true)
|
||||
|
|
@ -326,7 +325,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def visible_for(%User{} = user, for_user) do
|
||||
if superuser?(for_user) do
|
||||
if privileged?(for_user, :users_manage_activation_state) do
|
||||
:visible
|
||||
else
|
||||
visible_account_status(user)
|
||||
|
|
@ -353,10 +352,45 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
@spec superuser?(User.t()) :: boolean()
|
||||
def superuser?(%User{local: true, is_admin: true}), do: true
|
||||
def superuser?(%User{local: true, is_moderator: true}), do: true
|
||||
def superuser?(_), do: false
|
||||
@spec privileged?(User.t(), atom()) :: boolean()
|
||||
def privileged?(%User{is_admin: false, is_moderator: false}, _), do: false
|
||||
|
||||
def privileged?(
|
||||
%User{local: true, is_admin: is_admin, is_moderator: is_moderator},
|
||||
privilege_tag
|
||||
),
|
||||
do:
|
||||
privileged_for?(privilege_tag, is_admin, :admin_privileges) or
|
||||
privileged_for?(privilege_tag, is_moderator, :moderator_privileges)
|
||||
|
||||
def privileged?(_, _), do: false
|
||||
|
||||
defp privileged_for?(privilege_tag, true, config_role_key),
|
||||
do: privilege_tag in Config.get([:instance, config_role_key])
|
||||
|
||||
defp privileged_for?(_, _, _), do: false
|
||||
|
||||
@spec privileges(User.t()) :: [atom()]
|
||||
def privileges(%User{local: false}) do
|
||||
[]
|
||||
end
|
||||
|
||||
def privileges(%User{is_moderator: false, is_admin: false}) do
|
||||
[]
|
||||
end
|
||||
|
||||
def privileges(%User{local: true, is_moderator: true, is_admin: true}) do
|
||||
(Config.get([:instance, :moderator_privileges]) ++ Config.get([:instance, :admin_privileges]))
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
def privileges(%User{local: true, is_moderator: true, is_admin: false}) do
|
||||
Config.get([:instance, :moderator_privileges])
|
||||
end
|
||||
|
||||
def privileges(%User{local: true, is_moderator: false, is_admin: true}) do
|
||||
Config.get([:instance, :admin_privileges])
|
||||
end
|
||||
|
||||
@spec invisible?(User.t()) :: boolean()
|
||||
def invisible?(%User{invisible: true}), do: true
|
||||
|
|
@ -453,7 +487,6 @@ defmodule Pleroma.User do
|
|||
:nickname,
|
||||
:public_key,
|
||||
:avatar,
|
||||
:ap_enabled,
|
||||
:banner,
|
||||
:is_locked,
|
||||
:last_refreshed_at,
|
||||
|
|
@ -1026,11 +1059,7 @@ defmodule Pleroma.User do
|
|||
end
|
||||
|
||||
def maybe_direct_follow(%User{} = follower, %User{} = followed) do
|
||||
if not ap_enabled?(followed) do
|
||||
follow(follower, followed)
|
||||
else
|
||||
{:ok, follower, followed}
|
||||
end
|
||||
{:ok, follower, followed}
|
||||
end
|
||||
|
||||
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
|
||||
|
|
@ -1167,24 +1196,10 @@ defmodule Pleroma.User do
|
|||
|> update_and_set_cache()
|
||||
end
|
||||
|
||||
def update_and_set_cache(%{data: %Pleroma.User{} = user} = changeset) do
|
||||
was_superuser_before_update = User.superuser?(user)
|
||||
|
||||
def update_and_set_cache(changeset) do
|
||||
with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do
|
||||
set_cache(user)
|
||||
end
|
||||
|> maybe_remove_report_notifications(was_superuser_before_update)
|
||||
end
|
||||
|
||||
defp maybe_remove_report_notifications({:ok, %Pleroma.User{} = user} = result, true) do
|
||||
if not User.superuser?(user),
|
||||
do: user |> Notification.destroy_multiple_from_types(["pleroma:report"])
|
||||
|
||||
result
|
||||
end
|
||||
|
||||
defp maybe_remove_report_notifications(result, _) do
|
||||
result
|
||||
end
|
||||
|
||||
def get_user_friends_ap_ids(user) do
|
||||
|
|
@ -1877,7 +1892,6 @@ defmodule Pleroma.User do
|
|||
confirmation_token: nil,
|
||||
domain_blocks: [],
|
||||
is_active: false,
|
||||
ap_enabled: false,
|
||||
is_moderator: false,
|
||||
is_admin: false,
|
||||
mascot: nil,
|
||||
|
|
@ -2130,10 +2144,6 @@ defmodule Pleroma.User do
|
|||
end
|
||||
end
|
||||
|
||||
def ap_enabled?(%User{local: true}), do: true
|
||||
def ap_enabled?(%User{ap_enabled: ap_enabled}), do: ap_enabled
|
||||
def ap_enabled?(_), do: false
|
||||
|
||||
@doc "Gets or fetch a user by uri or nickname."
|
||||
@spec get_or_fetch(String.t()) :: {:ok, User.t()} | {:error, String.t()}
|
||||
def get_or_fetch("http://" <> _host = uri), do: get_or_fetch_by_ap_id(uri)
|
||||
|
|
@ -2265,6 +2275,11 @@ defmodule Pleroma.User do
|
|||
|> Repo.all()
|
||||
end
|
||||
|
||||
@spec all_users_with_privilege(atom()) :: [User.t()]
|
||||
def all_users_with_privilege(privilege) do
|
||||
User.Query.build(%{is_privileged: privilege}) |> Repo.all()
|
||||
end
|
||||
|
||||
def muting_reblogs?(%User{} = user, %User{} = target) do
|
||||
UserRelationship.reblog_mute_exists?(user, target)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,12 +9,14 @@ defmodule Pleroma.User.Backup do
|
|||
import Ecto.Query
|
||||
import Pleroma.Web.Gettext
|
||||
|
||||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Activity
|
||||
alias Pleroma.Bookmark
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
alias Pleroma.User.Backup.State
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.UserView
|
||||
|
|
@ -25,6 +27,8 @@ defmodule Pleroma.User.Backup do
|
|||
field(:file_name, :string)
|
||||
field(:file_size, :integer, default: 0)
|
||||
field(:processed, :boolean, default: false)
|
||||
field(:state, State, default: :invalid)
|
||||
field(:processed_number, :integer, default: 0)
|
||||
|
||||
belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
|
||||
|
||||
|
|
@ -46,7 +50,8 @@ defmodule Pleroma.User.Backup do
|
|||
%__MODULE__{
|
||||
user_id: user.id,
|
||||
content_type: "application/zip",
|
||||
file_name: name
|
||||
file_name: name,
|
||||
state: :pending
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -109,27 +114,108 @@ defmodule Pleroma.User.Backup do
|
|||
|
||||
def get(id), do: Repo.get(__MODULE__, id)
|
||||
|
||||
defp set_state(backup, state, processed_number \\ nil) do
|
||||
struct =
|
||||
%{state: state}
|
||||
|> Pleroma.Maps.put_if_present(:processed_number, processed_number)
|
||||
|
||||
backup
|
||||
|> cast(struct, [:state, :processed_number])
|
||||
|> Repo.update()
|
||||
end
|
||||
|
||||
def process(%__MODULE__{} = backup) do
|
||||
with {:ok, zip_file} <- export(backup),
|
||||
set_state(backup, :running, 0)
|
||||
|
||||
current_pid = self()
|
||||
|
||||
task =
|
||||
Task.Supervisor.async_nolink(
|
||||
Pleroma.TaskSupervisor,
|
||||
__MODULE__,
|
||||
:do_process,
|
||||
[backup, current_pid]
|
||||
)
|
||||
|
||||
wait_backup(backup, backup.processed_number, task)
|
||||
end
|
||||
|
||||
def do_process(backup, current_pid) do
|
||||
with {:ok, zip_file} <- export(backup, current_pid),
|
||||
{:ok, %{size: size}} <- File.stat(zip_file),
|
||||
{:ok, _upload} <- upload(backup, zip_file) do
|
||||
backup
|
||||
|> cast(%{file_size: size, processed: true}, [:file_size, :processed])
|
||||
|> cast(
|
||||
%{
|
||||
file_size: size,
|
||||
processed: true,
|
||||
state: :complete
|
||||
},
|
||||
[:file_size, :processed, :state]
|
||||
)
|
||||
|> Repo.update()
|
||||
end
|
||||
end
|
||||
|
||||
defp wait_backup(backup, current_processed, task) do
|
||||
wait_time = Pleroma.Config.get([__MODULE__, :process_wait_time])
|
||||
|
||||
receive do
|
||||
{:progress, new_processed} ->
|
||||
total_processed = current_processed + new_processed
|
||||
|
||||
set_state(backup, :running, total_processed)
|
||||
wait_backup(backup, total_processed, task)
|
||||
|
||||
{:DOWN, _ref, _proc, _pid, reason} ->
|
||||
backup = get(backup.id)
|
||||
|
||||
if reason != :normal do
|
||||
Logger.error("Backup #{backup.id} process ended abnormally: #{inspect(reason)}")
|
||||
|
||||
{:ok, backup} = set_state(backup, :failed)
|
||||
|
||||
cleanup(backup)
|
||||
|
||||
{:error,
|
||||
%{
|
||||
backup: backup,
|
||||
reason: :exit,
|
||||
details: reason
|
||||
}}
|
||||
else
|
||||
{:ok, backup}
|
||||
end
|
||||
after
|
||||
wait_time ->
|
||||
Logger.error(
|
||||
"Backup #{backup.id} timed out after no response for #{wait_time}ms, terminating"
|
||||
)
|
||||
|
||||
Task.Supervisor.terminate_child(Pleroma.TaskSupervisor, task.pid)
|
||||
|
||||
{:ok, backup} = set_state(backup, :failed)
|
||||
|
||||
cleanup(backup)
|
||||
|
||||
{:error,
|
||||
%{
|
||||
backup: backup,
|
||||
reason: :timeout
|
||||
}}
|
||||
end
|
||||
end
|
||||
|
||||
@files ['actor.json', 'outbox.json', 'likes.json', 'bookmarks.json']
|
||||
def export(%__MODULE__{} = backup) do
|
||||
def export(%__MODULE__{} = backup, caller_pid) do
|
||||
backup = Repo.preload(backup, :user)
|
||||
name = String.trim_trailing(backup.file_name, ".zip")
|
||||
dir = dir(name)
|
||||
dir = backup_tempdir(backup)
|
||||
|
||||
with :ok <- File.mkdir(dir),
|
||||
:ok <- actor(dir, backup.user),
|
||||
:ok <- statuses(dir, backup.user),
|
||||
:ok <- likes(dir, backup.user),
|
||||
:ok <- bookmarks(dir, backup.user),
|
||||
:ok <- actor(dir, backup.user, caller_pid),
|
||||
:ok <- statuses(dir, backup.user, caller_pid),
|
||||
:ok <- likes(dir, backup.user, caller_pid),
|
||||
:ok <- bookmarks(dir, backup.user, caller_pid),
|
||||
{:ok, zip_path} <- :zip.create(String.to_charlist(dir <> ".zip"), @files, cwd: dir),
|
||||
{:ok, _} <- File.rm_rf(dir) do
|
||||
{:ok, to_string(zip_path)}
|
||||
|
|
@ -157,11 +243,12 @@ defmodule Pleroma.User.Backup do
|
|||
end
|
||||
end
|
||||
|
||||
defp actor(dir, user) do
|
||||
defp actor(dir, user, caller_pid) do
|
||||
with {:ok, json} <-
|
||||
UserView.render("user.json", %{user: user})
|
||||
|> Map.merge(%{"likes" => "likes.json", "bookmarks" => "bookmarks.json"})
|
||||
|> Jason.encode() do
|
||||
send(caller_pid, {:progress, 1})
|
||||
File.write(Path.join(dir, "actor.json"), json)
|
||||
end
|
||||
end
|
||||
|
|
@ -180,47 +267,80 @@ defmodule Pleroma.User.Backup do
|
|||
)
|
||||
end
|
||||
|
||||
defp write(query, dir, name, fun) do
|
||||
defp should_report?(num, chunk_size), do: rem(num, chunk_size) == 0
|
||||
|
||||
defp backup_tempdir(backup) do
|
||||
name = String.trim_trailing(backup.file_name, ".zip")
|
||||
dir(name)
|
||||
end
|
||||
|
||||
defp cleanup(backup) do
|
||||
dir = backup_tempdir(backup)
|
||||
File.rm_rf(dir)
|
||||
end
|
||||
|
||||
defp write(query, dir, name, fun, caller_pid) do
|
||||
path = Path.join(dir, "#{name}.json")
|
||||
|
||||
chunk_size = Pleroma.Config.get([__MODULE__, :process_chunk_size])
|
||||
|
||||
with {:ok, file} <- File.open(path, [:write, :utf8]),
|
||||
:ok <- write_header(file, name) do
|
||||
total =
|
||||
query
|
||||
|> Pleroma.Repo.chunk_stream(100)
|
||||
|> Pleroma.Repo.chunk_stream(chunk_size, _returns_as = :one, timeout: :infinity)
|
||||
|> Enum.reduce(0, fn i, acc ->
|
||||
with {:ok, data} <- fun.(i),
|
||||
with {:ok, data} <-
|
||||
(try do
|
||||
fun.(i)
|
||||
rescue
|
||||
e -> {:error, e}
|
||||
end),
|
||||
{:ok, str} <- Jason.encode(data),
|
||||
:ok <- IO.write(file, str <> ",\n") do
|
||||
if should_report?(acc + 1, chunk_size) do
|
||||
send(caller_pid, {:progress, chunk_size})
|
||||
end
|
||||
|
||||
acc + 1
|
||||
else
|
||||
_ -> acc
|
||||
{:error, e} ->
|
||||
Logger.warn(
|
||||
"Error processing backup item: #{inspect(e)}\n The item is: #{inspect(i)}"
|
||||
)
|
||||
|
||||
acc
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
end)
|
||||
|
||||
send(caller_pid, {:progress, rem(total, chunk_size)})
|
||||
|
||||
with :ok <- :file.pwrite(file, {:eof, -2}, "\n],\n \"totalItems\": #{total}}") do
|
||||
File.close(file)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp bookmarks(dir, %{id: user_id} = _user) do
|
||||
defp bookmarks(dir, %{id: user_id} = _user, caller_pid) do
|
||||
Bookmark
|
||||
|> where(user_id: ^user_id)
|
||||
|> join(:inner, [b], activity in assoc(b, :activity))
|
||||
|> select([b, a], %{id: b.id, object: fragment("(?)->>'object'", a.data)})
|
||||
|> write(dir, "bookmarks", fn a -> {:ok, a.object} end)
|
||||
|> write(dir, "bookmarks", fn a -> {:ok, a.object} end, caller_pid)
|
||||
end
|
||||
|
||||
defp likes(dir, user) do
|
||||
defp likes(dir, user, caller_pid) do
|
||||
user.ap_id
|
||||
|> Activity.Queries.by_actor()
|
||||
|> Activity.Queries.by_type("Like")
|
||||
|> select([like], %{id: like.id, object: fragment("(?)->>'object'", like.data)})
|
||||
|> write(dir, "likes", fn a -> {:ok, a.object} end)
|
||||
|> write(dir, "likes", fn a -> {:ok, a.object} end, caller_pid)
|
||||
end
|
||||
|
||||
defp statuses(dir, user) do
|
||||
defp statuses(dir, user, caller_pid) do
|
||||
opts =
|
||||
%{}
|
||||
|> Map.put(:type, ["Create", "Announce"])
|
||||
|
|
@ -233,10 +353,15 @@ defmodule Pleroma.User.Backup do
|
|||
]
|
||||
|> Enum.concat()
|
||||
|> ActivityPub.fetch_activities_query(opts)
|
||||
|> write(dir, "outbox", fn a ->
|
||||
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
|
||||
{:ok, Map.delete(activity, "@context")}
|
||||
end
|
||||
end)
|
||||
|> write(
|
||||
dir,
|
||||
"outbox",
|
||||
fn a ->
|
||||
with {:ok, activity} <- Transmogrifier.prepare_outgoing(a.data) do
|
||||
{:ok, Map.delete(activity, "@context")}
|
||||
end
|
||||
end,
|
||||
caller_pid
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ defmodule Pleroma.User.Query do
|
|||
import Ecto.Query
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.FollowingRelationship
|
||||
alias Pleroma.User
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ defmodule Pleroma.User.Query do
|
|||
is_suggested: boolean(),
|
||||
is_discoverable: boolean(),
|
||||
super_users: boolean(),
|
||||
is_privileged: atom(),
|
||||
invisible: boolean(),
|
||||
internal: boolean(),
|
||||
followers: User.t(),
|
||||
|
|
@ -136,6 +138,43 @@ defmodule Pleroma.User.Query do
|
|||
)
|
||||
end
|
||||
|
||||
defp compose_query({:is_privileged, privilege}, query) do
|
||||
moderator_privileged = privilege in Config.get([:instance, :moderator_privileges])
|
||||
admin_privileged = privilege in Config.get([:instance, :admin_privileges])
|
||||
|
||||
query = compose_query({:active, true}, query)
|
||||
query = compose_query({:local, true}, query)
|
||||
|
||||
case {admin_privileged, moderator_privileged} do
|
||||
{false, false} ->
|
||||
where(
|
||||
query,
|
||||
false
|
||||
)
|
||||
|
||||
{true, true} ->
|
||||
where(
|
||||
query,
|
||||
[u],
|
||||
u.is_admin or u.is_moderator
|
||||
)
|
||||
|
||||
{true, false} ->
|
||||
where(
|
||||
query,
|
||||
[u],
|
||||
u.is_admin
|
||||
)
|
||||
|
||||
{false, true} ->
|
||||
where(
|
||||
query,
|
||||
[u],
|
||||
u.is_moderator
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
defp compose_query({:local, _}, query), do: location_query(query, true)
|
||||
|
||||
defp compose_query({:external, _}, query), do: location_query(query, false)
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp increase_replies_count_if_reply(_create_data), do: :noop
|
||||
|
||||
@object_types ~w[ChatMessage Question Answer Audio Video Event Article Note Page]
|
||||
@object_types ~w[ChatMessage Question Answer Audio Video Image Event Article Note Page]
|
||||
@impl true
|
||||
def persist(%{"type" => type} = object, meta) when type in @object_types do
|
||||
with {:ok, object} <- Object.create(object) do
|
||||
|
|
@ -404,11 +404,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
_ <- notify_and_stream(activity),
|
||||
:ok <-
|
||||
maybe_federate(stripped_activity) do
|
||||
User.all_superusers()
|
||||
User.all_users_with_privilege(:reports_manage_reports)
|
||||
|> Enum.filter(fn user -> user.ap_id != actor end)
|
||||
|> Enum.filter(fn user -> not is_nil(user.email) end)
|
||||
|> Enum.each(fn superuser ->
|
||||
superuser
|
||||
|> Enum.each(fn privileged_user ->
|
||||
privileged_user
|
||||
|> Pleroma.Emails.AdminEmail.report(actor, account, statuses, content)
|
||||
|> Pleroma.Emails.Mailer.deliver_async()
|
||||
end)
|
||||
|
|
@ -458,6 +458,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|> maybe_preload_objects(opts)
|
||||
|> maybe_preload_bookmarks(opts)
|
||||
|> maybe_set_thread_muted_field(opts)
|
||||
|> restrict_unauthenticated(opts[:user])
|
||||
|> restrict_blocked(opts)
|
||||
|> restrict_blockers_visibility(opts)
|
||||
|> restrict_recipients(recipients, opts[:user])
|
||||
|
|
@ -1218,6 +1219,27 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
defp restrict_filtered(query, _), do: query
|
||||
|
||||
defp restrict_unauthenticated(query, nil) do
|
||||
local = Config.restrict_unauthenticated_access?(:activities, :local)
|
||||
remote = Config.restrict_unauthenticated_access?(:activities, :remote)
|
||||
|
||||
cond do
|
||||
local and remote ->
|
||||
from(activity in query, where: false)
|
||||
|
||||
local ->
|
||||
from(activity in query, where: activity.local == false)
|
||||
|
||||
remote ->
|
||||
from(activity in query, where: activity.local == true)
|
||||
|
||||
true ->
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp restrict_unauthenticated(query, _), do: query
|
||||
|
||||
defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query
|
||||
|
||||
defp exclude_poll_votes(query, _) do
|
||||
|
|
@ -1456,13 +1478,22 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
|
||||
@spec upload(Upload.source(), keyword()) :: {:ok, Object.t()} | {:error, any()}
|
||||
def upload(file, opts \\ []) do
|
||||
with {:ok, data} <- Upload.store(file, opts) do
|
||||
with {:ok, data} <- Upload.store(sanitize_upload_file(file), opts) do
|
||||
obj_data = Maps.put_if_present(data, "actor", opts[:actor])
|
||||
|
||||
Repo.insert(%Object{data: obj_data})
|
||||
end
|
||||
end
|
||||
|
||||
defp sanitize_upload_file(%Plug.Upload{filename: filename} = upload) when is_binary(filename) do
|
||||
%Plug.Upload{
|
||||
upload
|
||||
| filename: Path.basename(filename)
|
||||
}
|
||||
end
|
||||
|
||||
defp sanitize_upload_file(upload), do: upload
|
||||
|
||||
@spec get_actor_url(any()) :: binary() | nil
|
||||
defp get_actor_url(url) when is_binary(url), do: url
|
||||
defp get_actor_url(%{"href" => href}) when is_binary(href), do: href
|
||||
|
|
@ -1541,7 +1572,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
%{
|
||||
ap_id: data["id"],
|
||||
uri: get_actor_url(data["url"]),
|
||||
ap_enabled: true,
|
||||
banner: normalize_image(data["image"]),
|
||||
fields: fields,
|
||||
emoji: emojis,
|
||||
|
|
@ -1662,7 +1692,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end
|
||||
end
|
||||
|
||||
def fetch_and_prepare_user_from_ap_id(ap_id, additional \\ []) do
|
||||
defp fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id),
|
||||
{:ok, data} <- user_data_from_user_object(data, additional) do
|
||||
{:ok, maybe_update_follow_information(data)}
|
||||
|
|
@ -1715,6 +1745,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
end)
|
||||
end
|
||||
|
||||
def pin_data_from_featured_collection(obj) do
|
||||
Logger.error("Could not parse featured collection #{inspect(obj)}")
|
||||
%{}
|
||||
end
|
||||
|
||||
def fetch_and_prepare_featured_from_ap_id(nil) do
|
||||
{:ok, %{}}
|
||||
end
|
||||
|
|
@ -1745,24 +1780,20 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
def make_user_from_ap_id(ap_id, additional \\ []) do
|
||||
user = User.get_cached_by_ap_id(ap_id)
|
||||
|
||||
if user && !User.ap_enabled?(user) do
|
||||
Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
else
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
with {:ok, data} <- fetch_and_prepare_user_from_ap_id(ap_id, additional) do
|
||||
{:ok, _pid} = Task.start(fn -> pinned_fetch_task(data) end)
|
||||
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
if user do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
else
|
||||
maybe_handle_clashing_nickname(data)
|
||||
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
data
|
||||
|> User.remote_user_changeset()
|
||||
|> Repo.insert()
|
||||
|> User.set_cache()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI.ActivityDraft
|
||||
alias Pleroma.Web.Endpoint
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
|
|
@ -54,13 +55,87 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
{:ok, data, []}
|
||||
end
|
||||
|
||||
defp unicode_emoji_react(_object, data, emoji) do
|
||||
data
|
||||
|> Map.put("content", emoji)
|
||||
|> Map.put("type", "EmojiReact")
|
||||
end
|
||||
|
||||
defp add_emoji_content(data, emoji, url) do
|
||||
tag = [
|
||||
%{
|
||||
"id" => url,
|
||||
"type" => "Emoji",
|
||||
"name" => Emoji.maybe_quote(emoji),
|
||||
"icon" => %{
|
||||
"type" => "Image",
|
||||
"url" => url
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
data
|
||||
|> Map.put("content", Emoji.maybe_quote(emoji))
|
||||
|> Map.put("type", "EmojiReact")
|
||||
|> Map.put("tag", tag)
|
||||
end
|
||||
|
||||
defp remote_custom_emoji_react(
|
||||
%{data: %{"reactions" => existing_reactions}},
|
||||
data,
|
||||
emoji
|
||||
) do
|
||||
[emoji_code, instance] = String.split(Emoji.maybe_strip_name(emoji), "@")
|
||||
|
||||
matching_reaction =
|
||||
Enum.find(
|
||||
existing_reactions,
|
||||
fn [name, _, url] ->
|
||||
if url != nil do
|
||||
url = URI.parse(url)
|
||||
url.host == instance && name == emoji_code
|
||||
end
|
||||
end
|
||||
)
|
||||
|
||||
if matching_reaction do
|
||||
[name, _, url] = matching_reaction
|
||||
add_emoji_content(data, name, url)
|
||||
else
|
||||
{:error, "Could not react"}
|
||||
end
|
||||
end
|
||||
|
||||
defp remote_custom_emoji_react(_object, _data, _emoji) do
|
||||
{:error, "Could not react"}
|
||||
end
|
||||
|
||||
defp local_custom_emoji_react(data, emoji) do
|
||||
with %{file: path} = emojo <- Emoji.get(emoji) do
|
||||
url = "#{Endpoint.url()}#{path}"
|
||||
add_emoji_content(data, emojo.code, url)
|
||||
else
|
||||
_ -> {:error, "Emoji does not exist"}
|
||||
end
|
||||
end
|
||||
|
||||
defp custom_emoji_react(object, data, emoji) do
|
||||
if String.contains?(emoji, "@") do
|
||||
remote_custom_emoji_react(object, data, emoji)
|
||||
else
|
||||
local_custom_emoji_react(data, emoji)
|
||||
end
|
||||
end
|
||||
|
||||
@spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
|
||||
def emoji_react(actor, object, emoji) do
|
||||
with {:ok, data, meta} <- object_action(actor, object) do
|
||||
data =
|
||||
data
|
||||
|> Map.put("content", emoji)
|
||||
|> Map.put("type", "EmojiReact")
|
||||
if Emoji.is_unicode_emoji?(emoji) do
|
||||
unicode_emoji_react(object, data, emoji)
|
||||
else
|
||||
custom_emoji_react(object, data, emoji)
|
||||
end
|
||||
|
||||
{:ok, data, meta}
|
||||
end
|
||||
|
|
@ -142,6 +217,7 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
"tag" => Keyword.values(draft.tags) |> Enum.uniq()
|
||||
}
|
||||
|> add_in_reply_to(draft.in_reply_to)
|
||||
|> add_quote(draft.quote_post)
|
||||
|> Map.merge(draft.extra)
|
||||
|
||||
{:ok, data, []}
|
||||
|
|
@ -157,6 +233,16 @@ defmodule Pleroma.Web.ActivityPub.Builder do
|
|||
end
|
||||
end
|
||||
|
||||
defp add_quote(object, nil), do: object
|
||||
|
||||
defp add_quote(object, quote_post) do
|
||||
with %Object{} = quote_object <- Object.normalize(quote_post, fetch: false) do
|
||||
Map.put(object, "quoteUrl", quote_object.data["id"])
|
||||
else
|
||||
_ -> object
|
||||
end
|
||||
end
|
||||
|
||||
def chat_message(actor, recipient, content, opts \\ []) do
|
||||
basic = %{
|
||||
"id" => Utils.generate_object_id(),
|
||||
|
|
|
|||
281
lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
Normal file
281
lib/pleroma/web/activity_pub/mrf/emoji_policy.ex
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.EmojiPolicy do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Object.Updater
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or force-unlisted emojis with certain URLs or names"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp config_remove_url do
|
||||
Pleroma.Config.get([:mrf_emoji, :remove_url], [])
|
||||
end
|
||||
|
||||
defp config_remove_shortcode do
|
||||
Pleroma.Config.get([:mrf_emoji, :remove_shortcode], [])
|
||||
end
|
||||
|
||||
defp config_unlist_url do
|
||||
Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_url], [])
|
||||
end
|
||||
|
||||
defp config_unlist_shortcode do
|
||||
Pleroma.Config.get([:mrf_emoji, :federated_timeline_removal_shortcode], [])
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :manual
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => type, "object" => %{"type" => objtype} = object} = message)
|
||||
when type in ["Create", "Update"] and objtype in Pleroma.Constants.status_object_types() do
|
||||
with {:ok, object} <-
|
||||
Updater.do_with_history(object, fn object ->
|
||||
{:ok, process_remove(object, :url, config_remove_url())}
|
||||
end),
|
||||
{:ok, object} <-
|
||||
Updater.do_with_history(object, fn object ->
|
||||
{:ok, process_remove(object, :shortcode, config_remove_shortcode())}
|
||||
end),
|
||||
activity <- Map.put(message, "object", object),
|
||||
activity <- maybe_delist(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => type} = object) when type in Pleroma.Constants.actor_types() do
|
||||
with object <- process_remove(object, :url, config_remove_url()),
|
||||
object <- process_remove(object, :shortcode, config_remove_shortcode()) do
|
||||
{:ok, object}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"type" => "EmojiReact"} = object) do
|
||||
with {:ok, _} <-
|
||||
matched_emoji_checker(config_remove_url(), config_remove_shortcode()).(object) do
|
||||
{:ok, object}
|
||||
else
|
||||
_ ->
|
||||
{:reject, "[EmojiPolicy] Rejected for having disallowed emoji"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(message) do
|
||||
{:ok, message}
|
||||
end
|
||||
|
||||
defp match_string?(string, pattern) when is_binary(pattern) do
|
||||
string == pattern
|
||||
end
|
||||
|
||||
defp match_string?(string, %Regex{} = pattern) do
|
||||
String.match?(string, pattern)
|
||||
end
|
||||
|
||||
defp match_any?(string, patterns) do
|
||||
Enum.any?(patterns, &match_string?(string, &1))
|
||||
end
|
||||
|
||||
defp url_from_tag(%{"icon" => %{"url" => url}}), do: url
|
||||
defp url_from_tag(_), do: nil
|
||||
|
||||
defp url_from_emoji({_name, url}), do: url
|
||||
|
||||
defp shortcode_from_tag(%{"name" => name}) when is_binary(name), do: String.trim(name, ":")
|
||||
defp shortcode_from_tag(_), do: nil
|
||||
|
||||
defp shortcode_from_emoji({name, _url}), do: name
|
||||
|
||||
defp process_remove(object, :url, patterns) do
|
||||
process_remove_impl(object, &url_from_tag/1, &url_from_emoji/1, patterns)
|
||||
end
|
||||
|
||||
defp process_remove(object, :shortcode, patterns) do
|
||||
process_remove_impl(object, &shortcode_from_tag/1, &shortcode_from_emoji/1, patterns)
|
||||
end
|
||||
|
||||
defp process_remove_impl(object, extract_from_tag, extract_from_emoji, patterns) do
|
||||
object =
|
||||
if object["tag"] do
|
||||
Map.put(
|
||||
object,
|
||||
"tag",
|
||||
Enum.filter(
|
||||
object["tag"],
|
||||
fn
|
||||
%{"type" => "Emoji"} = tag ->
|
||||
str = extract_from_tag.(tag)
|
||||
|
||||
if is_binary(str) do
|
||||
not match_any?(str, patterns)
|
||||
else
|
||||
true
|
||||
end
|
||||
|
||||
_ ->
|
||||
true
|
||||
end
|
||||
)
|
||||
)
|
||||
else
|
||||
object
|
||||
end
|
||||
|
||||
object =
|
||||
if object["emoji"] do
|
||||
Map.put(
|
||||
object,
|
||||
"emoji",
|
||||
object["emoji"]
|
||||
|> Enum.reduce(%{}, fn {name, url} = emoji, acc ->
|
||||
if not match_any?(extract_from_emoji.(emoji), patterns) do
|
||||
Map.put(acc, name, url)
|
||||
else
|
||||
acc
|
||||
end
|
||||
end)
|
||||
)
|
||||
else
|
||||
object
|
||||
end
|
||||
|
||||
object
|
||||
end
|
||||
|
||||
defp matched_emoji_checker(urls, shortcodes) do
|
||||
fn object ->
|
||||
if any_emoji_match?(object, &url_from_tag/1, &url_from_emoji/1, urls) or
|
||||
any_emoji_match?(
|
||||
object,
|
||||
&shortcode_from_tag/1,
|
||||
&shortcode_from_emoji/1,
|
||||
shortcodes
|
||||
) do
|
||||
{:matched, nil}
|
||||
else
|
||||
{:ok, %{}}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_delist(%{"object" => object, "to" => to, "type" => "Create"} = activity) do
|
||||
check = matched_emoji_checker(config_unlist_url(), config_unlist_shortcode())
|
||||
|
||||
should_delist? = fn object ->
|
||||
with {:ok, _} <- Pleroma.Object.Updater.do_with_history(object, check) do
|
||||
false
|
||||
else
|
||||
_ -> true
|
||||
end
|
||||
end
|
||||
|
||||
if Pleroma.Constants.as_public() in to and should_delist?.(object) do
|
||||
to = List.delete(to, Pleroma.Constants.as_public())
|
||||
cc = [Pleroma.Constants.as_public() | activity["cc"] || []]
|
||||
|
||||
activity
|
||||
|> Map.put("to", to)
|
||||
|> Map.put("cc", cc)
|
||||
else
|
||||
activity
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_delist(activity), do: activity
|
||||
|
||||
defp any_emoji_match?(object, extract_from_tag, extract_from_emoji, patterns) do
|
||||
Kernel.||(
|
||||
Enum.any?(
|
||||
object["tag"] || [],
|
||||
fn
|
||||
%{"type" => "Emoji"} = tag ->
|
||||
str = extract_from_tag.(tag)
|
||||
|
||||
if is_binary(str) do
|
||||
match_any?(str, patterns)
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
_ ->
|
||||
false
|
||||
end
|
||||
),
|
||||
(object["emoji"] || [])
|
||||
|> Enum.any?(fn emoji -> match_any?(extract_from_emoji.(emoji), patterns) end)
|
||||
)
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def describe do
|
||||
mrf_emoji =
|
||||
Pleroma.Config.get(:mrf_emoji, [])
|
||||
|> Enum.map(fn {key, value} ->
|
||||
{key, Enum.map(value, &Utils.describe_regex_or_string/1)}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
{:ok, %{mrf_emoji: mrf_emoji}}
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_emoji,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.EmojiPolicy",
|
||||
label: "MRF Emoji",
|
||||
description:
|
||||
"Reject or force-unlisted emojis whose URLs or names match a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
|
||||
children: [
|
||||
%{
|
||||
key: :remove_url,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in emoji whose URL matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :remove_shortcode,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in emoji whose shortcode matches being removed from the message. This will apply to statuses, emoji reactions, and user profiles.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal_url,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message with emojis whose URLs match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["https://example.org/foo.png", ~r/example.org\/foo/iu]
|
||||
},
|
||||
%{
|
||||
key: :federated_timeline_removal_shortcode,
|
||||
type: {:list, :string},
|
||||
description: """
|
||||
A list of patterns which result in message with emojis whose shortcodes match being removed from federated timelines (a.k.a unlisted). This will apply only to statuses.
|
||||
|
||||
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
|
||||
""",
|
||||
suggestions: ["foo", ~r/foo/iu]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
||||
|
|
@ -95,11 +95,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.ForceMentionsInContent do
|
|||
|> Enum.reject(&is_nil/1)
|
||||
|> sort_replied_user(replied_to_user)
|
||||
|
||||
explicitly_mentioned_uris = extract_mention_uris_from_content(content)
|
||||
explicitly_mentioned_uris =
|
||||
extract_mention_uris_from_content(content)
|
||||
|> MapSet.new()
|
||||
|
||||
added_mentions =
|
||||
Enum.reduce(mention_users, "", fn %User{ap_id: uri} = user, acc ->
|
||||
unless uri in explicitly_mentioned_uris do
|
||||
Enum.reduce(mention_users, "", fn %User{ap_id: ap_id, uri: uri} = user, acc ->
|
||||
if MapSet.disjoint?(MapSet.new([ap_id, uri]), explicitly_mentioned_uris) do
|
||||
acc <> Formatter.mention_from_user(user, %{mentions_format: :compact}) <> " "
|
||||
else
|
||||
acc
|
||||
|
|
|
|||
78
lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
Normal file
78
lib/pleroma/web/activity_pub/mrf/inline_quote_policy.ex
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2021 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy do
|
||||
@moduledoc "Force a quote line into the message content."
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
defp build_inline_quote(template, url) do
|
||||
quote_line = String.replace(template, "{url}", "<a href=\"#{url}\">#{url}</a>")
|
||||
|
||||
"<span class=\"quote-inline\"><br/><br/>#{quote_line}</span>"
|
||||
end
|
||||
|
||||
defp has_inline_quote?(content, quote_url) do
|
||||
cond do
|
||||
# Does the quote URL exist in the content?
|
||||
content =~ quote_url -> true
|
||||
# Does the content already have a .quote-inline span?
|
||||
content =~ "<span class=\"quote-inline\">" -> true
|
||||
# No inline quote found
|
||||
true -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||
content = object["content"] || ""
|
||||
|
||||
if has_inline_quote?(content, quote_url) do
|
||||
object
|
||||
else
|
||||
template = Pleroma.Config.get([:mrf_inline_quote, :template])
|
||||
|
||||
content =
|
||||
if String.ends_with?(content, "</p>"),
|
||||
do:
|
||||
String.trim_trailing(content, "</p>") <>
|
||||
build_inline_quote(template, quote_url) <> "</p>",
|
||||
else: content <> build_inline_quote(template, quote_url)
|
||||
|
||||
Map.put(object, "content", content)
|
||||
end
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl true
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :auto
|
||||
|
||||
@impl true
|
||||
def config_description do
|
||||
%{
|
||||
key: :mrf_inline_quote,
|
||||
related_policy: "Pleroma.Web.ActivityPub.MRF.InlineQuotePolicy",
|
||||
label: "MRF Inline Quote Policy",
|
||||
type: :group,
|
||||
description: "Force quote url to appear in post content.",
|
||||
children: [
|
||||
%{
|
||||
key: :template,
|
||||
type: :string,
|
||||
description:
|
||||
"The template to append to the post. `{url}` will be replaced with the actual link to the quoted post.",
|
||||
suggestions: ["<bdi>RT:</bdi> {url}"]
|
||||
}
|
||||
]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -5,6 +5,8 @@
|
|||
defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
||||
require Pleroma.Constants
|
||||
|
||||
alias Pleroma.Web.ActivityPub.MRF.Utils
|
||||
|
||||
@moduledoc "Reject or Word-Replace messages with a keyword or regex"
|
||||
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
|
@ -128,7 +130,6 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
|
||||
@impl true
|
||||
def describe do
|
||||
# This horror is needed to convert regex sigils to strings
|
||||
mrf_keyword =
|
||||
Pleroma.Config.get(:mrf_keyword, [])
|
||||
|> Enum.map(fn {key, value} ->
|
||||
|
|
@ -136,21 +137,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do
|
|||
Enum.map(value, fn
|
||||
{pattern, replacement} ->
|
||||
%{
|
||||
"pattern" =>
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end,
|
||||
"pattern" => Utils.describe_regex_or_string(pattern),
|
||||
"replacement" => replacement
|
||||
}
|
||||
|
||||
pattern ->
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
Utils.describe_regex_or_string(pattern)
|
||||
end)}
|
||||
end)
|
||||
|> Enum.into(%{})
|
||||
|
|
|
|||
49
lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
Normal file
49
lib/pleroma/web/activity_pub/mrf/quote_to_link_tag_policy.ex
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.QuoteToLinkTagPolicy do
|
||||
@moduledoc "Force a Link tag for posts quoting another post. (may break outgoing federation of quote posts with older Pleroma versions)"
|
||||
@behaviour Pleroma.Web.ActivityPub.MRF.Policy
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(%{"object" => %{"quoteUrl" => _} = object} = activity) do
|
||||
{:ok, Map.put(activity, "object", filter_object(object))}
|
||||
end
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def filter(object), do: {:ok, object}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def describe, do: {:ok, %{}}
|
||||
|
||||
@impl Pleroma.Web.ActivityPub.MRF.Policy
|
||||
def history_awareness, do: :auto
|
||||
|
||||
defp filter_object(%{"quoteUrl" => quote_url} = object) do
|
||||
tags = object["tag"] || []
|
||||
|
||||
if Enum.any?(tags, fn tag ->
|
||||
CommonFixes.is_object_link_tag(tag) and tag["href"] == quote_url
|
||||
end) do
|
||||
object
|
||||
else
|
||||
object
|
||||
|> Map.put(
|
||||
"tag",
|
||||
tags ++
|
||||
[
|
||||
%{
|
||||
"type" => "Link",
|
||||
"mediaType" => Pleroma.Constants.activity_json_canonical_mime_type(),
|
||||
"href" => quote_url
|
||||
}
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
15
lib/pleroma/web/activity_pub/mrf/utils.ex
Normal file
15
lib/pleroma/web/activity_pub/mrf/utils.ex
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.MRF.Utils do
|
||||
@spec describe_regex_or_string(String.t() | Regex.t()) :: String.t()
|
||||
def describe_regex_or_string(pattern) do
|
||||
# This horror is needed to convert regex sigils to strings
|
||||
if not is_binary(pattern) do
|
||||
inspect(pattern)
|
||||
else
|
||||
pattern
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -21,7 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnnounceValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AnswerValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.BlockValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.ChatMessageValidator
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator
|
||||
|
|
@ -102,7 +102,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype} = object} = create_activity,
|
||||
meta
|
||||
)
|
||||
when objtype in ~w[Question Answer Audio Video Event Article Note Page] do
|
||||
when objtype in ~w[Question Answer Audio Video Image Event Article Note Page] do
|
||||
with {:ok, object_data} <- cast_and_apply_and_stringify_with_history(object),
|
||||
meta = Keyword.put(meta, :object_data, object_data),
|
||||
{:ok, create_activity} <-
|
||||
|
|
@ -115,13 +115,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
end
|
||||
|
||||
def validate(%{"type" => type} = object, meta)
|
||||
when type in ~w[Event Question Audio Video Article Note Page] do
|
||||
when type in ~w[Event Question Audio Video Image Article Note Page] do
|
||||
validator =
|
||||
case type do
|
||||
"Event" -> EventValidator
|
||||
"Question" -> QuestionValidator
|
||||
"Audio" -> AudioVideoValidator
|
||||
"Video" -> AudioVideoValidator
|
||||
"Audio" -> AudioImageVideoValidator
|
||||
"Video" -> AudioImageVideoValidator
|
||||
"Image" -> AudioImageVideoValidator
|
||||
"Article" -> ArticleNotePageValidator
|
||||
"Note" -> ArticleNotePageValidator
|
||||
"Page" -> ArticleNotePageValidator
|
||||
|
|
@ -233,8 +234,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
|
|||
AnswerValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Video] do
|
||||
AudioVideoValidator.cast_and_apply(object)
|
||||
def cast_and_apply(%{"type" => type} = object) when type in ~w[Audio Image Video] do
|
||||
AudioImageVideoValidator.cast_and_apply(object)
|
||||
end
|
||||
|
||||
def cast_and_apply(%{"type" => "Event"} = object) do
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AddRemoveValidator do
|
|||
end
|
||||
|
||||
defp maybe_refetch_user(%User{ap_id: ap_id}) do
|
||||
Pleroma.Web.ActivityPub.Transmogrifier.upgrade_user_from_ap_id(ap_id)
|
||||
# Maybe it could use User.get_or_fetch_by_ap_id to avoid refreshing too often
|
||||
User.fetch_by_ap_id(ap_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.ArticleNotePageValidator do
|
|||
|> fix_tag()
|
||||
|> fix_replies()
|
||||
|> fix_attachments()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> Transmogrifier.fix_content_map()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioImageVideoValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
|
|
@ -55,9 +55,14 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
url
|
||||
|> Enum.concat(mpeg_url["tag"] || [])
|
||||
|> Enum.find(fn
|
||||
%{"mediaType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
%{"mimeType" => mime_type} -> String.starts_with?(mime_type, ["video/", "audio/"])
|
||||
_ -> false
|
||||
%{"mediaType" => mime_type} ->
|
||||
String.starts_with?(mime_type, ["video/", "audio/", "image/"])
|
||||
|
||||
%{"mimeType" => mime_type} ->
|
||||
String.starts_with?(mime_type, ["video/", "audio/", "image/"])
|
||||
|
||||
_ ->
|
||||
false
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
@ -94,6 +99,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_url()
|
||||
|> fix_content()
|
||||
|
|
@ -110,7 +116,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.AudioVideoValidator do
|
|||
|
||||
defp validate_data(data_cng) do
|
||||
data_cng
|
||||
|> validate_inclusion(:type, ["Audio", "Video"])
|
||||
|> validate_inclusion(:type, ~w[Audio Image Video])
|
||||
|> validate_required([:id, :actor, :attributedTo, :type, :context])
|
||||
|> CommonValidations.validate_any_presence([:cc, :to])
|
||||
|> CommonValidations.validate_fields_match([:actor, :attributedTo])
|
||||
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||
end
|
||||
end
|
||||
|
||||
# All objects except Answer and CHatMessage
|
||||
# All objects except Answer and ChatMessage
|
||||
defmacro object_fields do
|
||||
quote bind_quoted: binding() do
|
||||
field(:content, :string)
|
||||
|
|
@ -58,7 +58,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFields do
|
|||
field(:like_count, :integer, default: 0)
|
||||
field(:announcement_count, :integer, default: 0)
|
||||
field(:inReplyTo, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.Uri)
|
||||
field(:quoteUrl, ObjectValidators.ObjectID)
|
||||
field(:url, ObjectValidators.BareUri)
|
||||
|
||||
field(:likes, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
field(:announcements, {:array, ObjectValidators.ObjectID}, default: [])
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
def cast_and_filter_recipients(message, field, follower_collection, field_fallback \\ []) do
|
||||
{:ok, data} = ObjectValidators.Recipients.cast(message[field] || field_fallback)
|
||||
|
||||
|
|
@ -76,4 +78,48 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes do
|
|||
|
||||
Map.put(data, "to", to)
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"quoteUrl" => _quote_url} = data), do: data
|
||||
|
||||
# Fedibird
|
||||
# https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||
def fix_quote_url(%{"quoteUri" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
# Old Fedibird (bug)
|
||||
# https://github.com/fedibird/mastodon/issues/9
|
||||
def fix_quote_url(%{"quoteURL" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
# Misskey fallback
|
||||
def fix_quote_url(%{"_misskey_quote" => quote_url} = data) do
|
||||
Map.put(data, "quoteUrl", quote_url)
|
||||
end
|
||||
|
||||
def fix_quote_url(%{"tag" => [_ | _] = tags} = data) do
|
||||
tag = Enum.find(tags, &is_object_link_tag/1)
|
||||
|
||||
if not is_nil(tag) do
|
||||
data
|
||||
|> Map.put("quoteUrl", tag["href"])
|
||||
else
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
def fix_quote_url(data), do: data
|
||||
|
||||
# https://codeberg.org/fediverse/fep/src/branch/main/fep/e232/fep-e232.md
|
||||
def is_object_link_tag(%{
|
||||
"type" => "Link",
|
||||
"mediaType" => media_type,
|
||||
"href" => href
|
||||
})
|
||||
when media_type in Pleroma.Constants.activity_json_mime_types() and is_binary(href) do
|
||||
true
|
||||
end
|
||||
|
||||
def is_object_link_tag(_), do: false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -136,11 +136,11 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
|
|||
|
||||
# This figures out if a user is able to create, delete or modify something
|
||||
# based on the domain and superuser status
|
||||
@spec validate_modification_rights(Ecto.Changeset.t()) :: Ecto.Changeset.t()
|
||||
def validate_modification_rights(cng) do
|
||||
@spec validate_modification_rights(Ecto.Changeset.t(), atom()) :: Ecto.Changeset.t()
|
||||
def validate_modification_rights(cng, privilege) do
|
||||
actor = User.get_cached_by_ap_id(get_field(cng, :actor))
|
||||
|
||||
if User.superuser?(actor) || same_domain?(cng) do
|
||||
if User.privileged?(actor, privilege) || same_domain?(cng) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
|
|||
|> validate_required([:id, :type, :actor, :to, :cc, :object])
|
||||
|> validate_inclusion(:type, ["Delete"])
|
||||
|> validate_delete_actor(:actor)
|
||||
|> validate_modification_rights()
|
||||
|> validate_modification_rights(:messages_delete)
|
||||
|> validate_object_or_user_presence(allowed_types: @deletable_types)
|
||||
|> add_deleted_activity_id()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,8 +5,10 @@
|
|||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
||||
use Ecto.Schema
|
||||
|
||||
alias Pleroma.Emoji
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes
|
||||
alias Pleroma.Web.ActivityPub.ObjectValidators.TagValidator
|
||||
|
||||
import Ecto.Changeset
|
||||
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
|
||||
|
|
@ -19,6 +21,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
import Elixir.Pleroma.Web.ActivityPub.ObjectValidators.CommonFields
|
||||
message_fields()
|
||||
activity_fields()
|
||||
embeds_many(:tag, TagValidator)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -43,7 +46,8 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|
||||
def changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, __schema__(:fields))
|
||||
|> cast(data, __schema__(:fields) -- [:tag])
|
||||
|> cast_embed(:tag)
|
||||
end
|
||||
|
||||
defp fix(data) do
|
||||
|
|
@ -53,12 +57,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_activity_addressing()
|
||||
|
||||
with %Object{} = object <- Object.normalize(data["object"]) do
|
||||
data
|
||||
|> CommonFixes.fix_activity_context(object)
|
||||
|> CommonFixes.fix_object_action_recipients(object)
|
||||
else
|
||||
_ -> data
|
||||
data = Map.put_new(data, "tag", [])
|
||||
|
||||
case Object.normalize(data["object"]) do
|
||||
%Object{} = object ->
|
||||
data
|
||||
|> CommonFixes.fix_activity_context(object)
|
||||
|> CommonFixes.fix_object_action_recipients(object)
|
||||
|
||||
_ ->
|
||||
data
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -82,11 +90,31 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
defp validate_emoji(cng) do
|
||||
content = get_field(cng, :content)
|
||||
|
||||
if Pleroma.Emoji.is_unicode_emoji?(content) do
|
||||
if Emoji.is_unicode_emoji?(content) || Emoji.is_custom_emoji?(content) do
|
||||
cng
|
||||
else
|
||||
cng
|
||||
|> add_error(:content, "must be a single character emoji")
|
||||
|> add_error(:content, "is not a valid emoji")
|
||||
end
|
||||
end
|
||||
|
||||
defp maybe_validate_tag_presence(cng) do
|
||||
content = get_field(cng, :content)
|
||||
|
||||
if Emoji.is_unicode_emoji?(content) do
|
||||
cng
|
||||
else
|
||||
tag = get_field(cng, :tag)
|
||||
emoji_name = Emoji.maybe_strip_name(content)
|
||||
|
||||
case tag do
|
||||
[%{name: ^emoji_name, type: "Emoji", icon: %{url: _}}] ->
|
||||
cng
|
||||
|
||||
_ ->
|
||||
cng
|
||||
|> add_error(:tag, "does not contain an Emoji tag")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -97,5 +125,6 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
|
|||
|> validate_actor_presence()
|
||||
|> validate_object_presence()
|
||||
|> validate_emoji()
|
||||
|> maybe_validate_tag_presence()
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.QuestionValidator do
|
|||
data
|
||||
|> CommonFixes.fix_actor()
|
||||
|> CommonFixes.fix_object_defaults()
|
||||
|> CommonFixes.fix_quote_url()
|
||||
|> Transmogrifier.fix_emoji()
|
||||
|> fix_closed()
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,15 +9,20 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|
||||
import Ecto.Changeset
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@primary_key false
|
||||
embedded_schema do
|
||||
# Common
|
||||
field(:type, :string)
|
||||
field(:name, :string)
|
||||
|
||||
# Mention, Hashtag
|
||||
# Mention, Hashtag, Link
|
||||
field(:href, ObjectValidators.Uri)
|
||||
|
||||
# Link
|
||||
field(:mediaType, :string)
|
||||
|
||||
# Emoji
|
||||
embeds_one :icon, IconObjectValidator, primary_key: false do
|
||||
field(:type, :string)
|
||||
|
|
@ -68,6 +73,19 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.TagValidator do
|
|||
|> validate_required([:type, :name, :icon])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => "Link"} = data) do
|
||||
struct
|
||||
|> cast(data, [:type, :name, :mediaType, :href])
|
||||
|> validate_inclusion(:mediaType, Pleroma.Constants.activity_json_mime_types())
|
||||
|> validate_required([:type, :href, :mediaType])
|
||||
end
|
||||
|
||||
def changeset(struct, %{"type" => _} = data) do
|
||||
struct
|
||||
|> cast(data, [])
|
||||
|> Map.put(:action, :ignore)
|
||||
end
|
||||
|
||||
def icon_changeset(struct, data) do
|
||||
struct
|
||||
|> cast(data, [:type, :url])
|
||||
|
|
|
|||
|
|
@ -199,7 +199,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
|
||||
inboxes =
|
||||
recipients
|
||||
|> Enum.filter(&User.ap_enabled?/1)
|
||||
|> Enum.map(fn actor -> actor.inbox end)
|
||||
|> Enum.filter(fn inbox -> should_federate?(inbox, public) end)
|
||||
|> Instances.filter_reachable()
|
||||
|
|
@ -241,7 +240,6 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
|
|||
json = Jason.encode!(data)
|
||||
|
||||
recipients(actor, activity)
|
||||
|> Enum.filter(fn user -> User.ap_enabled?(user) end)
|
||||
|> Enum.map(fn %User{} = user ->
|
||||
determine_inbox(activity, user)
|
||||
end)
|
||||
|
|
|
|||
|
|
@ -437,37 +437,13 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
if orig_object_data["type"] in Pleroma.Constants.updatable_object_types() do
|
||||
%{
|
||||
updated_data: updated_object_data,
|
||||
updated: updated,
|
||||
used_history_in_new_object?: used_history_in_new_object?
|
||||
} = Object.Updater.make_new_object_data_from_update_object(orig_object_data, updated_object)
|
||||
{:ok, _, updated} =
|
||||
Object.Updater.do_update_and_invalidate_cache(orig_object, updated_object)
|
||||
|
||||
changeset =
|
||||
orig_object
|
||||
|> Repo.preload(:hashtags)
|
||||
|> Object.change(%{data: updated_object_data})
|
||||
|
||||
with {:ok, new_object} <- Repo.update(changeset),
|
||||
{:ok, _} <- Object.invalid_object_cache(new_object),
|
||||
{:ok, _} <- Object.set_cache(new_object),
|
||||
# The metadata/utils.ex uses the object id for the cache.
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(new_object.id) do
|
||||
if used_history_in_new_object? do
|
||||
with create_activity when not is_nil(create_activity) <-
|
||||
Pleroma.Activity.get_create_by_object_ap_id(orig_object_ap_id),
|
||||
{:ok, _} <- Pleroma.Activity.HTML.invalidate_cache_for(create_activity.id) do
|
||||
nil
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
if updated do
|
||||
object
|
||||
|> Activity.normalize()
|
||||
|> ActivityPub.notify_and_stream()
|
||||
end
|
||||
if updated do
|
||||
object
|
||||
|> Activity.normalize()
|
||||
|> ActivityPub.notify_and_stream()
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -529,7 +505,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
|
|||
end
|
||||
|
||||
def handle_object_creation(%{"type" => objtype} = object, _activity, meta)
|
||||
when objtype in ~w[Audio Video Event Article Note Page] do
|
||||
when objtype in ~w[Audio Video Image Event Article Note Page] do
|
||||
with {:ok, object, meta} <- Pipeline.common_pipeline(object, meta) do
|
||||
{:ok, object, meta}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.Federator
|
||||
alias Pleroma.Workers.TransmogrifierWorker
|
||||
|
||||
import Ecto.Query
|
||||
|
||||
|
|
@ -167,6 +166,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def fix_in_reply_to(object, _options), do: object
|
||||
|
||||
def fix_quote_url_and_maybe_fetch(object, options \\ []) do
|
||||
quote_url =
|
||||
case Pleroma.Web.ActivityPub.ObjectValidators.CommonFixes.fix_quote_url(object) do
|
||||
%{"quoteUrl" => quote_url} -> quote_url
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
with {:quoting?, true} <- {:quoting?, not is_nil(quote_url)},
|
||||
{:ok, quoted_object} <- get_obj_helper(quote_url, options),
|
||||
%Activity{} <- Activity.get_create_by_object_ap_id(quoted_object.data["id"]) do
|
||||
Map.put(object, "quoteUrl", quoted_object.data["id"])
|
||||
else
|
||||
{:quoting?, _} ->
|
||||
object
|
||||
|
||||
e ->
|
||||
Logger.warn("Couldn't fetch #{inspect(quote_url)}, error: #{inspect(e)}")
|
||||
object
|
||||
end
|
||||
end
|
||||
|
||||
defp prepare_in_reply_to(in_reply_to) do
|
||||
cond do
|
||||
is_bitstring(in_reply_to) ->
|
||||
|
|
@ -447,7 +467,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
%{"type" => "Create", "object" => %{"type" => objtype, "id" => obj_id}} = data,
|
||||
options
|
||||
)
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page} do
|
||||
when objtype in ~w{Question Answer ChatMessage Audio Video Event Article Note Page Image} do
|
||||
fetch_options = Keyword.put(options, :depth, (options[:depth] || 0) + 1)
|
||||
|
||||
object =
|
||||
|
|
@ -455,6 +475,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> strip_internal_fields()
|
||||
|> fix_type(fetch_options)
|
||||
|> fix_in_reply_to(fetch_options)
|
||||
|> fix_quote_url_and_maybe_fetch(fetch_options)
|
||||
|
||||
data = Map.put(data, "object", object)
|
||||
options = Keyword.put(options, :local, false)
|
||||
|
|
@ -629,6 +650,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
def set_reply_to_uri(obj), do: obj
|
||||
|
||||
@doc """
|
||||
Fedibird compatibility
|
||||
https://github.com/fedibird/mastodon/commit/dbd7ae6cf58a92ec67c512296b4daaea0d01e6ac
|
||||
"""
|
||||
def set_quote_url(%{"quoteUrl" => quote_url} = object) when is_binary(quote_url) do
|
||||
Map.put(object, "quoteUri", quote_url)
|
||||
end
|
||||
|
||||
def set_quote_url(obj), do: obj
|
||||
|
||||
@doc """
|
||||
Serialized Mastodon-compatible `replies` collection containing _self-replies_.
|
||||
Based on Mastodon's ActivityPub::NoteSerializer#replies.
|
||||
|
|
@ -683,6 +714,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|> prepare_attachments
|
||||
|> set_conversation
|
||||
|> set_reply_to_uri
|
||||
|> set_quote_url
|
||||
|> set_replies
|
||||
|> strip_internal_fields
|
||||
|> strip_internal_tags
|
||||
|
|
@ -946,47 +978,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|
|||
|
||||
defp strip_internal_tags(object), do: object
|
||||
|
||||
def perform(:user_upgrade, user) do
|
||||
# we pass a fake user so that the followers collection is stripped away
|
||||
old_follower_address = User.ap_followers(%User{nickname: user.nickname})
|
||||
|
||||
from(
|
||||
a in Activity,
|
||||
where: ^old_follower_address in a.recipients,
|
||||
update: [
|
||||
set: [
|
||||
recipients:
|
||||
fragment(
|
||||
"array_replace(?,?,?)",
|
||||
a.recipients,
|
||||
^old_follower_address,
|
||||
^user.follower_address
|
||||
)
|
||||
]
|
||||
]
|
||||
)
|
||||
|> Repo.update_all([])
|
||||
end
|
||||
|
||||
def upgrade_user_from_ap_id(ap_id) do
|
||||
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
|
||||
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
|
||||
{:ok, user} <- update_user(user, data) do
|
||||
{:ok, _pid} = Task.start(fn -> ActivityPub.pinned_fetch_task(user) end)
|
||||
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
|
||||
{:ok, user}
|
||||
else
|
||||
%User{} = user -> {:ok, user}
|
||||
e -> e
|
||||
end
|
||||
end
|
||||
|
||||
defp update_user(user, data) do
|
||||
user
|
||||
|> User.remote_user_changeset(data)
|
||||
|> User.update_and_set_cache()
|
||||
end
|
||||
|
||||
def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
|
||||
Map.put(data, "url", url["href"])
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
"Page",
|
||||
"Question",
|
||||
"Answer",
|
||||
"Audio"
|
||||
"Audio",
|
||||
"Image"
|
||||
]
|
||||
@strip_status_report_states ~w(closed resolved)
|
||||
@supported_report_states ~w(open closed resolved)
|
||||
|
|
@ -325,21 +326,29 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
|
||||
|
||||
def add_emoji_reaction_to_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
||||
object
|
||||
) do
|
||||
reactions = get_cached_emoji_reactions(object)
|
||||
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
url = maybe_emoji_url(emoji, activity)
|
||||
|
||||
new_reactions =
|
||||
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
|
||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||
if is_nil(candidate_url) do
|
||||
emoji == candidate
|
||||
else
|
||||
url == candidate_url
|
||||
end
|
||||
end) do
|
||||
nil ->
|
||||
reactions ++ [[emoji, [actor]]]
|
||||
reactions ++ [[emoji, [actor], url]]
|
||||
|
||||
index ->
|
||||
List.update_at(
|
||||
reactions,
|
||||
index,
|
||||
fn [emoji, users] -> [emoji, Enum.uniq([actor | users])] end
|
||||
fn [emoji, users, url] -> [emoji, Enum.uniq([actor | users]), url] end
|
||||
)
|
||||
end
|
||||
|
||||
|
|
@ -348,18 +357,40 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
update_element_in_object("reaction", new_reactions, object, count)
|
||||
end
|
||||
|
||||
defp maybe_emoji_url(
|
||||
name,
|
||||
%Activity{
|
||||
data: %{
|
||||
"tag" => [
|
||||
%{"type" => "Emoji", "name" => name, "icon" => %{"url" => url}}
|
||||
]
|
||||
}
|
||||
}
|
||||
),
|
||||
do: url
|
||||
|
||||
defp maybe_emoji_url(_, _), do: nil
|
||||
|
||||
def emoji_count(reactions_list) do
|
||||
Enum.reduce(reactions_list, 0, fn [_, users], acc -> acc + length(users) end)
|
||||
Enum.reduce(reactions_list, 0, fn [_, users, _], acc -> acc + length(users) end)
|
||||
end
|
||||
|
||||
def remove_emoji_reaction_from_object(
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}},
|
||||
%Activity{data: %{"content" => emoji, "actor" => actor}} = activity,
|
||||
object
|
||||
) do
|
||||
emoji = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
reactions = get_cached_emoji_reactions(object)
|
||||
url = maybe_emoji_url(emoji, activity)
|
||||
|
||||
new_reactions =
|
||||
case Enum.find_index(reactions, fn [candidate, _] -> emoji == candidate end) do
|
||||
case Enum.find_index(reactions, fn [candidate, _, candidate_url] ->
|
||||
if is_nil(candidate_url) do
|
||||
emoji == candidate
|
||||
else
|
||||
url == candidate_url
|
||||
end
|
||||
end) do
|
||||
nil ->
|
||||
reactions
|
||||
|
||||
|
|
@ -367,9 +398,9 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
List.update_at(
|
||||
reactions,
|
||||
index,
|
||||
fn [emoji, users] -> [emoji, List.delete(users, actor)] end
|
||||
fn [emoji, users, url] -> [emoji, List.delete(users, actor), url] end
|
||||
)
|
||||
|> Enum.reject(fn [_, users] -> Enum.empty?(users) end)
|
||||
|> Enum.reject(fn [_, users, _] -> Enum.empty?(users) end)
|
||||
end
|
||||
|
||||
count = emoji_count(new_reactions)
|
||||
|
|
@ -377,11 +408,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
end
|
||||
|
||||
def get_cached_emoji_reactions(object) do
|
||||
if is_list(object.data["reactions"]) do
|
||||
object.data["reactions"]
|
||||
else
|
||||
[]
|
||||
end
|
||||
Object.get_emoji_reactions(object)
|
||||
end
|
||||
|
||||
@spec add_like_to_object(Activity.t(), Object.t()) ::
|
||||
|
|
@ -489,17 +516,37 @@ defmodule Pleroma.Web.ActivityPub.Utils do
|
|||
|
||||
def get_latest_reaction(internal_activity_id, %{ap_id: ap_id}, emoji) do
|
||||
%{data: %{"object" => object_ap_id}} = Activity.get_by_id(internal_activity_id)
|
||||
emoji = Pleroma.Emoji.maybe_quote(emoji)
|
||||
|
||||
"EmojiReact"
|
||||
|> Activity.Queries.by_type()
|
||||
|> where(actor: ^ap_id)
|
||||
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|
||||
|> custom_emoji_discriminator(emoji)
|
||||
|> Activity.Queries.by_object_id(object_ap_id)
|
||||
|> order_by([activity], fragment("? desc nulls last", activity.id))
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
defp custom_emoji_discriminator(query, emoji) do
|
||||
if String.contains?(emoji, "@") do
|
||||
stripped = Pleroma.Emoji.maybe_strip_name(emoji)
|
||||
[name, domain] = String.split(stripped, "@")
|
||||
domain_pattern = "%/" <> domain <> "/%"
|
||||
emoji_pattern = Pleroma.Emoji.maybe_quote(name)
|
||||
|
||||
query
|
||||
|> where([activity], fragment("?->>'content' = ?
|
||||
AND EXISTS (
|
||||
SELECT FROM jsonb_array_elements(?->'tag') elem
|
||||
WHERE elem->>'id' ILIKE ?
|
||||
)", activity.data, ^emoji_pattern, activity.data, ^domain_pattern))
|
||||
else
|
||||
query
|
||||
|> where([activity], fragment("?->>'content' = ?", activity.data, ^emoji))
|
||||
end
|
||||
end
|
||||
|
||||
#### Announce-related helpers
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -18,13 +18,24 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do
|
|||
def index(conn, _params) do
|
||||
installed = installed()
|
||||
|
||||
# FIrst get frontends from config,
|
||||
# then add frontends that are installed but not in the config
|
||||
frontends =
|
||||
[:frontends, :available]
|
||||
|> Config.get([])
|
||||
Config.get([:frontends, :available], [])
|
||||
|> Enum.map(fn {name, desc} ->
|
||||
Map.put(desc, "installed", name in installed)
|
||||
desc
|
||||
|> Map.put("installed", name in installed)
|
||||
|> Map.put("installed_refs", installed_refs(name))
|
||||
end)
|
||||
|
||||
frontends =
|
||||
frontends ++
|
||||
(installed
|
||||
|> Enum.filter(fn n -> not Enum.any?(frontends, fn f -> f["name"] == n end) end)
|
||||
|> Enum.map(fn name ->
|
||||
%{"name" => name, "installed" => true, "installed_refs" => installed_refs(name)}
|
||||
end))
|
||||
|
||||
render(conn, "index.json", frontends: frontends)
|
||||
end
|
||||
|
||||
|
|
@ -43,4 +54,12 @@ defmodule Pleroma.Web.AdminAPI.FrontendController do
|
|||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def installed_refs(name) do
|
||||
if name in installed() do
|
||||
File.ls!(Path.join(Pleroma.Frontend.dir(), name))
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ defmodule Pleroma.Web.AdminAPI.Report do
|
|||
|
||||
defp make_fake_activity(act, user) do
|
||||
%Activity{
|
||||
id: "pleroma:fake",
|
||||
id: "pleroma:fake:#{act["id"]}",
|
||||
data: %{
|
||||
"actor" => user.ap_id,
|
||||
"type" => "Create",
|
||||
|
|
|
|||
|
|
@ -15,7 +15,8 @@ defmodule Pleroma.Web.AdminAPI.FrontendView do
|
|||
git: frontend["git"],
|
||||
build_url: frontend["build_url"],
|
||||
ref: frontend["ref"],
|
||||
installed: frontend["installed"]
|
||||
installed: frontend["installed"],
|
||||
installed_refs: frontend["installed_refs"]
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,6 +10,14 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
|
||||
@behaviour OpenApi
|
||||
|
||||
defp streaming_paths do
|
||||
%{
|
||||
"/api/v1/streaming" => %OpenApiSpex.PathItem{
|
||||
get: Pleroma.Web.ApiSpec.StreamingOperation.streaming_operation()
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
@impl OpenApi
|
||||
def spec(opts \\ []) do
|
||||
%OpenApi{
|
||||
|
|
@ -45,7 +53,7 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
}
|
||||
},
|
||||
# populate the paths from a phoenix router
|
||||
paths: OpenApiSpex.Paths.from_router(Router),
|
||||
paths: Map.merge(streaming_paths(), OpenApiSpex.Paths.from_router(Router)),
|
||||
components: %OpenApiSpex.Components{
|
||||
parameters: %{
|
||||
"accountIdOrNickname" =>
|
||||
|
|
@ -95,7 +103,8 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Relays",
|
||||
"Report managment",
|
||||
"Status administration",
|
||||
"User administration"
|
||||
"User administration",
|
||||
"Announcement management"
|
||||
]
|
||||
},
|
||||
%{"name" => "Applications", "tags" => ["Applications", "Push subscriptions"]},
|
||||
|
|
@ -110,10 +119,12 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Follow requests",
|
||||
"Mascot",
|
||||
"Markers",
|
||||
"Notifications"
|
||||
"Notifications",
|
||||
"Filters",
|
||||
"Settings"
|
||||
]
|
||||
},
|
||||
%{"name" => "Instance", "tags" => ["Custom emojis"]},
|
||||
%{"name" => "Instance", "tags" => ["Custom emojis", "Instance misc"]},
|
||||
%{"name" => "Messaging", "tags" => ["Chats", "Conversations"]},
|
||||
%{
|
||||
"name" => "Statuses",
|
||||
|
|
@ -125,10 +136,21 @@ defmodule Pleroma.Web.ApiSpec do
|
|||
"Retrieve status information",
|
||||
"Scheduled statuses",
|
||||
"Search",
|
||||
"Status actions"
|
||||
"Status actions",
|
||||
"Media attachments"
|
||||
]
|
||||
},
|
||||
%{"name" => "Miscellaneous", "tags" => ["Emoji packs", "Reports", "Suggestions"]}
|
||||
%{
|
||||
"name" => "Miscellaneous",
|
||||
"tags" => [
|
||||
"Emoji packs",
|
||||
"Reports",
|
||||
"Suggestions",
|
||||
"Announcements",
|
||||
"Remote interaction",
|
||||
"Others"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -452,7 +452,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
operationId: "AccountController.blocks",
|
||||
description: "View your blocks. See also accounts/:id/{block,unblock}",
|
||||
security: [%{"oAuth" => ["read:blocks"]}],
|
||||
parameters: pagination_params(),
|
||||
parameters: [with_relationships_param() | pagination_params()],
|
||||
responses: %{
|
||||
200 => Operation.response("Accounts", "application/json", array_of_accounts())
|
||||
}
|
||||
|
|
@ -461,7 +461,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do
|
|||
|
||||
def lookup_operation do
|
||||
%Operation{
|
||||
tags: ["Account lookup"],
|
||||
tags: ["Retrieve account information"],
|
||||
summary: "Find a user by nickname",
|
||||
operationId: "AccountController.lookup",
|
||||
parameters: [
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
|
|||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement managment"],
|
||||
tags: ["Announcement management"],
|
||||
summary: "Retrieve a list of announcements",
|
||||
operationId: "AdminAPI.AnnouncementController.index",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
|
|
@ -46,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
|
|||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement managment"],
|
||||
tags: ["Announcement management"],
|
||||
summary: "Display one announcement",
|
||||
operationId: "AdminAPI.AnnouncementController.show",
|
||||
security: [%{"oAuth" => ["admin:read"]}],
|
||||
|
|
@ -69,7 +69,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
|
|||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement managment"],
|
||||
tags: ["Announcement management"],
|
||||
summary: "Delete one announcement",
|
||||
operationId: "AdminAPI.AnnouncementController.delete",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
|
|
@ -92,7 +92,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
|
|||
|
||||
def create_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement managment"],
|
||||
tags: ["Announcement management"],
|
||||
summary: "Create one announcement",
|
||||
operationId: "AdminAPI.AnnouncementController.create",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
|
|
@ -107,7 +107,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.AnnouncementOperation do
|
|||
|
||||
def change_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement managment"],
|
||||
tags: ["Announcement management"],
|
||||
summary: "Change one announcement",
|
||||
operationId: "AdminAPI.AnnouncementController.change",
|
||||
security: [%{"oAuth" => ["admin:write"]}],
|
||||
|
|
|
|||
|
|
@ -51,8 +51,9 @@ defmodule Pleroma.Web.ApiSpec.Admin.FrontendOperation do
|
|||
name: %Schema{type: :string},
|
||||
git: %Schema{type: :string, format: :uri, nullable: true},
|
||||
build_url: %Schema{type: :string, format: :uri, nullable: true},
|
||||
ref: %Schema{type: :string},
|
||||
installed: %Schema{type: :boolean}
|
||||
ref: %Schema{type: :string, nullable: true},
|
||||
installed: %Schema{type: :boolean},
|
||||
installed_refs: %Schema{type: :array, items: %Schema{type: :string}}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
|
|||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Status adminitration)"],
|
||||
tags: ["Status administration"],
|
||||
summary: "Get status",
|
||||
operationId: "AdminAPI.StatusController.show",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
|
|||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Status adminitration)"],
|
||||
tags: ["Status administration"],
|
||||
summary: "Change the scope of a status",
|
||||
operationId: "AdminAPI.StatusController.update",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -99,7 +99,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
|
|||
|
||||
def delete_operation do
|
||||
%Operation{
|
||||
tags: ["Status adminitration)"],
|
||||
tags: ["Status administration"],
|
||||
summary: "Delete status",
|
||||
operationId: "AdminAPI.StatusController.delete",
|
||||
parameters: [id_param() | admin_api_params()],
|
||||
|
|
@ -143,7 +143,7 @@ defmodule Pleroma.Web.ApiSpec.Admin.StatusOperation do
|
|||
}
|
||||
},
|
||||
tags: %Schema{type: :string},
|
||||
is_confirmed: %Schema{type: :string}
|
||||
is_confirmed: %Schema{type: :boolean}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do
|
|||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement"],
|
||||
tags: ["Announcements"],
|
||||
summary: "Retrieve a list of announcements",
|
||||
operationId: "MastodonAPI.AnnouncementController.index",
|
||||
security: [%{"oAuth" => []}],
|
||||
|
|
@ -28,7 +28,7 @@ defmodule Pleroma.Web.ApiSpec.AnnouncementOperation do
|
|||
|
||||
def mark_read_operation do
|
||||
%Operation{
|
||||
tags: ["Announcement"],
|
||||
tags: ["Announcements"],
|
||||
summary: "Mark one announcement as read",
|
||||
operationId: "MastodonAPI.AnnouncementController.mark_read",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.DirectoryOperation do
|
|||
|
||||
def index_operation do
|
||||
%Operation{
|
||||
tags: ["Directory"],
|
||||
tags: ["Others"],
|
||||
summary: "Profile directory",
|
||||
operationId: "DirectoryController.index",
|
||||
parameters:
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve instance information",
|
||||
description: "Information about the server",
|
||||
operationId: "InstanceController.show",
|
||||
|
|
@ -25,7 +25,7 @@ defmodule Pleroma.Web.ApiSpec.InstanceOperation do
|
|||
|
||||
def peers_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve list of known instances",
|
||||
operationId: "InstanceController.peers",
|
||||
responses: %{
|
||||
|
|
|
|||
|
|
@ -64,7 +64,13 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
content_type: %Schema{type: :string},
|
||||
file_name: %Schema{type: :string},
|
||||
file_size: %Schema{type: :integer},
|
||||
processed: %Schema{type: :boolean}
|
||||
processed: %Schema{type: :boolean, description: "whether this backup has succeeded"},
|
||||
state: %Schema{
|
||||
type: :string,
|
||||
description: "the state of the backup",
|
||||
enum: ["pending", "running", "complete", "failed"]
|
||||
},
|
||||
processed_number: %Schema{type: :integer, description: "the number of records processed"}
|
||||
},
|
||||
example: %{
|
||||
"content_type" => "application/zip",
|
||||
|
|
@ -72,7 +78,9 @@ defmodule Pleroma.Web.ApiSpec.PleromaBackupOperation do
|
|||
"https://cofe.fe:4000/media/backups/archive-foobar-20200908T164207-Yr7vuT5Wycv-sN3kSN2iJ0k-9pMo60j9qmvRCdDqIew.zip",
|
||||
"file_size" => 4105,
|
||||
"inserted_at" => "2020-09-08T16:42:07.000Z",
|
||||
"processed" => true
|
||||
"processed" => true,
|
||||
"state" => "complete",
|
||||
"processed_number" => 20
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -133,7 +133,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiFileOperation do
|
|||
defp files_object do
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
additionalProperties: %Schema{
|
||||
type: :string,
|
||||
description: "Filename of the emoji",
|
||||
extensions: %{"x-additionalPropertiesName": "Emoji name"}
|
||||
},
|
||||
description: "Object with emoji names as keys and filenames as values"
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -227,13 +227,29 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
|||
|
||||
defp emoji_packs_response do
|
||||
Operation.response(
|
||||
"Object with pack names as keys and pack contents as values",
|
||||
"Emoji packs and the count",
|
||||
"application/json",
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: emoji_pack(),
|
||||
properties: %{
|
||||
packs: %Schema{
|
||||
type: :object,
|
||||
description: "Object with pack names as keys and pack contents as values",
|
||||
additionalProperties: %Schema{
|
||||
emoji_pack()
|
||||
| extensions: %{"x-additionalPropertiesName": "Pack name"}
|
||||
}
|
||||
},
|
||||
count: %Schema{
|
||||
type: :integer,
|
||||
description: "Number of emoji packs"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"emojos" => emoji_pack().example
|
||||
"packs" => %{
|
||||
"emojos" => emoji_pack().example
|
||||
},
|
||||
"count" => 1
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -274,7 +290,11 @@ defmodule Pleroma.Web.ApiSpec.PleromaEmojiPackOperation do
|
|||
defp files_object do
|
||||
%Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
additionalProperties: %Schema{
|
||||
type: :string,
|
||||
description: "Filename",
|
||||
extensions: %{"x-additionalPropertiesName": "Emoji name"}
|
||||
},
|
||||
description: "Object with emoji names as keys and filenames as values"
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaInstancesOperation do
|
|||
|
||||
def show_operation do
|
||||
%Operation{
|
||||
tags: ["Instance"],
|
||||
tags: ["Instance misc"],
|
||||
summary: "Retrieve federation status",
|
||||
description: "Information about instances deemed unreachable by the server",
|
||||
operationId: "PleromaInstances.show",
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
summary: "Creates a new Listen activity for an account",
|
||||
security: [%{"oAuth" => ["write"]}],
|
||||
operationId: "PleromaAPI.ScrobbleController.create",
|
||||
deprecated: true,
|
||||
requestBody: request_body("Parameters", create_request(), requried: true),
|
||||
responses: %{
|
||||
200 => Operation.response("Scrobble", "application/json", scrobble())
|
||||
|
|
@ -34,6 +35,7 @@ defmodule Pleroma.Web.ApiSpec.PleromaScrobbleOperation do
|
|||
tags: ["Scrobbles"],
|
||||
summary: "Requests a list of current and recent Listen activities for an account",
|
||||
operationId: "PleromaAPI.ScrobbleController.index",
|
||||
deprecated: true,
|
||||
parameters: [
|
||||
%Reference{"$ref": "#/components/parameters/accountIdOrNickname"} | pagination_params()
|
||||
],
|
||||
|
|
|
|||
|
|
@ -440,7 +440,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
|
||||
def show_history_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status history"],
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Status history",
|
||||
description: "View history of a status",
|
||||
operationId: "StatusController.show_history",
|
||||
|
|
@ -457,7 +457,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
|
||||
def show_source_operation do
|
||||
%Operation{
|
||||
tags: ["Retrieve status source"],
|
||||
tags: ["Retrieve status information"],
|
||||
summary: "Status source",
|
||||
description: "View source of a status",
|
||||
operationId: "StatusController.show_source",
|
||||
|
|
@ -474,7 +474,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
|
||||
def update_operation do
|
||||
%Operation{
|
||||
tags: ["Update status"],
|
||||
tags: ["Status actions"],
|
||||
summary: "Update status",
|
||||
description: "Change the content of a status",
|
||||
operationId: "StatusController.update",
|
||||
|
|
@ -581,6 +581,11 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do
|
|||
type: :string,
|
||||
description:
|
||||
"Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`."
|
||||
},
|
||||
quote_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
|
|
|
|||
464
lib/pleroma/web/api_spec/operations/streaming_operation.ex
Normal file
464
lib/pleroma/web/api_spec/operations/streaming_operation.ex
Normal file
|
|
@ -0,0 +1,464 @@
|
|||
# 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.StreamingOperation do
|
||||
alias OpenApiSpex.Operation
|
||||
alias OpenApiSpex.Response
|
||||
alias OpenApiSpex.Schema
|
||||
alias Pleroma.Web.ApiSpec.NotificationOperation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Chat
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Conversation
|
||||
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
|
||||
alias Pleroma.Web.ApiSpec.Schemas.Status
|
||||
|
||||
require Pleroma.Constants
|
||||
|
||||
@spec open_api_operation(atom) :: Operation.t()
|
||||
def open_api_operation(action) do
|
||||
operation = String.to_existing_atom("#{action}_operation")
|
||||
apply(__MODULE__, operation, [])
|
||||
end
|
||||
|
||||
@spec streaming_operation() :: Operation.t()
|
||||
def streaming_operation do
|
||||
%Operation{
|
||||
tags: ["Timelines"],
|
||||
summary: "Establish streaming connection",
|
||||
description: """
|
||||
Receive statuses in real-time via WebSocket.
|
||||
|
||||
You can specify the access token on the query string or through the `sec-websocket-protocol` header. Using
|
||||
the query string to authenticate is considered unsafe and should not be used unless you have to (e.g. to maintain
|
||||
your client's compatibility with Mastodon).
|
||||
|
||||
You may specify a stream on the query string. If you do so and you are connecting to a stream that requires logged-in users,
|
||||
you must specify the access token at the time of the connection (i.e. via query string or header).
|
||||
|
||||
Otherwise, you have the option to authenticate after you have established the connection through client-sent events.
|
||||
|
||||
The "Request body" section below describes what events clients can send through WebSocket, and the "Responses" section
|
||||
describes what events server will send through WebSocket.
|
||||
""",
|
||||
security: [%{"oAuth" => ["read:statuses", "read:notifications"]}],
|
||||
operationId: "WebsocketHandler.streaming",
|
||||
parameters:
|
||||
[
|
||||
Operation.parameter(:connection, :header, %Schema{type: :string}, "connection header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(:upgrade, :header, %Schema{type: :string}, "upgrade header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:"sec-websocket-key",
|
||||
:header,
|
||||
%Schema{type: :string},
|
||||
"sec-websocket-key header",
|
||||
required: true
|
||||
),
|
||||
Operation.parameter(
|
||||
:"sec-websocket-version",
|
||||
:header,
|
||||
%Schema{type: :string},
|
||||
"sec-websocket-version header",
|
||||
required: true
|
||||
)
|
||||
] ++ stream_params() ++ access_token_params(),
|
||||
requestBody: request_body("Client-sent events", client_sent_events()),
|
||||
responses: %{
|
||||
101 => switching_protocols_response(),
|
||||
200 =>
|
||||
Operation.response(
|
||||
"Server-sent events",
|
||||
"application/json",
|
||||
server_sent_events()
|
||||
)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp stream_params do
|
||||
stream_specifier()
|
||||
|> Enum.map(fn {name, schema} ->
|
||||
Operation.parameter(name, :query, schema, get_schema(schema).description)
|
||||
end)
|
||||
end
|
||||
|
||||
defp access_token_params do
|
||||
[
|
||||
Operation.parameter(:access_token, :query, token(), token().description),
|
||||
Operation.parameter(:"sec-websocket-protocol", :header, token(), token().description)
|
||||
]
|
||||
end
|
||||
|
||||
defp switching_protocols_response do
|
||||
%Response{
|
||||
description: "Switching protocols",
|
||||
headers: %{
|
||||
"connection" => %OpenApiSpex.Header{required: true},
|
||||
"upgrade" => %OpenApiSpex.Header{required: true},
|
||||
"sec-websocket-accept" => %OpenApiSpex.Header{required: true}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp server_sent_events do
|
||||
%Schema{
|
||||
oneOf: [
|
||||
update_event(),
|
||||
status_update_event(),
|
||||
notification_event(),
|
||||
chat_update_event(),
|
||||
follow_relationships_update_event(),
|
||||
conversation_event(),
|
||||
delete_event(),
|
||||
pleroma_respond_event()
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp stream do
|
||||
%Schema{
|
||||
type: :array,
|
||||
title: "Stream",
|
||||
description: """
|
||||
The stream identifier.
|
||||
The first item is the name of the stream. If the stream needs a differentiator, the second item will be the corresponding identifier.
|
||||
Currently, for the following stream types, there is a second element in the array:
|
||||
|
||||
- `list`: The second element is the id of the list, as a string.
|
||||
- `hashtag`: The second element is the name of the hashtag.
|
||||
- `public:remote:media` and `public:remote`: The second element is the domain of the corresponding instance.
|
||||
""",
|
||||
maxItems: 2,
|
||||
minItems: 1,
|
||||
items: %Schema{type: :string},
|
||||
example: ["hashtag", "mew"]
|
||||
}
|
||||
end
|
||||
|
||||
defp get_schema(%Schema{} = schema), do: schema
|
||||
defp get_schema(schema), do: schema.schema
|
||||
|
||||
defp server_sent_event_helper(name, description, type, payload, opts \\ []) do
|
||||
payload_type = Keyword.get(opts, :payload_type, :json)
|
||||
has_stream = Keyword.get(opts, :has_stream, true)
|
||||
|
||||
stream_properties =
|
||||
if has_stream do
|
||||
%{stream: stream()}
|
||||
else
|
||||
%{}
|
||||
end
|
||||
|
||||
stream_example = if has_stream, do: %{"stream" => get_schema(stream()).example}, else: %{}
|
||||
|
||||
stream_required = if has_stream, do: [:stream], else: []
|
||||
|
||||
payload_schema =
|
||||
if payload_type == :json do
|
||||
%Schema{
|
||||
title: "Event payload",
|
||||
description: "JSON-encoded string of #{get_schema(payload).title}",
|
||||
allOf: [payload]
|
||||
}
|
||||
else
|
||||
payload
|
||||
end
|
||||
|
||||
payload_example =
|
||||
if payload_type == :json do
|
||||
get_schema(payload).example |> Jason.encode!()
|
||||
else
|
||||
get_schema(payload).example
|
||||
end
|
||||
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: name,
|
||||
description: description,
|
||||
required: [:event, :payload] ++ stream_required,
|
||||
properties:
|
||||
%{
|
||||
event: %Schema{
|
||||
title: "Event type",
|
||||
description: "Type of the event.",
|
||||
type: :string,
|
||||
required: true,
|
||||
enum: [type]
|
||||
},
|
||||
payload: payload_schema
|
||||
}
|
||||
|> Map.merge(stream_properties),
|
||||
example:
|
||||
%{
|
||||
"event" => type,
|
||||
"payload" => payload_example
|
||||
}
|
||||
|> Map.merge(stream_example)
|
||||
}
|
||||
end
|
||||
|
||||
defp update_event do
|
||||
server_sent_event_helper("New status", "A newly-posted status.", "update", Status)
|
||||
end
|
||||
|
||||
defp status_update_event do
|
||||
server_sent_event_helper("Edit", "A status that was just edited", "status.update", Status)
|
||||
end
|
||||
|
||||
defp notification_event do
|
||||
server_sent_event_helper(
|
||||
"Notification",
|
||||
"A new notification.",
|
||||
"notification",
|
||||
NotificationOperation.notification()
|
||||
)
|
||||
end
|
||||
|
||||
defp follow_relationships_update_event do
|
||||
server_sent_event_helper(
|
||||
"Follow relationships update",
|
||||
"An update to follow relationships.",
|
||||
"pleroma:follow_relationships_update",
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: "Follow relationships update",
|
||||
required: [:state, :follower, :following],
|
||||
properties: %{
|
||||
state: %Schema{
|
||||
type: :string,
|
||||
description: "Follow state of the relationship.",
|
||||
enum: ["follow_pending", "follow_accept", "follow_reject", "unfollow"]
|
||||
},
|
||||
follower: %Schema{
|
||||
type: :object,
|
||||
description: "Information about the follower.",
|
||||
required: [:id, :follower_count, :following_count],
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
follower_count: %Schema{type: :integer},
|
||||
following_count: %Schema{type: :integer}
|
||||
}
|
||||
},
|
||||
following: %Schema{
|
||||
type: :object,
|
||||
description: "Information about the following person.",
|
||||
required: [:id, :follower_count, :following_count],
|
||||
properties: %{
|
||||
id: FlakeID,
|
||||
follower_count: %Schema{type: :integer},
|
||||
following_count: %Schema{type: :integer}
|
||||
}
|
||||
}
|
||||
},
|
||||
example: %{
|
||||
"state" => "follow_pending",
|
||||
"follower" => %{
|
||||
"id" => "someUser1",
|
||||
"follower_count" => 1,
|
||||
"following_count" => 1
|
||||
},
|
||||
"following" => %{
|
||||
"id" => "someUser2",
|
||||
"follower_count" => 1,
|
||||
"following_count" => 1
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp chat_update_event do
|
||||
server_sent_event_helper(
|
||||
"Chat update",
|
||||
"A new chat message.",
|
||||
"pleroma:chat_update",
|
||||
Chat
|
||||
)
|
||||
end
|
||||
|
||||
defp conversation_event do
|
||||
server_sent_event_helper(
|
||||
"Conversation update",
|
||||
"An update about a conversation",
|
||||
"conversation",
|
||||
Conversation
|
||||
)
|
||||
end
|
||||
|
||||
defp delete_event do
|
||||
server_sent_event_helper(
|
||||
"Delete",
|
||||
"A status that was just deleted.",
|
||||
"delete",
|
||||
%Schema{
|
||||
type: :string,
|
||||
title: "Status id",
|
||||
description: "Id of the deleted status",
|
||||
allOf: [FlakeID],
|
||||
example: "some-opaque-id"
|
||||
},
|
||||
payload_type: :string,
|
||||
has_stream: false
|
||||
)
|
||||
end
|
||||
|
||||
defp pleroma_respond_event do
|
||||
server_sent_event_helper(
|
||||
"Server response",
|
||||
"A response to a client-sent event.",
|
||||
"pleroma:respond",
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: "Results",
|
||||
required: [:result, :type],
|
||||
properties: %{
|
||||
result: %Schema{
|
||||
type: :string,
|
||||
title: "Result of the request",
|
||||
enum: ["success", "error", "ignored"]
|
||||
},
|
||||
error: %Schema{
|
||||
type: :string,
|
||||
title: "Error code",
|
||||
description: "An error identifier. Only appears if `result` is `error`."
|
||||
},
|
||||
type: %Schema{
|
||||
type: :string,
|
||||
description: "Type of the request."
|
||||
}
|
||||
},
|
||||
example: %{"result" => "success", "type" => "pleroma:authenticate"}
|
||||
},
|
||||
has_stream: false
|
||||
)
|
||||
end
|
||||
|
||||
defp client_sent_events do
|
||||
%Schema{
|
||||
oneOf: [
|
||||
subscribe_event(),
|
||||
unsubscribe_event(),
|
||||
authenticate_event()
|
||||
]
|
||||
}
|
||||
end
|
||||
|
||||
defp request_body(description, schema, opts \\ []) do
|
||||
%OpenApiSpex.RequestBody{
|
||||
description: description,
|
||||
content: %{
|
||||
"application/json" => %OpenApiSpex.MediaType{
|
||||
schema: schema,
|
||||
example: opts[:example],
|
||||
examples: opts[:examples]
|
||||
}
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
defp client_sent_event_helper(name, description, type, properties, opts) do
|
||||
required = opts[:required] || []
|
||||
|
||||
%Schema{
|
||||
type: :object,
|
||||
title: name,
|
||||
required: [:type] ++ required,
|
||||
description: description,
|
||||
properties:
|
||||
%{
|
||||
type: %Schema{type: :string, enum: [type], description: "Type of the event."}
|
||||
}
|
||||
|> Map.merge(properties),
|
||||
example: opts[:example]
|
||||
}
|
||||
end
|
||||
|
||||
defp subscribe_event do
|
||||
client_sent_event_helper(
|
||||
"Subscribe",
|
||||
"Subscribe to a stream.",
|
||||
"subscribe",
|
||||
stream_specifier(),
|
||||
required: [:stream],
|
||||
example: %{"type" => "subscribe", "stream" => "list", "list" => "1"}
|
||||
)
|
||||
end
|
||||
|
||||
defp unsubscribe_event do
|
||||
client_sent_event_helper(
|
||||
"Unsubscribe",
|
||||
"Unsubscribe from a stream.",
|
||||
"unsubscribe",
|
||||
stream_specifier(),
|
||||
required: [:stream],
|
||||
example: %{
|
||||
"type" => "unsubscribe",
|
||||
"stream" => "public:remote:media",
|
||||
"instance" => "example.org"
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp authenticate_event do
|
||||
client_sent_event_helper(
|
||||
"Authenticate",
|
||||
"Authenticate via an access token.",
|
||||
"pleroma:authenticate",
|
||||
%{
|
||||
token: token()
|
||||
},
|
||||
required: [:token]
|
||||
)
|
||||
end
|
||||
|
||||
defp token do
|
||||
%Schema{
|
||||
type: :string,
|
||||
description: "An OAuth access token with corresponding permissions.",
|
||||
example: "some token"
|
||||
}
|
||||
end
|
||||
|
||||
defp stream_specifier do
|
||||
%{
|
||||
stream: %Schema{
|
||||
type: :string,
|
||||
description: "The name of the stream.",
|
||||
enum:
|
||||
Pleroma.Constants.public_streams() ++
|
||||
[
|
||||
"public:remote",
|
||||
"public:remote:media",
|
||||
"user",
|
||||
"user:pleroma_chat",
|
||||
"user:notification",
|
||||
"direct",
|
||||
"list",
|
||||
"hashtag"
|
||||
]
|
||||
},
|
||||
list: %Schema{
|
||||
type: :string,
|
||||
title: "List id",
|
||||
description: "The id of the list. Required when `stream` is `list`.",
|
||||
example: "some-id"
|
||||
},
|
||||
tag: %Schema{
|
||||
type: :string,
|
||||
title: "Hashtag name",
|
||||
description: "The name of the hashtag. Required when `stream` is `hashtag`.",
|
||||
example: "mew"
|
||||
},
|
||||
instance: %Schema{
|
||||
type: :string,
|
||||
title: "Domain name",
|
||||
description:
|
||||
"Domain name of the instance. Required when `stream` is `public:remote` or `public:remote:media`.",
|
||||
example: "example.org"
|
||||
}
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -17,7 +17,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def emoji_operation do
|
||||
%Operation{
|
||||
tags: ["Emojis"],
|
||||
tags: ["Custom emojis"],
|
||||
summary: "List all custom emojis",
|
||||
operationId: "UtilController.emoji",
|
||||
parameters: [],
|
||||
|
|
@ -30,7 +30,8 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
properties: %{
|
||||
image_url: %Schema{type: :string},
|
||||
tags: %Schema{type: :array, items: %Schema{type: :string}}
|
||||
}
|
||||
},
|
||||
extensions: %{"x-additionalPropertiesName": "Emoji name"}
|
||||
},
|
||||
example: %{
|
||||
"firefox" => %{
|
||||
|
|
@ -45,7 +46,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def frontend_configurations_operation do
|
||||
%Operation{
|
||||
tags: ["Configuration"],
|
||||
tags: ["Others"],
|
||||
summary: "Dump frontend configurations",
|
||||
operationId: "UtilController.frontend_configurations",
|
||||
parameters: [],
|
||||
|
|
@ -53,7 +54,12 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
200 =>
|
||||
Operation.response("List", "application/json", %Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :object}
|
||||
additionalProperties: %Schema{
|
||||
type: :object,
|
||||
description:
|
||||
"Opaque object representing the instance-wide configuration for the frontend",
|
||||
extensions: %{"x-additionalPropertiesName": "Frontend name"}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -132,7 +138,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def update_notificaton_settings_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
tags: ["Settings"],
|
||||
summary: "Update Notification Settings",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.update_notificaton_settings",
|
||||
|
|
@ -207,6 +213,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
%Operation{
|
||||
summary: "Get a captcha",
|
||||
operationId: "UtilController.captcha",
|
||||
tags: ["Others"],
|
||||
parameters: [],
|
||||
responses: %{
|
||||
200 => Operation.response("Success", "application/json", %Schema{type: :object})
|
||||
|
|
@ -356,7 +363,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def healthcheck_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
tags: ["Others"],
|
||||
summary: "Quick status check on the instance",
|
||||
security: [%{"oAuth" => ["write:accounts"]}],
|
||||
operationId: "UtilController.healthcheck",
|
||||
|
|
@ -371,7 +378,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def remote_subscribe_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote Subscribe",
|
||||
operationId: "UtilController.remote_subscribe",
|
||||
parameters: [],
|
||||
|
|
@ -381,7 +388,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def remote_interaction_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Remote interaction",
|
||||
operationId: "UtilController.remote_interaction",
|
||||
requestBody: request_body("Parameters", remote_interaction_request(), required: true),
|
||||
|
|
@ -407,7 +414,7 @@ defmodule Pleroma.Web.ApiSpec.TwitterUtilOperation do
|
|||
|
||||
def show_subscribe_form_operation do
|
||||
%Operation{
|
||||
tags: ["Accounts"],
|
||||
tags: ["Remote interaction"],
|
||||
summary: "Show remote subscribe form",
|
||||
operationId: "UtilController.show_subscribe_form",
|
||||
parameters: [],
|
||||
|
|
|
|||
|
|
@ -144,7 +144,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
properties: %{
|
||||
content: %Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
additionalProperties: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate representation in the MIME type specified",
|
||||
extensions: %{"x-additionalPropertiesName": "MIME type"}
|
||||
},
|
||||
description:
|
||||
"A map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`"
|
||||
},
|
||||
|
|
@ -189,13 +193,37 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do
|
|||
nullable: true,
|
||||
description: "The `acct` property of User entity for replied user (if any)"
|
||||
},
|
||||
quote: %Schema{
|
||||
allOf: [%OpenApiSpex.Reference{"$ref": "#/components/schemas/Status"}],
|
||||
nullable: true,
|
||||
description: "Quoted status (if any)"
|
||||
},
|
||||
quote_id: %Schema{
|
||||
nullable: true,
|
||||
allOf: [FlakeID],
|
||||
description: "ID of the status being quoted, if any"
|
||||
},
|
||||
quote_url: %Schema{
|
||||
type: :string,
|
||||
format: :uri,
|
||||
nullable: true,
|
||||
description: "URL of the quoted status"
|
||||
},
|
||||
quote_visible: %Schema{
|
||||
type: :boolean,
|
||||
description: "`true` if the quoted post is visible to the user"
|
||||
},
|
||||
local: %Schema{
|
||||
type: :boolean,
|
||||
description: "`true` if the post was made on the local instance"
|
||||
},
|
||||
spoiler_text: %Schema{
|
||||
type: :object,
|
||||
additionalProperties: %Schema{type: :string},
|
||||
additionalProperties: %Schema{
|
||||
type: :string,
|
||||
description: "Alternate representation in the MIME type specified",
|
||||
extensions: %{"x-additionalPropertiesName": "MIME type"}
|
||||
},
|
||||
description:
|
||||
"A map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`."
|
||||
},
|
||||
|
|
|
|||
82
lib/pleroma/web/api_spec/scopes/compiler.ex
Normal file
82
lib/pleroma/web/api_spec/scopes/compiler.ex
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Scopes.Compiler do
|
||||
defmacro __before_compile__(_env) do
|
||||
strings = __MODULE__.extract_all_scopes()
|
||||
|
||||
quote do
|
||||
def placeholder do
|
||||
unquote do
|
||||
Enum.map(
|
||||
strings,
|
||||
fn string ->
|
||||
quote do
|
||||
Pleroma.Web.Gettext.dgettext_noop(
|
||||
"oauth_scopes",
|
||||
unquote(string)
|
||||
)
|
||||
end
|
||||
end
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def extract_all_scopes do
|
||||
extract_all_scopes_from(Pleroma.Web.ApiSpec.spec())
|
||||
end
|
||||
|
||||
def extract_all_scopes_from(specs) do
|
||||
specs.paths
|
||||
|> Enum.reduce([], fn
|
||||
{_path, %{} = path_item}, acc ->
|
||||
extract_routes(path_item)
|
||||
|> Enum.flat_map(fn operation -> process_operation(operation) end)
|
||||
|> Kernel.++(acc)
|
||||
|
||||
{_, _}, acc ->
|
||||
acc
|
||||
end)
|
||||
|> Enum.uniq()
|
||||
end
|
||||
|
||||
defp extract_routes(path_item) do
|
||||
path_item
|
||||
|> Map.from_struct()
|
||||
|> Enum.map(fn {_method, path_item} -> path_item end)
|
||||
|> Enum.filter(fn
|
||||
%OpenApiSpex.Operation{} = _operation -> true
|
||||
_ -> false
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_operation(operation) do
|
||||
operation.security
|
||||
|> Kernel.||([])
|
||||
|> Enum.flat_map(fn
|
||||
%{"oAuth" => scopes} -> process_scopes(scopes)
|
||||
_ -> []
|
||||
end)
|
||||
end
|
||||
|
||||
defp process_scopes(scopes) do
|
||||
scopes
|
||||
|> Enum.flat_map(fn scope ->
|
||||
process_scope(scope)
|
||||
end)
|
||||
end
|
||||
|
||||
def process_scope(scope) do
|
||||
hierarchy = String.split(scope, ":")
|
||||
|
||||
{_, list} =
|
||||
Enum.reduce(hierarchy, {"", []}, fn comp, {cur, list} ->
|
||||
{cur <> comp <> ":", [cur <> comp | list]}
|
||||
end)
|
||||
|
||||
list
|
||||
end
|
||||
end
|
||||
10
lib/pleroma/web/api_spec/scopes/translator.ex
Normal file
10
lib/pleroma/web/api_spec/scopes/translator.ex
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.ApiSpec.Scopes.Translator do
|
||||
require Pleroma.Web.ApiSpec.Scopes.Compiler
|
||||
require Pleroma.Web.Gettext
|
||||
|
||||
@before_compile Pleroma.Web.ApiSpec.Scopes.Compiler
|
||||
end
|
||||
|
|
@ -33,6 +33,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def post_chat_message(%User{} = user, %User{} = recipient, content, opts \\ []) do
|
||||
with maybe_attachment <- opts[:media_id] && Object.get_by_id(opts[:media_id]),
|
||||
:ok <- validate_chat_attachment_attribution(maybe_attachment, user),
|
||||
:ok <- validate_chat_content_length(content, !!maybe_attachment),
|
||||
{_, {:ok, chat_message_data, _meta}} <-
|
||||
{:build_object,
|
||||
|
|
@ -71,6 +72,17 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
text
|
||||
end
|
||||
|
||||
defp validate_chat_attachment_attribution(nil, _), do: :ok
|
||||
|
||||
defp validate_chat_attachment_attribution(attachment, user) do
|
||||
with :ok <- Object.authorize_access(attachment, user) do
|
||||
:ok
|
||||
else
|
||||
e ->
|
||||
e
|
||||
end
|
||||
end
|
||||
|
||||
defp validate_chat_content_length(_, true), do: :ok
|
||||
defp validate_chat_content_length(nil, false), do: {:error, :no_content}
|
||||
|
||||
|
|
@ -142,13 +154,13 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
|
||||
def delete(activity_id, user) do
|
||||
with {_, %Activity{data: %{"object" => _, "type" => "Create"}} = activity} <-
|
||||
{:find_activity, Activity.get_by_id(activity_id)},
|
||||
{:find_activity, Activity.get_by_id(activity_id, filter: [])},
|
||||
{_, %Object{} = object, _} <-
|
||||
{:find_object, Object.normalize(activity, fetch: false), activity},
|
||||
true <- User.superuser?(user) || user.ap_id == object.data["actor"],
|
||||
true <- User.privileged?(user, :messages_delete) || user.ap_id == object.data["actor"],
|
||||
{:ok, delete_data, _} <- Builder.delete(user, object.data["id"]),
|
||||
{:ok, delete, _} <- Pipeline.common_pipeline(delete_data, local: true) do
|
||||
if User.superuser?(user) and user.ap_id != object.data["actor"] do
|
||||
if User.privileged?(user, :messages_delete) and user.ap_id != object.data["actor"] do
|
||||
action =
|
||||
if object.data["type"] == "ChatMessage" do
|
||||
"chat_message_delete"
|
||||
|
|
@ -583,7 +595,7 @@ defmodule Pleroma.Web.CommonAPI do
|
|||
end
|
||||
|
||||
def update_report_state(activity_id, state) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id) do
|
||||
with %Activity{} = activity <- Activity.get_by_id(activity_id, filter: []) do
|
||||
Utils.update_report_state(activity, state)
|
||||
else
|
||||
nil -> {:error, :not_found}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Web.ActivityPub.Builder
|
||||
alias Pleroma.Web.ActivityPub.Visibility
|
||||
alias Pleroma.Web.CommonAPI
|
||||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
import Pleroma.Web.Gettext
|
||||
import Pleroma.Web.Utils.Guards, only: [not_empty_string: 1]
|
||||
|
||||
defstruct valid?: true,
|
||||
errors: [],
|
||||
|
|
@ -22,6 +24,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
attachments: [],
|
||||
in_reply_to: nil,
|
||||
in_reply_to_conversation: nil,
|
||||
quote_post: nil,
|
||||
visibility: nil,
|
||||
expires_at: nil,
|
||||
extra: nil,
|
||||
|
|
@ -53,7 +56,9 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|> poll()
|
||||
|> with_valid(&in_reply_to/1)
|
||||
|> with_valid(&in_reply_to_conversation/1)
|
||||
|> with_valid("e_post/1)
|
||||
|> with_valid(&visibility/1)
|
||||
|> with_valid("ing_visibility/1)
|
||||
|> content()
|
||||
|> with_valid(&to_and_cc/1)
|
||||
|> with_valid(&context/1)
|
||||
|
|
@ -111,7 +116,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
|
||||
defp attachments(%{params: params} = draft) do
|
||||
attachments = Utils.attachments_from_ids(params)
|
||||
attachments = Utils.attachments_from_ids(params, draft.user)
|
||||
draft = %__MODULE__{draft | attachments: attachments}
|
||||
|
||||
case Utils.validate_attachments_count(attachments) do
|
||||
|
|
@ -132,6 +137,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
|
||||
defp in_reply_to(draft), do: draft
|
||||
|
||||
defp quote_post(%{params: %{quote_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
|
||||
end
|
||||
end
|
||||
|
||||
defp quote_post(draft), do: draft
|
||||
|
||||
defp in_reply_to_conversation(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}
|
||||
|
|
@ -147,6 +164,29 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
end
|
||||
|
||||
defp can_quote?(_draft, _object, visibility) when visibility in ~w(public unlisted local) do
|
||||
true
|
||||
end
|
||||
|
||||
defp can_quote?(draft, object, "private") do
|
||||
draft.user.ap_id == object.data["actor"]
|
||||
end
|
||||
|
||||
defp can_quote?(_, _, _) do
|
||||
false
|
||||
end
|
||||
|
||||
defp quoting_visibility(%{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
|
||||
else
|
||||
_ -> add_error(draft, dgettext("errors", "Cannot quote private message"))
|
||||
end
|
||||
end
|
||||
|
||||
defp quoting_visibility(draft), do: draft
|
||||
|
||||
defp expires_at(draft) do
|
||||
case CommonAPI.check_expiry_date(draft.params[:expires_in]) do
|
||||
{:ok, expires_at} -> %__MODULE__{draft | expires_at: expires_at}
|
||||
|
|
@ -164,12 +204,15 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
|
|||
end
|
||||
end
|
||||
|
||||
defp content(draft) do
|
||||
defp content(%{mentions: mentions} = draft) do
|
||||
{content_html, mentioned_users, tags} = Utils.make_content_html(draft)
|
||||
|
||||
mentioned_ap_ids =
|
||||
Enum.map(mentioned_users, fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
||||
|
||||
mentions =
|
||||
mentioned_users
|
||||
|> Enum.map(fn {_, mentioned_user} -> mentioned_user.ap_id end)
|
||||
mentions
|
||||
|> Kernel.++(mentioned_ap_ids)
|
||||
|> Utils.get_addressed_users(draft.params[:to])
|
||||
|
||||
%__MODULE__{draft | content_html: content_html, mentions: mentions, tags: tags}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,21 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
require Logger
|
||||
require Pleroma.Constants
|
||||
|
||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}) do
|
||||
attachments_from_ids_descs(ids, desc)
|
||||
def attachments_from_ids(%{media_ids: ids, descriptions: desc}, user) do
|
||||
attachments_from_ids_descs(ids, desc, user)
|
||||
end
|
||||
|
||||
def attachments_from_ids(%{media_ids: ids}) do
|
||||
attachments_from_ids_no_descs(ids)
|
||||
def attachments_from_ids(%{media_ids: ids}, user) do
|
||||
attachments_from_ids_no_descs(ids, user)
|
||||
end
|
||||
|
||||
def attachments_from_ids(_), do: []
|
||||
def attachments_from_ids(_, _), do: []
|
||||
|
||||
def attachments_from_ids_no_descs([]), do: []
|
||||
def attachments_from_ids_no_descs([], _), do: []
|
||||
|
||||
def attachments_from_ids_no_descs(ids) do
|
||||
def attachments_from_ids_no_descs(ids, user) do
|
||||
Enum.map(ids, fn media_id ->
|
||||
case get_attachment(media_id) do
|
||||
case get_attachment(media_id, user) do
|
||||
%Object{data: data} -> data
|
||||
_ -> nil
|
||||
end
|
||||
|
|
@ -45,21 +45,27 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
def attachments_from_ids_descs([], _), do: []
|
||||
def attachments_from_ids_descs([], _, _), do: []
|
||||
|
||||
def attachments_from_ids_descs(ids, descs_str) do
|
||||
def attachments_from_ids_descs(ids, descs_str, user) do
|
||||
{_, descs} = Jason.decode(descs_str)
|
||||
|
||||
Enum.map(ids, fn media_id ->
|
||||
with %Object{data: data} <- get_attachment(media_id) do
|
||||
with %Object{data: data} <- get_attachment(media_id, user) do
|
||||
Map.put(data, "name", descs[media_id])
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp get_attachment(media_id) do
|
||||
Repo.get(Object, media_id)
|
||||
defp get_attachment(media_id, user) do
|
||||
with %Object{data: data} = object <- Repo.get(Object, media_id),
|
||||
%{"type" => type} when type in Pleroma.Constants.upload_object_types() <- data,
|
||||
:ok <- Object.authorize_access(object, user) do
|
||||
object
|
||||
else
|
||||
_ -> nil
|
||||
end
|
||||
end
|
||||
|
||||
@spec get_to_and_cc(ActivityDraft.t()) :: {list(String.t()), list(String.t())}
|
||||
|
|
@ -145,6 +151,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
when is_list(options) do
|
||||
limits = Config.get([:instance, :poll_limits])
|
||||
|
||||
options = options |> Enum.uniq()
|
||||
|
||||
with :ok <- validate_poll_expiration(expires_in, limits),
|
||||
:ok <- validate_poll_options_amount(options, limits),
|
||||
:ok <- validate_poll_options_length(options, limits) do
|
||||
|
|
@ -180,10 +188,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do
|
|||
end
|
||||
|
||||
defp validate_poll_options_amount(options, %{max_options: max_options}) do
|
||||
if Enum.count(options) > max_options do
|
||||
{:error, "Poll can't contain more than #{max_options} options"}
|
||||
else
|
||||
:ok
|
||||
cond do
|
||||
Enum.count(options) < 2 ->
|
||||
{:error, "Poll must contain at least 2 options"}
|
||||
|
||||
Enum.count(options) > max_options ->
|
||||
{:error, "Poll can't contain more than #{max_options} options"}
|
||||
|
||||
true ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -101,13 +101,10 @@ defmodule Pleroma.Web.Endpoint do
|
|||
plug(Plug.Logger, log: :debug)
|
||||
|
||||
plug(Plug.Parsers,
|
||||
parsers: [
|
||||
:urlencoded,
|
||||
{:multipart, length: {Config, :get, [[:instance, :upload_limit]]}},
|
||||
:json
|
||||
],
|
||||
parsers: [:urlencoded, Pleroma.Web.Multipart, :json],
|
||||
pass: ["*/*"],
|
||||
json_decoder: Jason,
|
||||
# Note: this is compile-time only, won't work for database-config
|
||||
length: Config.get([:instance, :upload_limit]),
|
||||
body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.Federator do
|
|||
alias Pleroma.Activity
|
||||
alias Pleroma.Object.Containment
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.ActivityPub.ActivityPub
|
||||
alias Pleroma.Web.ActivityPub.Transmogrifier
|
||||
alias Pleroma.Web.ActivityPub.Utils
|
||||
alias Pleroma.Web.Federator.Publisher
|
||||
|
|
@ -80,7 +79,7 @@ defmodule Pleroma.Web.Federator do
|
|||
|
||||
# NOTE: we use the actor ID to do the containment, this is fine because an
|
||||
# actor shouldn't be acting on objects outside their own AP server.
|
||||
with {_, {:ok, _user}} <- {:actor, ap_enabled_actor(actor)},
|
||||
with {_, {:ok, _user}} <- {:actor, User.get_or_fetch_by_ap_id(actor)},
|
||||
nil <- Activity.normalize(params["id"]),
|
||||
{_, :ok} <-
|
||||
{:correct_origin?, Containment.contain_origin_from_id(actor, params)},
|
||||
|
|
@ -110,14 +109,4 @@ defmodule Pleroma.Web.Federator do
|
|||
{:error, e}
|
||||
end
|
||||
end
|
||||
|
||||
def ap_enabled_actor(id) do
|
||||
user = User.get_cached_by_ap_id(id)
|
||||
|
||||
if User.ap_enabled?(user) do
|
||||
{:ok, user}
|
||||
else
|
||||
ActivityPub.make_user_from_ap_id(id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
use Phoenix.HTML
|
||||
use Pleroma.Web, :view
|
||||
|
||||
alias Pleroma.Formatter
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.User
|
||||
alias Pleroma.Web.Gettext
|
||||
|
|
@ -72,7 +71,9 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
|
||||
def last_activity(activities), do: List.last(activities)
|
||||
|
||||
def activity_title(%{"content" => content, "summary" => summary} = data, opts \\ %{}) do
|
||||
def activity_title(%{"content" => content} = data, opts \\ %{}) do
|
||||
summary = Map.get(data, "summary", "")
|
||||
|
||||
title =
|
||||
cond do
|
||||
summary != "" -> summary
|
||||
|
|
@ -81,9 +82,8 @@ defmodule Pleroma.Web.Feed.FeedView do
|
|||
end
|
||||
|
||||
title
|
||||
|> Pleroma.Web.Metadata.Utils.scrub_html()
|
||||
|> Pleroma.Emoji.Formatter.demojify()
|
||||
|> Formatter.truncate(opts[:max_length], opts[:omission])
|
||||
|> Pleroma.Web.Metadata.Utils.scrub_html_and_truncate(opts[:max_length], opts[:omission])
|
||||
|> HtmlEntities.encode()
|
||||
end
|
||||
|
||||
def activity_description(data) do
|
||||
|
|
|
|||
|
|
@ -263,20 +263,29 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
{:error, %Ecto.Changeset{errors: [background: {"file is too large", _}]}} ->
|
||||
render_error(conn, :request_entity_too_large, "File is too large")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:bio, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Bio is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:name, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Name is too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:fields, {"invalid", _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "One or more field entries are too long")
|
||||
|
||||
{:error, %Ecto.Changeset{errors: [{:fields, {_, _}} | _]}} ->
|
||||
render_error(conn, :request_entity_too_large, "Too many field entries")
|
||||
|
||||
_e ->
|
||||
render_error(conn, :forbidden, "Invalid request")
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_fields_attributes(fields) do
|
||||
if Enum.all?(fields, &is_tuple/1) do
|
||||
Enum.map(fields, fn {_, v} -> v end)
|
||||
else
|
||||
Enum.map(fields, fn
|
||||
%{} = field -> %{"name" => field.name, "value" => field.value}
|
||||
field -> field
|
||||
end)
|
||||
end
|
||||
if(Enum.all?(fields, &is_tuple/1), do: Enum.map(fields, fn {_, v} -> v end), else: fields)
|
||||
|> Enum.map(fn
|
||||
%{} = field -> %{"name" => field.name, "value" => field.value}
|
||||
field -> field
|
||||
end)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/relationships"
|
||||
|
|
@ -543,7 +552,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
|
|||
|
||||
conn
|
||||
|> add_link_headers(users)
|
||||
|> render("index.json", users: users, for: user, as: :user)
|
||||
|> render("index.json",
|
||||
users: users,
|
||||
for: user,
|
||||
as: :user,
|
||||
embed_relationships: embed_relationships?(params)
|
||||
)
|
||||
end
|
||||
|
||||
@doc "GET /api/v1/accounts/lookup"
|
||||
|
|
|
|||
|
|
@ -61,7 +61,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do
|
|||
end
|
||||
|
||||
def get_notifications(user, params \\ %{}) do
|
||||
options = cast_params(params)
|
||||
options =
|
||||
cast_params(params) |> Map.update(:include_types, [], fn include_types -> include_types end)
|
||||
|
||||
options =
|
||||
if ("pleroma:report" not in options.include_types and
|
||||
User.privileged?(user, :reports_manage_reports)) or
|
||||
User.privileged?(user, :reports_manage_reports) do
|
||||
options
|
||||
else
|
||||
options
|
||||
|> Map.update(:exclude_types, ["pleroma:report"], fn current_exclude_types ->
|
||||
current_exclude_types ++ ["pleroma:report"]
|
||||
end)
|
||||
end
|
||||
|
||||
user
|
||||
|> Notification.for_user_query(options)
|
||||
|
|
|
|||
|
|
@ -370,19 +370,22 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
defp maybe_put_chat_token(data, _, _, _), do: data
|
||||
|
||||
defp maybe_put_role(data, %User{show_role: true} = user, _) do
|
||||
data
|
||||
|> Kernel.put_in([:pleroma, :is_admin], user.is_admin)
|
||||
|> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator)
|
||||
put_role(data, user)
|
||||
end
|
||||
|
||||
defp maybe_put_role(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
data
|
||||
|> Kernel.put_in([:pleroma, :is_admin], user.is_admin)
|
||||
|> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator)
|
||||
put_role(data, user)
|
||||
end
|
||||
|
||||
defp maybe_put_role(data, _, _), do: data
|
||||
|
||||
defp put_role(data, user) do
|
||||
data
|
||||
|> Kernel.put_in([:pleroma, :is_admin], user.is_admin)
|
||||
|> Kernel.put_in([:pleroma, :is_moderator], user.is_moderator)
|
||||
|> Kernel.put_in([:pleroma, :privileges], User.privileges(user))
|
||||
end
|
||||
|
||||
defp maybe_put_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
Kernel.put_in(
|
||||
data,
|
||||
|
|
@ -399,12 +402,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
|
|||
|
||||
defp maybe_put_allow_following_move(data, _, _), do: data
|
||||
|
||||
defp maybe_put_activation_status(data, user, %User{is_admin: true}) do
|
||||
Kernel.put_in(data, [:pleroma, :deactivated], !user.is_active)
|
||||
defp maybe_put_activation_status(data, user, user_for) do
|
||||
if User.privileged?(user_for, :users_manage_activation_state),
|
||||
do: Kernel.put_in(data, [:pleroma, :deactivated], !user.is_active),
|
||||
else: data
|
||||
end
|
||||
|
||||
defp maybe_put_activation_status(data, _, _), do: data
|
||||
|
||||
defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{id: user_id}) do
|
||||
data
|
||||
|> Kernel.put_in(
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
thumbnail:
|
||||
URI.merge(Pleroma.Web.Endpoint.url(), Keyword.get(instance, :instance_thumbnail))
|
||||
|> to_string,
|
||||
languages: ["en"],
|
||||
languages: Keyword.get(instance, :languages, ["en"]),
|
||||
registrations: Keyword.get(instance, :registrations_open),
|
||||
approval_required: Keyword.get(instance, :account_approval_required),
|
||||
# Extra (not present in Mastodon):
|
||||
|
|
@ -48,7 +48,6 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
federation: federation(),
|
||||
fields_limits: fields_limits(),
|
||||
post_formats: Config.get([:instance, :allowed_post_formats]),
|
||||
privileged_staff: Config.get([:instance, :privileged_staff]),
|
||||
birthday_required: Config.get([:instance, :birthday_required]),
|
||||
birthday_min_age: Config.get([:instance, :birthday_min_age])
|
||||
},
|
||||
|
|
@ -70,6 +69,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
"multifetch",
|
||||
"pleroma:api/v1/notifications:include_types_filter",
|
||||
"editing",
|
||||
"quote_posting",
|
||||
if Config.get([:activitypub, :blockers_visible]) do
|
||||
"blockers_visible"
|
||||
end,
|
||||
|
|
@ -93,6 +93,7 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do
|
|||
"safe_dm_mentions"
|
||||
end,
|
||||
"pleroma_emoji_reactions",
|
||||
"pleroma_custom_emoji_reactions",
|
||||
"pleroma_chat_messages",
|
||||
if Config.get([:instance, :show_reactions]) do
|
||||
"exposable_reactions"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
alias Pleroma.Web.MastodonAPI.NotificationView
|
||||
alias Pleroma.Web.MastodonAPI.StatusView
|
||||
alias Pleroma.Web.MediaProxy
|
||||
alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
|
||||
|
||||
defp object_id_for(%{data: %{"object" => %{"id" => id}}}) when is_binary(id), do: id
|
||||
|
|
@ -145,7 +146,9 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|
|||
end
|
||||
|
||||
defp put_emoji(response, activity) do
|
||||
Map.put(response, :emoji, activity.data["content"])
|
||||
response
|
||||
|> Map.put(:emoji, activity.data["content"])
|
||||
|> Map.put(:emoji_url, MediaProxy.url(Pleroma.Emoji.emoji_url(activity.data)))
|
||||
end
|
||||
|
||||
defp put_chat_message(response, activity, reading_user, opts) do
|
||||
|
|
|
|||
|
|
@ -57,6 +57,27 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end)
|
||||
end
|
||||
|
||||
defp get_quoted_activities([]), do: %{}
|
||||
|
||||
defp get_quoted_activities(activities) do
|
||||
activities
|
||||
|> Enum.map(fn
|
||||
%{data: %{"type" => "Create"}} = activity ->
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
object && object.data["quoteUrl"] != "" && object.data["quoteUrl"]
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end)
|
||||
|> Enum.filter(& &1)
|
||||
|> Activity.create_by_object_ap_id_with_object()
|
||||
|> Repo.all()
|
||||
|> Enum.reduce(%{}, fn activity, acc ->
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
if object, do: Map.put(acc, object.data["id"], activity), else: acc
|
||||
end)
|
||||
end
|
||||
|
||||
# DEPRECATED This field seems to be a left-over from the StatusNet era.
|
||||
# If your application uses `pleroma.conversation_id`: this field is deprecated.
|
||||
# It is currently stubbed instead by doing a CRC32 of the context, and
|
||||
|
|
@ -97,6 +118,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
# length(activities_with_links) * timeout
|
||||
fetch_rich_media_for_activities(activities)
|
||||
replied_to_activities = get_replied_to_activities(activities)
|
||||
quoted_activities = get_quoted_activities(activities)
|
||||
|
||||
parent_activities =
|
||||
activities
|
||||
|
|
@ -129,6 +151,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
opts =
|
||||
opts
|
||||
|> Map.put(:replied_to_activities, replied_to_activities)
|
||||
|> Map.put(:quoted_activities, quoted_activities)
|
||||
|> Map.put(:parent_activities, parent_activities)
|
||||
|> Map.put(:relationships, relationships_opt)
|
||||
|
||||
|
|
@ -277,7 +300,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end
|
||||
|
||||
reply_to = get_reply_to(activity, opts)
|
||||
|
||||
reply_to_user = reply_to && CommonAPI.get_user(reply_to.data["actor"])
|
||||
|
||||
history_len =
|
||||
|
|
@ -290,6 +312,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
# Here the implicit index of the current content is 0
|
||||
chrono_order = history_len - 1
|
||||
|
||||
quote_activity = get_quote(activity, opts)
|
||||
|
||||
quote_id =
|
||||
case quote_activity do
|
||||
%Activity{id: id} -> id
|
||||
_ -> nil
|
||||
end
|
||||
|
||||
quote_post =
|
||||
if visible_for_user?(quote_activity, opts[:for]) and opts[:show_quote] != false do
|
||||
quote_rendering_opts = Map.merge(opts, %{activity: quote_activity, show_quote: false})
|
||||
render("show.json", quote_rendering_opts)
|
||||
else
|
||||
nil
|
||||
end
|
||||
|
||||
content =
|
||||
object
|
||||
|> render_content()
|
||||
|
|
@ -334,14 +372,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end
|
||||
|
||||
emoji_reactions =
|
||||
object.data
|
||||
|> Map.get("reactions", [])
|
||||
object
|
||||
|> Object.get_emoji_reactions()
|
||||
|> EmojiReactionController.filter_allowed_users(
|
||||
opts[:for],
|
||||
Map.get(opts, :with_muted, false)
|
||||
)
|
||||
|> Stream.map(fn {emoji, users} ->
|
||||
build_emoji_map(emoji, users, opts[:for])
|
||||
|> Stream.map(fn {emoji, users, url} ->
|
||||
build_emoji_map(emoji, users, url, opts[:for])
|
||||
end)
|
||||
|> Enum.to_list()
|
||||
|
||||
|
|
@ -398,6 +436,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
conversation_id: get_context_id(activity),
|
||||
context: object.data["context"],
|
||||
in_reply_to_account_acct: reply_to_user && reply_to_user.nickname,
|
||||
quote: quote_post,
|
||||
quote_id: quote_id,
|
||||
quote_url: object.data["quoteUrl"],
|
||||
quote_visible: visible_for_user?(quote_activity, opts[:for]),
|
||||
content: %{"text/plain" => content_plaintext},
|
||||
spoiler_text: %{"text/plain" => summary},
|
||||
expires_at: expires_at,
|
||||
|
|
@ -633,6 +675,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end
|
||||
end
|
||||
|
||||
def get_quote(activity, %{quoted_activities: quoted_activities}) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
with nil <- quoted_activities[object.data["quoteUrl"]] do
|
||||
# For when a quote post is inside an Announce
|
||||
Activity.get_create_by_object_ap_id_with_object(object.data["quoteUrl"])
|
||||
end
|
||||
end
|
||||
|
||||
def get_quote(%{data: %{"object" => _object}} = activity, _) do
|
||||
object = Object.normalize(activity, fetch: false)
|
||||
|
||||
if object.data["quoteUrl"] && object.data["quoteUrl"] != "" do
|
||||
Activity.get_create_by_object_ap_id(object.data["quoteUrl"])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def render_content(%{data: %{"name" => name}} = object) when not is_nil(name) and name != "" do
|
||||
url = object.data["url"] || object.data["id"]
|
||||
|
||||
|
|
@ -702,11 +763,13 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
|
|||
end
|
||||
end
|
||||
|
||||
defp build_emoji_map(emoji, users, current_user) do
|
||||
defp build_emoji_map(emoji, users, url, current_user) do
|
||||
%{
|
||||
name: emoji,
|
||||
name: Pleroma.Web.PleromaAPI.EmojiReactionView.emoji_name(emoji, url),
|
||||
count: length(users),
|
||||
me: !!(current_user && current_user.ap_id in users)
|
||||
url: MediaProxy.url(url),
|
||||
me: !!(current_user && current_user.ap_id in users),
|
||||
account_ids: Enum.map(users, fn user -> User.get_cached_by_ap_id(user).id end)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
alias Pleroma.User
|
||||
alias Pleroma.Web.OAuth.Token
|
||||
alias Pleroma.Web.Streamer
|
||||
alias Pleroma.Web.StreamerView
|
||||
|
||||
@behaviour :cowboy_websocket
|
||||
|
||||
|
|
@ -32,8 +33,15 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
req
|
||||
end
|
||||
|
||||
topics =
|
||||
if topic do
|
||||
[topic]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
{:cowboy_websocket, req,
|
||||
%{user: user, topic: topic, oauth_token: oauth_token, count: 0, timer: nil},
|
||||
%{user: user, topics: topics, oauth_token: oauth_token, count: 0, timer: nil},
|
||||
%{idle_timeout: @timeout}}
|
||||
else
|
||||
{:error, :bad_topic} ->
|
||||
|
|
@ -50,10 +58,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
|
||||
def websocket_init(state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic}"
|
||||
"#{__MODULE__} accepted websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics}"
|
||||
)
|
||||
|
||||
Streamer.add_socket(state.topic, state.oauth_token)
|
||||
Enum.each(state.topics, fn topic -> Streamer.add_socket(topic, state.oauth_token) end)
|
||||
{:ok, %{state | timer: timer()}}
|
||||
end
|
||||
|
||||
|
|
@ -66,16 +74,26 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
# We only receive pings for now
|
||||
def websocket_handle(:ping, state), do: {:ok, state}
|
||||
|
||||
def websocket_handle({:text, text}, state) do
|
||||
with {:ok, %{} = event} <- Jason.decode(text) do
|
||||
handle_client_event(event, state)
|
||||
else
|
||||
_ ->
|
||||
Logger.error("#{__MODULE__} received non-JSON event: #{inspect(text)}")
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
def websocket_handle(frame, state) do
|
||||
Logger.error("#{__MODULE__} received frame: #{inspect(frame)}")
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
def websocket_info({:render_with_user, view, template, item}, state) do
|
||||
def websocket_info({:render_with_user, view, template, item, topic}, state) do
|
||||
user = %User{} = User.get_cached_by_ap_id(state.user.ap_id)
|
||||
|
||||
unless Streamer.filtered_by_user?(user, item) do
|
||||
websocket_info({:text, view.render(template, item, user)}, %{state | user: user})
|
||||
websocket_info({:text, view.render(template, item, user, topic)}, %{state | user: user})
|
||||
else
|
||||
{:ok, state}
|
||||
end
|
||||
|
|
@ -109,10 +127,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
|
||||
def terminate(reason, _req, state) do
|
||||
Logger.debug(
|
||||
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topic #{state.topic || "?"}: #{inspect(reason)}"
|
||||
"#{__MODULE__} terminating websocket connection for user #{(state.user || %{id: "anonymous"}).id}, topics #{state.topics || "?"}: #{inspect(reason)}"
|
||||
)
|
||||
|
||||
Streamer.remove_socket(state.topic)
|
||||
Enum.each(state.topics, fn topic -> Streamer.remove_socket(topic) end)
|
||||
:ok
|
||||
end
|
||||
|
||||
|
|
@ -137,4 +155,103 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do
|
|||
defp timer do
|
||||
Process.send_after(self(), :tick, @tick)
|
||||
end
|
||||
|
||||
defp handle_client_event(%{"type" => "subscribe", "stream" => _topic} = params, state) do
|
||||
with {_, {:ok, topic}} <-
|
||||
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
|
||||
{_, false} <- {:subscribed, topic in state.topics} do
|
||||
Streamer.add_socket(topic, state.oauth_token)
|
||||
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "success"})}
|
||||
], %{state | topics: [topic | state.topics]}}
|
||||
else
|
||||
{:subscribed, true} ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{type: "subscribe", result: "ignored"})}
|
||||
], state}
|
||||
|
||||
{:topic, {:error, error}} ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{
|
||||
type: "subscribe",
|
||||
result: "error",
|
||||
error: error
|
||||
})}
|
||||
], state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_client_event(%{"type" => "unsubscribe", "stream" => _topic} = params, state) do
|
||||
with {_, {:ok, topic}} <-
|
||||
{:topic, Streamer.get_topic(params["stream"], state.user, state.oauth_token, params)},
|
||||
{_, true} <- {:subscribed, topic in state.topics} do
|
||||
Streamer.remove_socket(topic)
|
||||
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "success"})}
|
||||
], %{state | topics: List.delete(state.topics, topic)}}
|
||||
else
|
||||
{:subscribed, false} ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{type: "unsubscribe", result: "ignored"})}
|
||||
], state}
|
||||
|
||||
{:topic, {:error, error}} ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{
|
||||
type: "unsubscribe",
|
||||
result: "error",
|
||||
error: error
|
||||
})}
|
||||
], state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_client_event(
|
||||
%{"type" => "pleroma:authenticate", "token" => access_token} = _params,
|
||||
state
|
||||
) do
|
||||
with {:auth, nil, nil} <- {:auth, state.user, state.oauth_token},
|
||||
{:ok, user, oauth_token} <- authenticate_request(access_token, nil) do
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{
|
||||
type: "pleroma:authenticate",
|
||||
result: "success"
|
||||
})}
|
||||
], %{state | user: user, oauth_token: oauth_token}}
|
||||
else
|
||||
{:auth, _, _} ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{
|
||||
type: "pleroma:authenticate",
|
||||
result: "error",
|
||||
error: :already_authenticated
|
||||
})}
|
||||
], state}
|
||||
|
||||
_ ->
|
||||
{[
|
||||
{:text,
|
||||
StreamerView.render("pleroma_respond.json", %{
|
||||
type: "pleroma:authenticate",
|
||||
result: "error",
|
||||
error: :unauthorized
|
||||
})}
|
||||
], state}
|
||||
end
|
||||
end
|
||||
|
||||
defp handle_client_event(params, state) do
|
||||
Logger.error("#{__MODULE__} received unknown event: #{inspect(params)}")
|
||||
{[], state}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
|||
alias Pleroma.Web.MediaProxy
|
||||
alias Plug.Conn
|
||||
|
||||
plug(:sandbox)
|
||||
|
||||
def remote(conn, %{"sig" => sig64, "url" => url64}) do
|
||||
with {_, true} <- {:enabled, MediaProxy.enabled?()},
|
||||
{:ok, url} <- MediaProxy.decode_url(sig64, url64),
|
||||
|
|
@ -202,4 +204,9 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do
|
|||
defp media_proxy_opts do
|
||||
Config.get([:media_proxy, :proxy_opts], [])
|
||||
end
|
||||
|
||||
defp sandbox(conn, _params) do
|
||||
conn
|
||||
|> merge_resp_headers([{"content-security-policy", "sandbox;"}])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -8,12 +8,20 @@ defmodule Pleroma.Web.Metadata.Providers.RelMe do
|
|||
|
||||
@impl Provider
|
||||
def build_tags(%{user: user}) do
|
||||
bio_tree = Floki.parse_fragment!(user.bio)
|
||||
profile_tree =
|
||||
user.bio
|
||||
|> append_fields_tag(user.fields)
|
||||
|> Floki.parse_fragment!()
|
||||
|
||||
(Floki.attribute(bio_tree, "link[rel~=me]", "href") ++
|
||||
Floki.attribute(bio_tree, "a[rel~=me]", "href"))
|
||||
(Floki.attribute(profile_tree, "link[rel~=me]", "href") ++
|
||||
Floki.attribute(profile_tree, "a[rel~=me]", "href"))
|
||||
|> Enum.map(fn link ->
|
||||
{:link, [rel: "me", href: link], []}
|
||||
end)
|
||||
end
|
||||
|
||||
defp append_fields_tag(bio, fields) do
|
||||
fields
|
||||
|> Enum.reduce(bio, fn %{"value" => v}, res -> res <> v end)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -76,9 +76,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
|||
{:meta, [name: "twitter:card", content: "summary_large_image"], []},
|
||||
{:meta,
|
||||
[
|
||||
name: "twitter:player",
|
||||
name: "twitter:image",
|
||||
content: MediaProxy.url(url["href"])
|
||||
], []}
|
||||
], []},
|
||||
{:meta, [name: "twitter:image:alt", content: truncate(attachment["name"])], []}
|
||||
| acc
|
||||
]
|
||||
|> maybe_add_dimensions(url)
|
||||
|
|
@ -130,4 +131,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do
|
|||
metadata
|
||||
end
|
||||
end
|
||||
|
||||
defp truncate(nil), do: ""
|
||||
|
||||
defp truncate(text) do
|
||||
# truncate to 420 characters
|
||||
# see https://developer.twitter.com/en/docs/twitter-for-websites/cards/overview/markup
|
||||
Pleroma.Formatter.truncate(text, 420)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,12 +30,13 @@ defmodule Pleroma.Web.Metadata.Utils do
|
|||
|> scrub_html_and_truncate_object_field(object)
|
||||
end
|
||||
|
||||
def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content) do
|
||||
def scrub_html_and_truncate(content, max_length \\ 200, omission \\ "...")
|
||||
when is_binary(content) do
|
||||
content
|
||||
|> scrub_html
|
||||
|> Emoji.Formatter.demojify()
|
||||
|> HtmlEntities.decode()
|
||||
|> Formatter.truncate(max_length)
|
||||
|> Formatter.truncate(max_length, omission)
|
||||
end
|
||||
|
||||
def scrub_html(content) when is_binary(content) do
|
||||
|
|
|
|||
22
lib/pleroma/web/multipart.ex
Normal file
22
lib/pleroma/web/multipart.ex
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2023 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
# <https://hexdocs.pm/plug/Plug.Parsers.MULTIPART.html#module-dynamic-configuration>
|
||||
defmodule Pleroma.Web.Multipart do
|
||||
@multipart Plug.Parsers.MULTIPART
|
||||
|
||||
def init(opts) do
|
||||
opts
|
||||
end
|
||||
|
||||
def parse(conn, "multipart", subtype, headers, opts) do
|
||||
length = Pleroma.Config.get([:instance, :upload_limit])
|
||||
opts = @multipart.init([length: length] ++ opts)
|
||||
@multipart.parse(conn, "multipart", subtype, headers, opts)
|
||||
end
|
||||
|
||||
def parse(conn, _type, _subtype, _headers, _opts) do
|
||||
{:next, conn}
|
||||
end
|
||||
end
|
||||
|
|
@ -49,6 +49,10 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
|
|||
enabled: false
|
||||
},
|
||||
staffAccounts: staff_accounts,
|
||||
roles: %{
|
||||
admin: Config.get([:instance, :admin_privileges]),
|
||||
moderator: Config.get([:instance, :moderator_privileges])
|
||||
},
|
||||
federation: federation,
|
||||
pollLimits: Config.get([:instance, :poll_limits]),
|
||||
postFormats: Config.get([:instance, :allowed_post_formats]),
|
||||
|
|
@ -69,8 +73,7 @@ defmodule Pleroma.Web.Nodeinfo.Nodeinfo do
|
|||
mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false),
|
||||
features: features,
|
||||
restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]),
|
||||
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false),
|
||||
privilegedStaff: Config.get([:instance, :privileged_staff])
|
||||
skipThreadContainment: Config.get([:instance, :skip_thread_containment], false)
|
||||
}
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
|
||||
with true <- Pleroma.Config.get([:instance, :show_reactions]),
|
||||
%Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
|
||||
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
|
||||
Object.normalize(activity, fetch: false) do
|
||||
%Object{} = object <- Object.normalize(activity, fetch: false),
|
||||
reactions <- Object.get_emoji_reactions(object) do
|
||||
reactions =
|
||||
reactions
|
||||
|> filter(params)
|
||||
|
|
@ -50,29 +50,32 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
if not with_muted, do: User.cached_muted_users_ap_ids(user), else: []
|
||||
end
|
||||
|
||||
filter_emoji = fn emoji, users ->
|
||||
filter_emoji = fn emoji, users, url ->
|
||||
case Enum.reject(users, &(&1 in exclude_ap_ids)) do
|
||||
[] -> nil
|
||||
users -> {emoji, users}
|
||||
users -> {emoji, users, url}
|
||||
end
|
||||
end
|
||||
|
||||
reactions
|
||||
|> Stream.map(fn
|
||||
[emoji, users] when is_list(users) -> filter_emoji.(emoji, users)
|
||||
{emoji, users} when is_list(users) -> filter_emoji.(emoji, users)
|
||||
_ -> nil
|
||||
[emoji, users, url] when is_list(users) -> filter_emoji.(emoji, users, url)
|
||||
end)
|
||||
|> Stream.reject(&is_nil/1)
|
||||
end
|
||||
|
||||
defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
|
||||
Enum.filter(reactions, fn [e, _] -> e == emoji end)
|
||||
Enum.filter(reactions, fn [e, _, _] -> e == emoji end)
|
||||
end
|
||||
|
||||
defp filter(reactions, _), do: reactions
|
||||
|
||||
def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
|
||||
emoji =
|
||||
emoji
|
||||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
@ -83,6 +86,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
|
|||
end
|
||||
|
||||
def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
|
||||
emoji =
|
||||
emoji
|
||||
|> Pleroma.Emoji.fully_qualify_emoji()
|
||||
|> Pleroma.Emoji.maybe_quote()
|
||||
|
||||
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
|
||||
activity = Activity.get_by_id(activity_id)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,12 +9,22 @@ defmodule Pleroma.Web.PleromaAPI.BackupView do
|
|||
alias Pleroma.Web.CommonAPI.Utils
|
||||
|
||||
def render("show.json", %{backup: %Backup{} = backup}) do
|
||||
# To deal with records before the migration
|
||||
state =
|
||||
if backup.state == :invalid do
|
||||
if backup.processed, do: :complete, else: :failed
|
||||
else
|
||||
backup.state
|
||||
end
|
||||
|
||||
%{
|
||||
id: backup.id,
|
||||
content_type: backup.content_type,
|
||||
url: download_url(backup),
|
||||
file_size: backup.file_size,
|
||||
processed: backup.processed,
|
||||
state: to_string(state),
|
||||
processed_number: backup.processed_number,
|
||||
inserted_at: Utils.to_masto_date(backup.inserted_at)
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,17 +7,30 @@ defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
|
|||
|
||||
alias Pleroma.Web.MastodonAPI.AccountView
|
||||
|
||||
def emoji_name(emoji, nil), do: emoji
|
||||
|
||||
def emoji_name(emoji, url) do
|
||||
url = URI.parse(url)
|
||||
|
||||
if url.host == Pleroma.Web.Endpoint.host() do
|
||||
emoji
|
||||
else
|
||||
"#{emoji}@#{url.host}"
|
||||
end
|
||||
end
|
||||
|
||||
def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
|
||||
render_many(emoji_reactions, __MODULE__, "show.json", opts)
|
||||
end
|
||||
|
||||
def render("show.json", %{emoji_reaction: {emoji, user_ap_ids}, user: user}) do
|
||||
def render("show.json", %{emoji_reaction: {emoji, user_ap_ids, url}, user: user}) do
|
||||
users = fetch_users(user_ap_ids)
|
||||
|
||||
%{
|
||||
name: emoji,
|
||||
name: emoji_name(emoji, url),
|
||||
count: length(users),
|
||||
accounts: render(AccountView, "index.json", users: users, for: user),
|
||||
url: Pleroma.Web.MediaProxy.url(url),
|
||||
me: !!(user && user.ap_id in user_ap_ids)
|
||||
}
|
||||
end
|
||||
|
|
|
|||
|
|
@ -38,10 +38,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
|
|||
|
||||
def call(conn, _), do: conn
|
||||
|
||||
def checkpw(password, "$6" <> _ = password_hash) do
|
||||
:crypt.crypt(password, password_hash) == password_hash
|
||||
end
|
||||
|
||||
def checkpw(password, "$2" <> _ = password_hash) do
|
||||
# Handle bcrypt passwords for Mastodon migration
|
||||
Bcrypt.verify_pass(password, password_hash)
|
||||
|
|
@ -60,10 +56,6 @@ defmodule Pleroma.Web.Plugs.AuthenticationPlug do
|
|||
do_update_password(user, password)
|
||||
end
|
||||
|
||||
def maybe_update_password(%User{password_hash: "$6" <> _} = user, password) do
|
||||
do_update_password(user, password)
|
||||
end
|
||||
|
||||
def maybe_update_password(user, _), do: {:ok, user}
|
||||
|
||||
defp do_update_password(user, password) do
|
||||
|
|
|
|||
44
lib/pleroma/web/plugs/ensure_privileged_plug.ex
Normal file
44
lib/pleroma/web/plugs/ensure_privileged_plug.ex
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsurePrivilegedPlug do
|
||||
@moduledoc """
|
||||
Ensures staff are privileged enough to do certain tasks.
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{is_admin: false, is_moderator: false}}} = conn, _) do
|
||||
conn
|
||||
|> render_error(:forbidden, "User isn't privileged.")
|
||||
|> halt()
|
||||
end
|
||||
|
||||
def call(
|
||||
%{assigns: %{user: %User{is_admin: is_admin, is_moderator: is_moderator}}} = conn,
|
||||
privilege
|
||||
) do
|
||||
if (is_admin and privilege in Config.get([:instance, :admin_privileges])) or
|
||||
(is_moderator and privilege in Config.get([:instance, :moderator_privileges])) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> render_error(:forbidden, "User isn't privileged.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|> render_error(:forbidden, "User isn't privileged.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
# Pleroma: A lightweight social networking server
|
||||
# Copyright © 2017-2022 Pleroma Authors <https://pleroma.social/>
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
defmodule Pleroma.Web.Plugs.EnsureStaffPrivilegedPlug do
|
||||
@moduledoc """
|
||||
Ensures staff are privileged enough to do certain tasks.
|
||||
"""
|
||||
import Pleroma.Web.TranslationHelpers
|
||||
import Plug.Conn
|
||||
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.User
|
||||
|
||||
def init(options) do
|
||||
options
|
||||
end
|
||||
|
||||
def call(%{assigns: %{user: %User{is_admin: true}}} = conn, _), do: conn
|
||||
|
||||
def call(%{assigns: %{user: %User{is_moderator: true}}} = conn, _) do
|
||||
if Config.get!([:instance, :privileged_staff]) do
|
||||
conn
|
||||
else
|
||||
conn
|
||||
|> render_error(:forbidden, "User is not an admin.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
||||
def call(conn, _) do
|
||||
conn
|
||||
|> render_error(:forbidden, "User is not a staff member.")
|
||||
|> halt()
|
||||
end
|
||||
end
|
||||
|
|
@ -93,18 +93,26 @@ defmodule Pleroma.Web.Plugs.HTTPSecurityPlug do
|
|||
|
||||
img_src = "img-src 'self' data: blob:"
|
||||
media_src = "media-src 'self'"
|
||||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||
|
||||
# Strict multimedia CSP enforcement only when MediaProxy is enabled
|
||||
{img_src, media_src} =
|
||||
{img_src, media_src, connect_src} =
|
||||
if Config.get([:media_proxy, :enabled]) &&
|
||||
!Config.get([:media_proxy, :proxy_opts, :redirect_on_failure]) do
|
||||
sources = build_csp_multimedia_source_list()
|
||||
{[img_src, sources], [media_src, sources]}
|
||||
else
|
||||
{[img_src, " https:"], [media_src, " https:"]}
|
||||
end
|
||||
|
||||
connect_src = ["connect-src 'self' blob: ", static_url, ?\s, websocket_url]
|
||||
{
|
||||
[img_src, sources],
|
||||
[media_src, sources],
|
||||
[connect_src, sources]
|
||||
}
|
||||
else
|
||||
{
|
||||
[img_src, " https:"],
|
||||
[media_src, " https:"],
|
||||
[connect_src, " https:"]
|
||||
}
|
||||
end
|
||||
|
||||
connect_src =
|
||||
if Config.get(:env) == :dev do
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ defmodule Pleroma.Web.Plugs.HTTPSignaturePlug do
|
|||
end
|
||||
|
||||
def call(conn, _opts) do
|
||||
if get_format(conn) == "activity+json" do
|
||||
if get_format(conn) in ["json", "activity+json"] do
|
||||
conn
|
||||
|> maybe_assign_valid_signature()
|
||||
|> maybe_require_signature()
|
||||
|
|
|
|||
|
|
@ -35,9 +35,9 @@ defmodule Pleroma.Web.Plugs.UploadedMedia do
|
|||
conn =
|
||||
case fetch_query_params(conn) do
|
||||
%{query_params: %{"name" => name}} = conn ->
|
||||
name = String.replace(name, "\"", "\\\"")
|
||||
name = String.replace(name, ~s["], ~s[\\"])
|
||||
|
||||
put_resp_header(conn, "content-disposition", "filename=\"#{name}\"")
|
||||
put_resp_header(conn, "content-disposition", ~s[inline; filename="#{name}"])
|
||||
|
||||
conn ->
|
||||
conn
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.Preload do
|
|||
terms =
|
||||
params
|
||||
|> parser.generate_terms()
|
||||
|> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v))} end)
|
||||
|> Enum.map(fn {k, v} -> {k, Base.encode64(Jason.encode!(v, escape: :html_safe))} end)
|
||||
|> Enum.into(%{})
|
||||
|
||||
Map.merge(acc, terms)
|
||||
|
|
@ -19,7 +19,7 @@ defmodule Pleroma.Web.Preload do
|
|||
|
||||
rendered_html =
|
||||
preload_data
|
||||
|> Jason.encode!()
|
||||
|> Jason.encode!(escape: :html_safe)
|
||||
|> build_script_tag()
|
||||
|> HTML.safe_to_string()
|
||||
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue