Merge remote-tracking branch 'remotes/origin/develop' into authenticated-api-oauth-check-enforcement

This commit is contained in:
Ivan Tashkinov 2020-04-15 19:20:34 +03:00
commit bedf92e064
117 changed files with 2612 additions and 532 deletions

View file

@ -5,7 +5,6 @@
defmodule Mix.Pleroma do
@doc "Common functions to be reused in mix tasks"
def start_pleroma do
Mix.Task.run("app.start")
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do

View file

@ -67,7 +67,8 @@ defmodule Mix.Tasks.Pleroma.Benchmark do
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
activities: activities,
for: user,
as: :activity
as: :activity,
skip_relationships: true
})
end
},

View file

@ -14,8 +14,8 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{options, [], []} = parse_global_opts(args)
manifest =
fetch_manifest(if options[:manifest], do: options[:manifest], else: default_manifest())
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(url_or_path)
Enum.each(manifest, fn {name, info} ->
to_print = [
@ -40,9 +40,9 @@ defmodule Mix.Tasks.Pleroma.Emoji do
{options, pack_names, []} = parse_global_opts(args)
manifest_url = if options[:manifest], do: options[:manifest], else: default_manifest()
url_or_path = options[:manifest] || default_manifest()
manifest = fetch_manifest(manifest_url)
manifest = fetch_manifest(url_or_path)
for pack_name <- pack_names do
if Map.has_key?(manifest, pack_name) do
@ -75,7 +75,10 @@ defmodule Mix.Tasks.Pleroma.Emoji do
end
# The url specified in files should be in the same directory
files_url = Path.join(Path.dirname(manifest_url), pack["files"])
files_url =
url_or_path
|> Path.dirname()
|> Path.join(pack["files"])
IO.puts(
IO.ANSI.format([
@ -133,38 +136,51 @@ defmodule Mix.Tasks.Pleroma.Emoji do
end
end
def run(["gen-pack", src]) do
def run(["gen-pack" | args]) do
start_pleroma()
proposed_name = Path.basename(src) |> Path.rootname()
name = String.trim(IO.gets("Pack name [#{proposed_name}]: "))
# If there's no name, use the default one
name = if String.length(name) > 0, do: name, else: proposed_name
license = String.trim(IO.gets("License: "))
homepage = String.trim(IO.gets("Homepage: "))
description = String.trim(IO.gets("Description: "))
proposed_files_name = "#{name}.json"
files_name = String.trim(IO.gets("Save file list to [#{proposed_files_name}]: "))
files_name = if String.length(files_name) > 0, do: files_name, else: proposed_files_name
default_exts = [".png", ".gif"]
default_exts_str = Enum.join(default_exts, " ")
exts =
String.trim(
IO.gets("Emoji file extensions (separated with spaces) [#{default_exts_str}]: ")
{opts, [src], []} =
OptionParser.parse(
args,
strict: [
name: :string,
license: :string,
homepage: :string,
description: :string,
files: :string,
extensions: :string
]
)
proposed_name = Path.basename(src) |> Path.rootname()
name = get_option(opts, :name, "Pack name:", proposed_name)
license = get_option(opts, :license, "License:")
homepage = get_option(opts, :homepage, "Homepage:")
description = get_option(opts, :description, "Description:")
proposed_files_name = "#{name}_files.json"
files_name = get_option(opts, :files, "Save file list to:", proposed_files_name)
default_exts = [".png", ".gif"]
custom_exts =
get_option(
opts,
:extensions,
"Emoji file extensions (separated with spaces):",
Enum.join(default_exts, " ")
)
|> String.split(" ", trim: true)
exts =
if String.length(exts) > 0 do
String.split(exts, " ")
|> Enum.filter(fn e -> e |> String.trim() |> String.length() > 0 end)
else
if MapSet.equal?(MapSet.new(default_exts), MapSet.new(custom_exts)) do
default_exts
else
custom_exts
end
IO.puts("Using #{Enum.join(exts, " ")} extensions")
IO.puts("Downloading the pack and generating SHA256")
binary_archive = Tesla.get!(client(), src).body
@ -194,14 +210,16 @@ defmodule Mix.Tasks.Pleroma.Emoji do
IO.puts("""
#{files_name} has been created and contains the list of all found emojis in the pack.
Please review the files in the remove those not needed.
Please review the files in the pack and remove those not needed.
""")
if File.exists?("index.json") do
existing_data = File.read!("index.json") |> Jason.decode!()
pack_file = "#{name}.json"
if File.exists?(pack_file) do
existing_data = File.read!(pack_file) |> Jason.decode!()
File.write!(
"index.json",
pack_file,
Jason.encode!(
Map.merge(
existing_data,
@ -211,11 +229,11 @@ defmodule Mix.Tasks.Pleroma.Emoji do
)
)
IO.puts("index.json file has been update with the #{name} pack")
IO.puts("#{pack_file} has been updated with the #{name} pack")
else
File.write!("index.json", Jason.encode!(pack_json, pretty: true))
File.write!(pack_file, Jason.encode!(pack_json, pretty: true))
IO.puts("index.json has been created with the #{name} pack")
IO.puts("#{pack_file} has been created with the #{name} pack")
end
end

View file

@ -54,10 +54,19 @@ defmodule Pleroma.Config.TransferTask do
[:pleroma, nil, :prometheus]
end
{logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&transform_and_merge/1)
|> Enum.split_with(fn {group, _, _, _} -> group in [:logger, :quack] end)
logger
|> Enum.sort()
|> Enum.each(&configure/1)
started_applications = Application.started_applications()
(Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&merge_and_update/1)
other
|> Enum.map(&update/1)
|> Enum.uniq()
|> Enum.reject(&(&1 in reject_restart))
|> maybe_set_pleroma_last()
@ -81,51 +90,66 @@ defmodule Pleroma.Config.TransferTask do
end
end
defp group_for_restart(:logger, key, _, merged_value) do
# change logger configuration in runtime, without restart
if Keyword.keyword?(merged_value) and
key not in [:compile_time_application, :backends, :compile_time_purge_matching] do
Logger.configure_backend(key, merged_value)
else
Logger.configure([{key, merged_value}])
end
defp transform_and_merge(%{group: group, key: key, value: value} = setting) do
group = ConfigDB.from_string(group)
key = ConfigDB.from_string(key)
value = ConfigDB.from_binary(value)
nil
default = Config.Holder.default_config(group, key)
merged =
cond do
Ecto.get_meta(setting, :state) == :deleted -> default
can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
true -> value
end
{group, key, value, merged}
end
defp group_for_restart(group, _, _, _) when group != :pleroma, do: group
defp group_for_restart(group, key, value, _) do
if pleroma_need_restart?(group, key, value), do: group
# change logger configuration in runtime, without restart
defp configure({:quack, key, _, merged}) do
Logger.configure_backend(Quack.Logger, [{key, merged}])
:ok = update_env(:quack, key, merged)
end
defp merge_and_update(setting) do
defp configure({_, :backends, _, merged}) do
# removing current backends
Enum.each(Application.get_env(:logger, :backends), &Logger.remove_backend/1)
Enum.each(merged, &Logger.add_backend/1)
:ok = update_env(:logger, :backends, merged)
end
defp configure({group, key, _, merged}) do
merged =
if key == :console do
put_in(merged[:format], merged[:format] <> "\n")
else
merged
end
backend =
if key == :ex_syslogger,
do: {ExSyslogger, :ex_syslogger},
else: key
Logger.configure_backend(backend, merged)
:ok = update_env(:logger, group, merged)
end
defp update({group, key, value, merged}) do
try do
key = ConfigDB.from_string(setting.key)
group = ConfigDB.from_string(setting.group)
:ok = update_env(group, key, merged)
default = Config.Holder.default_config(group, key)
value = ConfigDB.from_binary(setting.value)
merged_value =
cond do
Ecto.get_meta(setting, :state) == :deleted -> default
can_be_merged?(default, value) -> ConfigDB.merge_group(group, key, default, value)
true -> value
end
:ok = update_env(group, key, merged_value)
group_for_restart(group, key, value, merged_value)
if group != :pleroma or pleroma_need_restart?(group, key, value), do: group
rescue
error ->
error_msg =
"updating env causes error, group: " <>
inspect(setting.group) <>
" key: " <>
inspect(setting.key) <>
" value: " <>
inspect(ConfigDB.from_binary(setting.value)) <> " error: " <> inspect(error)
"updating env causes error, group: #{inspect(group)}, key: #{inspect(key)}, value: #{
inspect(value)
} error: #{inspect(error)}"
Logger.warn(error_msg)
@ -133,6 +157,9 @@ defmodule Pleroma.Config.TransferTask do
end
end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
@spec pleroma_need_restart?(atom(), atom(), any()) :: boolean()
def pleroma_need_restart?(group, key, value) do
group_and_key_need_reboot?(group, key) or group_and_subkey_need_reboot?(group, key, value)
@ -150,9 +177,6 @@ defmodule Pleroma.Config.TransferTask do
end)
end
defp update_env(group, key, nil), do: Application.delete_env(group, key)
defp update_env(group, key, value), do: Application.put_env(group, key, value)
defp restart(_, :pleroma, env), do: Restarter.Pleroma.restart_after_boot(env)
defp restart(started_applications, app, _) do

View file

@ -4,10 +4,16 @@
import EctoEnum
defenum(UserRelationshipTypeEnum,
defenum(Pleroma.UserRelationship.Type,
block: 1,
mute: 2,
reblog_mute: 3,
notification_mute: 4,
inverse_subscription: 5
)
defenum(Pleroma.FollowingRelationship.State,
follow_pending: 1,
follow_accept: 2,
follow_reject: 3
)

View file

@ -8,12 +8,13 @@ defmodule Pleroma.FollowingRelationship do
import Ecto.Changeset
import Ecto.Query
alias Ecto.Changeset
alias FlakeId.Ecto.CompatType
alias Pleroma.Repo
alias Pleroma.User
schema "following_relationships" do
field(:state, :string, default: "accept")
field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending)
belongs_to(:follower, User, type: CompatType)
belongs_to(:following, User, type: CompatType)
@ -27,6 +28,18 @@ defmodule Pleroma.FollowingRelationship do
|> put_assoc(:follower, attrs.follower)
|> put_assoc(:following, attrs.following)
|> validate_required([:state, :follower, :following])
|> unique_constraint(:follower_id,
name: :following_relationships_follower_id_following_id_index
)
|> validate_not_self_relationship()
end
def state_to_enum(state) when state in ["pending", "accept", "reject"] do
String.to_existing_atom("follow_#{state}")
end
def state_to_enum(state) do
raise "State is not convertible to Pleroma.FollowingRelationship.State: #{state}"
end
def get(%User{} = follower, %User{} = following) do
@ -35,7 +48,7 @@ defmodule Pleroma.FollowingRelationship do
|> Repo.one()
end
def update(follower, following, "reject"), do: unfollow(follower, following)
def update(follower, following, :follow_reject), do: unfollow(follower, following)
def update(%User{} = follower, %User{} = following, state) do
case get(follower, following) do
@ -50,7 +63,7 @@ defmodule Pleroma.FollowingRelationship do
end
end
def follow(%User{} = follower, %User{} = following, state \\ "accept") do
def follow(%User{} = follower, %User{} = following, state \\ :follow_accept) do
%__MODULE__{}
|> changeset(%{follower: follower, following: following, state: state})
|> Repo.insert(on_conflict: :nothing)
@ -80,7 +93,7 @@ defmodule Pleroma.FollowingRelationship do
def get_follow_requests(%User{id: id}) do
__MODULE__
|> join(:inner, [r], f in assoc(r, :follower))
|> where([r], r.state == "pending")
|> where([r], r.state == ^:follow_pending)
|> where([r], r.following_id == ^id)
|> select([r, f], f)
|> Repo.all()
@ -88,7 +101,7 @@ defmodule Pleroma.FollowingRelationship do
def following?(%User{id: follower_id}, %User{id: followed_id}) do
__MODULE__
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: "accept")
|> where(follower_id: ^follower_id, following_id: ^followed_id, state: ^:follow_accept)
|> Repo.exists?()
end
@ -97,7 +110,7 @@ defmodule Pleroma.FollowingRelationship do
__MODULE__
|> join(:inner, [r], u in User, on: r.following_id == u.id)
|> where([r], r.follower_id == ^user.id)
|> where([r], r.state == "accept")
|> where([r], r.state == ^:follow_accept)
|> select([r, u], u.follower_address)
|> Repo.all()
@ -157,4 +170,30 @@ defmodule Pleroma.FollowingRelationship do
fr -> fr.follower_id == follower.id and fr.following_id == following.id
end)
end
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_follower_id_following_id_inequality()
|> validate_following_id_follower_id_inequality()
end
defp validate_follower_id_following_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :follower_id, fn _, follower_id ->
if follower_id == get_field(changeset, :following_id) do
[source_id: "can't be equal to following_id"]
else
[]
end
end)
end
defp validate_following_id_follower_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :following_id, fn _, following_id ->
if following_id == get_field(changeset, :follower_id) do
[target_id: "can't be equal to follower_id"]
else
[]
end
end)
end
end

View file

@ -35,9 +35,19 @@ defmodule Pleroma.Formatter do
nickname_text = get_nickname_text(nickname, opts)
link =
~s(<span class="h-card"><a data-user="#{id}" class="u-url mention" href="#{ap_id}" rel="ugc">@<span>#{
nickname_text
}</span></a></span>)
Phoenix.HTML.Tag.content_tag(
:span,
Phoenix.HTML.Tag.content_tag(
:a,
["@", Phoenix.HTML.Tag.content_tag(:span, nickname_text)],
"data-user": id,
class: "u-url mention",
href: ap_id,
rel: "ugc"
),
class: "h-card"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@ -49,7 +59,15 @@ defmodule Pleroma.Formatter do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
link = ~s(<a class="hashtag" data-tag="#{tag}" href="#{url}" rel="tag ugc">#{tag_text}</a>)
link =
Phoenix.HTML.Tag.content_tag(:a, tag_text,
class: "hashtag",
"data-tag": tag,
href: url,
rel: "tag ugc"
)
|> Phoenix.HTML.safe_to_string()
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end

View file

@ -49,8 +49,10 @@ defmodule Pleroma.Gun.Conn do
key = "#{uri.scheme}:#{uri.host}:#{uri.port}"
max_connections = pool_opts[:max_connections] || 250
conn_pid =
if Connections.count(name) < opts[:max_connection] do
if Connections.count(name) < max_connections do
do_open(uri, opts)
else
close_least_used_and_do_open(name, uri, opts)

View file

@ -32,6 +32,18 @@ defmodule Pleroma.Object.Containment do
get_actor(%{"actor" => actor})
end
def get_object(%{"object" => id}) when is_binary(id) do
id
end
def get_object(%{"object" => %{"id" => id}}) when is_binary(id) do
id
end
def get_object(_) do
nil
end
# TODO: We explicitly allow 'tag' URIs through, due to references to legacy OStatus
# objects being present in the test suite environment. Once these objects are
# removed, please also remove this.

View file

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

View file

@ -110,20 +110,9 @@ defmodule Pleroma.Plugs.RateLimiter do
end
def disabled?(conn) do
localhost_or_socket =
case Config.get([Pleroma.Web.Endpoint, :http, :ip]) do
{127, 0, 0, 1} -> true
{0, 0, 0, 0, 0, 0, 0, 1} -> true
{:local, _} -> true
_ -> false
end
remote_ip_not_found =
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
localhost_or_socket and remote_ip_not_found
if Map.has_key?(conn.assigns, :remote_ip_found),
do: !conn.assigns.remote_ip_found,
else: false
end
@inspect_bucket_not_found {:error, :not_found}

View file

@ -7,8 +7,6 @@ defmodule Pleroma.Plugs.RemoteIp do
This is a shim to call [`RemoteIp`](https://git.pleroma.social/pleroma/remote_ip) but with runtime configuration.
"""
import Plug.Conn
@behaviour Plug
@headers ~w[
@ -28,12 +26,11 @@ defmodule Pleroma.Plugs.RemoteIp do
def init(_), do: nil
def call(%{remote_ip: original_remote_ip} = conn, _) do
def call(conn, _) do
config = Pleroma.Config.get(__MODULE__, [])
if Keyword.get(config, :enabled, false) do
%{remote_ip: new_remote_ip} = conn = RemoteIp.call(conn, remote_ip_opts(config))
assign(conn, :remote_ip_found, original_remote_ip != new_remote_ip)
RemoteIp.call(conn, remote_ip_opts(config))
else
conn
end

View file

@ -41,6 +41,7 @@ defmodule Pleroma.Plugs.UploadedMedia do
conn ->
conn
end
|> merge_resp_headers([{"content-security-policy", "sandbox"}])
config = Pleroma.Config.get(Pleroma.Upload)

View file

@ -243,7 +243,7 @@ defmodule Pleroma.Pool.Connections do
@impl true
def handle_info({:DOWN, _ref, :process, conn_pid, reason}, state) do
Logger.debug("received DOWM message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
Logger.debug("received DOWN message for #{inspect(conn_pid)} reason -> #{inspect(reason)}")
state =
with {key, conn} <- find_conn(state.conns, conn_pid) do

View file

@ -16,6 +16,7 @@ defmodule Pleroma.User do
alias Pleroma.Conversation.Participation
alias Pleroma.Delivery
alias Pleroma.FollowingRelationship
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Keys
alias Pleroma.Notification
@ -452,7 +453,7 @@ defmodule Pleroma.User do
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
|> Enum.map(fn f -> Map.update!(f, "value", &parse_fields(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
@ -462,6 +463,12 @@ defmodule Pleroma.User do
end
end
defp parse_fields(value) do
value
|> Formatter.linkify(mentions_format: :full)
|> elem(0)
end
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
@ -693,7 +700,7 @@ defmodule Pleroma.User do
@spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()}
def maybe_direct_follow(%User{} = follower, %User{local: true, locked: true} = followed) do
follow(follower, followed, "pending")
follow(follower, followed, :follow_pending)
end
def maybe_direct_follow(%User{} = follower, %User{local: true} = followed) do
@ -713,14 +720,14 @@ defmodule Pleroma.User do
def follow_all(follower, followeds) do
followeds
|> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
|> Enum.each(&follow(follower, &1, "accept"))
|> Enum.each(&follow(follower, &1, :follow_accept))
set_cache(follower)
end
defdelegate following(user), to: FollowingRelationship
def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
def follow(%User{} = follower, %User{} = followed, state \\ :follow_accept) do
deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked])
cond do
@ -747,7 +754,7 @@ defmodule Pleroma.User do
def unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in ["accept", "pending"] ->
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
{:ok, followed} = update_follower_count(followed)
@ -765,6 +772,7 @@ defmodule Pleroma.User do
defdelegate following?(follower, followed), to: FollowingRelationship
@doc "Returns follow state as Pleroma.FollowingRelationship.State value"
def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
@ -778,8 +786,11 @@ defmodule Pleroma.User do
case {following_relationship, following.local} do
{nil, false} ->
case Utils.fetch_latest_follow(follower, following) do
%{data: %{"state" => state}} when state in ["pending", "accept"] -> state
_ -> nil
%Activity{data: %{"state" => state}} when state in ["pending", "accept"] ->
FollowingRelationship.state_to_enum(state)
_ ->
nil
end
{%{state: state}, _} ->
@ -1278,7 +1289,7 @@ defmodule Pleroma.User do
def blocks?(%User{} = user, %User{} = target) do
blocks_user?(user, target) ||
(!User.following?(user, target) && blocks_domain?(user, target))
(blocks_domain?(user, target) and not User.following?(user, target))
end
def blocks_user?(%User{} = user, %User{} = target) do
@ -1979,17 +1990,6 @@ defmodule Pleroma.User do
def fields(%{fields: fields}), do: fields
def sanitized_fields(%User{} = user) do
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
end
def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0)

View file

@ -148,7 +148,7 @@ defmodule Pleroma.User.Query do
as: :relationships,
on: r.following_id == ^id and r.follower_id == u.id
)
|> where([relationships: r], r.state == "accept")
|> where([relationships: r], r.state == ^:follow_accept)
end
defp compose_query({:friends, %User{id: id}}, query) do
@ -158,7 +158,7 @@ defmodule Pleroma.User.Query do
as: :relationships,
on: r.following_id == u.id and r.follower_id == ^id
)
|> where([relationships: r], r.state == "accept")
|> where([relationships: r], r.state == ^:follow_accept)
end
defp compose_query({:recipients_from_activity, to}, query) do
@ -173,7 +173,7 @@ defmodule Pleroma.User.Query do
)
|> where(
[u, following: f, relationships: r],
u.ap_id in ^to or (f.follower_address in ^to and r.state == "accept")
u.ap_id in ^to or (f.follower_address in ^to and r.state == ^:follow_accept)
)
|> distinct(true)
end

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
import Ecto.Changeset
import Ecto.Query
alias Ecto.Changeset
alias Pleroma.FollowingRelationship
alias Pleroma.Repo
alias Pleroma.User
@ -16,12 +17,12 @@ defmodule Pleroma.UserRelationship do
schema "user_relationships" do
belongs_to(:source, User, type: FlakeId.Ecto.CompatType)
belongs_to(:target, User, type: FlakeId.Ecto.CompatType)
field(:relationship_type, UserRelationshipTypeEnum)
field(:relationship_type, Pleroma.UserRelationship.Type)
timestamps(updated_at: false)
end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do
for relationship_type <- Keyword.keys(Pleroma.UserRelationship.Type.__enum_map__()) do
# `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
# `def create_notification_mute/2`, `def create_inverse_subscription/2`
def unquote(:"create_#{relationship_type}")(source, target),
@ -40,7 +41,7 @@ defmodule Pleroma.UserRelationship do
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__()
def user_relationship_mappings, do: Pleroma.UserRelationship.Type.__enum_map__()
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship
@ -129,17 +130,27 @@ defmodule Pleroma.UserRelationship do
end
@doc ":relationships option for StatusView / AccountView / NotificationView"
def view_relationships_option(nil = _reading_user, _actors) do
def view_relationships_option(reading_user, actors, opts \\ [])
def view_relationships_option(nil = _reading_user, _actors, _opts) do
%{user_relationships: [], following_relationships: []}
end
def view_relationships_option(%User{} = reading_user, actors) do
def view_relationships_option(%User{} = reading_user, actors, opts) do
{source_to_target_rel_types, target_to_source_rel_types} =
if opts[:source_mutes_only] do
# This option is used for rendering statuses (FE needs `muted` flag for each one anyways)
{[:mute], []}
else
{[:block, :mute, :notification_mute, :reblog_mute], [:block, :inverse_subscription]}
end
user_relationships =
UserRelationship.dictionary(
[reading_user],
actors,
[:block, :mute, :notification_mute, :reblog_mute],
[:block, :inverse_subscription]
source_to_target_rel_types,
target_to_source_rel_types
)
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
@ -147,16 +158,14 @@ defmodule Pleroma.UserRelationship do
%{user_relationships: user_relationships, following_relationships: following_relationships}
end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
defp validate_not_self_relationship(%Changeset{} = changeset) do
changeset
|> validate_change(:target_id, fn _, target_id ->
if target_id == get_field(changeset, :source_id) do
[target_id: "can't be equal to source_id"]
else
[]
end
end)
|> validate_change(:source_id, fn _, source_id ->
|> validate_source_id_target_id_inequality()
|> validate_target_id_source_id_inequality()
end
defp validate_source_id_target_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :source_id, fn _, source_id ->
if source_id == get_field(changeset, :target_id) do
[source_id: "can't be equal to target_id"]
else
@ -164,4 +173,14 @@ defmodule Pleroma.UserRelationship do
end
end)
end
defp validate_target_id_source_id_inequality(%Changeset{} = changeset) do
validate_change(changeset, :target_id, fn _, target_id ->
if target_id == get_field(changeset, :source_id) do
[target_id: "can't be equal to source_id"]
else
[]
end
end)
end
end

View file

@ -125,6 +125,21 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(_create_data), do: :noop
@spec persist(map(), keyword()) :: {:ok, Activity.t() | Object.t()}
def persist(object, meta) do
with local <- Keyword.fetch!(meta, :local),
{recipients, _, _} <- get_recipients(object),
{:ok, activity} <-
Repo.insert(%Activity{
data: object,
local: local,
recipients: recipients,
actor: object["actor"]
}) do
{:ok, activity, meta}
end
end
@spec insert(map(), boolean(), boolean(), boolean()) :: {:ok, Activity.t()} | {:error, any()}
def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when is_map(map) do
with nil <- Activity.normalize(map),
@ -706,7 +721,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end
end
defp fetch_activities_for_context_query(context, opts) do
def fetch_activities_for_context_query(context, opts) do
public = [Constants.as_public()]
recipients =

View file

@ -0,0 +1,43 @@
defmodule Pleroma.Web.ActivityPub.Builder do
@moduledoc """
This module builds the objects. Meant to be used for creating local objects.
This module encodes our addressing policies and general shape of our objects.
"""
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
def like(actor, object) do
object_actor = User.get_cached_by_ap_id(object.data["actor"])
# Address the actor of the object, and our actor's follower collection if the post is public.
to =
if Visibility.is_public?(object) do
[actor.follower_address, object.data["actor"]]
else
[object.data["actor"]]
end
# CC everyone who's been addressed in the object, except ourself and the object actor's
# follower collection
cc =
(object.data["to"] ++ (object.data["cc"] || []))
|> List.delete(actor.ap_id)
|> List.delete(object_actor.follower_address)
{:ok,
%{
"id" => Utils.generate_activity_id(),
"actor" => actor.ap_id,
"type" => "Like",
"object" => object.data["id"],
"to" => to,
"cc" => cc,
"context" => object.data["context"]
}, []}
end
end

View file

@ -0,0 +1,37 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidator do
@moduledoc """
This module is responsible for validating an object (which can be an activity)
and checking if it is both well formed and also compatible with our view of
the system.
"""
alias Pleroma.Object
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
@spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
def validate(object, meta)
def validate(%{"type" => "Like"} = object, meta) do
with {:ok, object} <-
object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
object = stringify_keys(object |> Map.from_struct())
{:ok, object, meta}
end
end
def stringify_keys(object) do
object
|> Map.new(fn {key, val} -> {to_string(key), val} end)
end
def fetch_actor_and_object(object) do
User.get_or_fetch_by_ap_id(object["actor"])
Object.normalize(object["object"])
:ok
end
end

View file

@ -0,0 +1,32 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
import Ecto.Changeset
alias Pleroma.Object
alias Pleroma.User
def validate_actor_presence(cng, field_name \\ :actor) do
cng
|> validate_change(field_name, fn field_name, actor ->
if User.get_cached_by_ap_id(actor) do
[]
else
[{field_name, "can't find user"}]
end
end)
end
def validate_object_presence(cng, field_name \\ :object) do
cng
|> validate_change(field_name, fn field_name, object ->
if Object.get_cached_by_ap_id(object) do
[]
else
[{field_name, "can't find object"}]
end
end)
end
end

View file

@ -0,0 +1,30 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.CreateNoteValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:actor, Types.ObjectID)
field(:type, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
embeds_one(:object, NoteValidator)
end
def cast_data(data) do
cast(%__MODULE__{}, data, __schema__(:fields))
end
end

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
alias Pleroma.Web.ActivityPub.Utils
import Ecto.Changeset
import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:type, :string)
field(:object, Types.ObjectID)
field(:actor, Types.ObjectID)
field(:context, :string)
field(:to, {:array, :string})
field(:cc, {:array, :string})
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, [:id, :type, :object, :actor, :context, :to, :cc])
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Like"])
|> validate_required([:id, :type, :object, :actor, :context, :to, :cc])
|> validate_actor_presence()
|> validate_object_presence()
|> validate_existing_like()
end
def validate_existing_like(%{changes: %{actor: actor, object: object}} = cng) do
if Utils.get_existing_like(actor, %{data: %{"id" => object}}) do
cng
|> add_error(:actor, "already liked this object")
|> add_error(:object, "already liked by this actor")
else
cng
end
end
def validate_existing_like(cng), do: cng
end

View file

@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ObjectValidators.NoteValidator do
use Ecto.Schema
alias Pleroma.Web.ActivityPub.ObjectValidators.Types
import Ecto.Changeset
@primary_key false
embedded_schema do
field(:id, Types.ObjectID, primary_key: true)
field(:to, {:array, :string}, default: [])
field(:cc, {:array, :string}, default: [])
field(:bto, {:array, :string}, default: [])
field(:bcc, {:array, :string}, default: [])
# TODO: Write type
field(:tag, {:array, :map}, default: [])
field(:type, :string)
field(:content, :string)
field(:context, :string)
field(:actor, Types.ObjectID)
field(:attributedTo, Types.ObjectID)
field(:summary, :string)
field(:published, Types.DateTime)
# TODO: Write type
field(:emoji, :map, default: %{})
field(:sensitive, :boolean, default: false)
# TODO: Write type
field(:attachment, {:array, :map}, default: [])
field(:replies_count, :integer, default: 0)
field(:like_count, :integer, default: 0)
field(:announcement_count, :integer, default: 0)
field(:inRepyTo, :string)
field(:likes, {:array, :string}, default: [])
field(:announcements, {:array, :string}, default: [])
# see if needed
field(:conversation, :string)
field(:context_id, :string)
end
def cast_and_validate(data) do
data
|> cast_data()
|> validate_data()
end
def cast_data(data) do
%__MODULE__{}
|> cast(data, __schema__(:fields))
end
def validate_data(data_cng) do
data_cng
|> validate_inclusion(:type, ["Note"])
|> validate_required([:id, :actor, :to, :cc, :type, :content, :context])
end
end

View file

@ -0,0 +1,34 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.DateTime do
@moduledoc """
The AP standard defines the date fields in AP as xsd:DateTime. Elixir's
DateTime can't parse this, but it can parse the related iso8601. This
module punches the date until it looks like iso8601 and normalizes to
it.
DateTimes without a timezone offset are treated as UTC.
Reference: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published
"""
use Ecto.Type
def type, do: :string
def cast(datetime) when is_binary(datetime) do
with {:ok, datetime, _} <- DateTime.from_iso8601(datetime) do
{:ok, DateTime.to_iso8601(datetime)}
else
{:error, :missing_offset} -> cast("#{datetime}Z")
_e -> :error
end
end
def cast(_), do: :error
def dump(data) do
{:ok, data}
end
def load(data) do
{:ok, data}
end
end

View file

@ -0,0 +1,29 @@
defmodule Pleroma.Web.ActivityPub.ObjectValidators.Types.ObjectID do
use Ecto.Type
def type, do: :string
def cast(object) when is_binary(object) do
# Host has to be present and scheme has to be an http scheme (for now)
case URI.parse(object) do
%URI{host: nil} -> :error
%URI{host: ""} -> :error
%URI{scheme: scheme} when scheme in ["https", "http"] -> {:ok, object}
_ -> :error
end
end
def cast(%{"id" => object}), do: cast(object)
def cast(_) do
:error
end
def dump(data) do
{:ok, data}
end
def load(data) do
{:ok, data}
end
end

View file

@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Pipeline do
alias Pleroma.Activity
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.MRF
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.SideEffects
alias Pleroma.Web.Federator
@spec common_pipeline(map(), keyword()) :: {:ok, Activity.t(), keyword()} | {:error, any()}
def common_pipeline(object, meta) do
with {_, {:ok, validated_object, meta}} <-
{:validate_object, ObjectValidator.validate(object, meta)},
{_, {:ok, mrfd_object}} <- {:mrf_object, MRF.filter(validated_object)},
{_, {:ok, %Activity{} = activity, meta}} <-
{:persist_object, ActivityPub.persist(mrfd_object, meta)},
{_, {:ok, %Activity{} = activity, meta}} <-
{:execute_side_effects, SideEffects.handle(activity, meta)},
{_, {:ok, _}} <- {:federation, maybe_federate(activity, meta)} do
{:ok, activity, meta}
else
{:mrf_object, {:reject, _}} -> {:ok, nil, meta}
e -> {:error, e}
end
end
defp maybe_federate(activity, meta) do
with {:ok, local} <- Keyword.fetch(meta, :local) do
if local do
Federator.publish(activity)
{:ok, :federated}
else
{:ok, :not_federated}
end
else
_e -> {:error, :badarg}
end
end
end

View file

@ -0,0 +1,28 @@
defmodule Pleroma.Web.ActivityPub.SideEffects do
@moduledoc """
This module looks at an inserted object and executes the side effects that it
implies. For example, a `Like` activity will increase the like count on the
liked object, a `Follow` activity will add the user to the follower
collection, and so on.
"""
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Web.ActivityPub.Utils
def handle(object, meta \\ [])
# Tasks this handles:
# - Add like to object
# - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do
liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object)
{:ok, object, meta}
end
# Nothing to do
def handle(object, meta) do
{:ok, object, meta}
end
end

View file

@ -13,6 +13,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ObjectValidator
alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Federator
@ -202,16 +205,46 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> Map.put("conversation", context)
end
defp add_if_present(map, _key, nil), do: map
defp add_if_present(map, key, value) do
Map.put(map, key, value)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
url =
cond do
is_list(data["url"]) -> List.first(data["url"])
is_map(data["url"]) -> data["url"]
true -> nil
end
data
|> Map.put("mediaType", media_type)
|> Map.put("url", url)
media_type =
cond do
is_map(url) && is_binary(url["mediaType"]) -> url["mediaType"]
is_binary(data["mediaType"]) -> data["mediaType"]
is_binary(data["mimeType"]) -> data["mimeType"]
true -> nil
end
href =
cond do
is_map(url) && is_binary(url["href"]) -> url["href"]
is_binary(data["url"]) -> data["url"]
is_binary(data["href"]) -> data["href"]
end
attachment_url =
%{"href" => href}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", Map.get(url || %{}, "type"))
%{"url" => [attachment_url]}
|> add_if_present("mediaType", media_type)
|> add_if_present("type", data["type"])
|> add_if_present("name", data["name"])
end)
Map.put(object, "attachment", attachments)
@ -491,7 +524,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{_, {:ok, follower}} <- {:follow, User.follow(follower, followed)},
{_, {:ok, _}} <-
{:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")},
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
{:ok, _relationship} <-
FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{
to: [follower.ap_id],
actor: followed,
@ -501,7 +535,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
else
{:user_blocked, true} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
@ -512,7 +546,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:follow, {:error, _}} ->
{:ok, _} = Utils.update_follow_state_for_all(activity, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "reject")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject)
ActivityPub.reject(%{
to: [follower.ap_id],
@ -522,7 +556,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
})
{:user_locked, true} ->
{:ok, _relationship} = FollowingRelationship.update(follower, followed, "pending")
{:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending)
:noop
end
@ -542,7 +576,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept") do
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept) do
ActivityPub.accept(%{
to: follow_activity.data["to"],
type: "Accept",
@ -565,7 +599,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
{:ok, follow_activity} <- get_follow_activity(follow_object, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
%User{local: true} = follower <- User.get_cached_by_ap_id(follow_activity.data["actor"]),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, activity} <-
ActivityPub.reject(%{
to: follow_activity.data["to"],
@ -609,17 +643,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
|> handle_incoming(options)
end
def handle_incoming(
%{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data,
_options
) do
with actor <- Containment.get_actor(data),
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id),
{:ok, activity, _object} <- ActivityPub.like(actor, object, id, false) do
def handle_incoming(%{"type" => "Like"} = data, _options) do
with {_, {:ok, cast_data_sym}} <-
{:casting_data,
data |> LikeValidator.cast_data() |> Ecto.Changeset.apply_action(:insert)},
cast_data = ObjectValidator.stringify_keys(Map.from_struct(cast_data_sym)),
:ok <- ObjectValidator.fetch_actor_and_object(cast_data),
{_, {:ok, cast_data}} <- {:ensure_context_presence, ensure_context_presence(cast_data)},
{_, {:ok, cast_data}} <-
{:ensure_recipients_presence, ensure_recipients_presence(cast_data)},
{_, {:ok, activity, _meta}} <-
{:common_pipeline, Pipeline.common_pipeline(cast_data, local: false)} do
{:ok, activity}
else
_e -> :error
e -> {:error, e}
end
end
@ -1243,4 +1280,45 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
def maybe_fix_user_url(data), do: data
def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
defp ensure_context_presence(%{"context" => context} = data) when is_binary(context),
do: {:ok, data}
defp ensure_context_presence(%{"object" => object} = data) when is_binary(object) do
with %{data: %{"context" => context}} when is_binary(context) <- Object.normalize(object) do
{:ok, Map.put(data, "context", context)}
else
_ ->
{:error, :no_context}
end
end
defp ensure_context_presence(_) do
{:error, :no_context}
end
defp ensure_recipients_presence(%{"to" => [_ | _], "cc" => [_ | _]} = data),
do: {:ok, data}
defp ensure_recipients_presence(%{"object" => object} = data) do
case Object.normalize(object) do
%{data: %{"actor" => actor}} ->
data =
data
|> Map.put("to", [actor])
|> Map.put("cc", data["cc"] || [])
{:ok, data}
nil ->
{:error, :no_object}
_ ->
{:error, :no_actor}
end
end
defp ensure_recipients_presence(_) do
{:error, :no_object}
end
end

View file

@ -258,7 +258,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do
@ -277,7 +277,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
else
_ -> {:error, :not_found}
end
@ -576,9 +576,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
@doc "Sends registration invite via email"
def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params) do
with true <-
Config.get([:instance, :invites_enabled]) &&
!Config.get([:instance, :registrations_open]),
with {_, false} <- {:registrations_open, Config.get([:instance, :registrations_open])},
{_, true} <- {:invites_enabled, Config.get([:instance, :invites_enabled])},
{:ok, invite_token} <- UserInviteToken.create_invite(),
email <-
Pleroma.Emails.UserEmail.user_invitation_email(
@ -589,6 +588,18 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
),
{:ok, _} <- Pleroma.Emails.Mailer.deliver(email) do
json_response(conn, :no_content, "")
else
{:registrations_open, _} ->
errors(
conn,
{:error, "To send invites you need to set the `registrations_open` option to false."}
)
{:invites_enabled, _} ->
errors(
conn,
{:error, "To send invites you need to set the `invites_enabled` option to true."}
)
end
end
@ -801,7 +812,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
conn
|> put_view(Pleroma.Web.AdminAPI.StatusView)
|> render("index.json", %{activities: activities, as: :activity})
|> render("index.json", %{activities: activities, as: :activity, skip_relationships: false})
end
def status_update(%{assigns: %{user: admin}} = conn, %{"id" => id} = params) do

View file

@ -38,7 +38,12 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
actor: merge_account_views(user),
content: content,
created_at: created_at,
statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}),
statuses:
StatusView.render("index.json", %{
activities: statuses,
as: :activity,
skip_relationships: false
}),
state: report.data["state"],
notes: render(__MODULE__, "index_notes.json", %{notes: report.report_notes})
}

View file

@ -0,0 +1,44 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec do
alias OpenApiSpex.OpenApi
alias Pleroma.Web.Endpoint
alias Pleroma.Web.Router
@behaviour OpenApi
@impl OpenApi
def spec do
%OpenApi{
servers: [
# Populate the Server info from a phoenix endpoint
OpenApiSpex.Server.from_endpoint(Endpoint)
],
info: %OpenApiSpex.Info{
title: "Pleroma",
description: Application.spec(:pleroma, :description) |> to_string(),
version: Application.spec(:pleroma, :vsn) |> to_string()
},
# populate the paths from a phoenix router
paths: OpenApiSpex.Paths.from_router(Router),
components: %OpenApiSpex.Components{
securitySchemes: %{
"oAuth" => %OpenApiSpex.SecurityScheme{
type: "oauth2",
flows: %OpenApiSpex.OAuthFlows{
password: %OpenApiSpex.OAuthFlow{
authorizationUrl: "/oauth/authorize",
tokenUrl: "/oauth/token",
scopes: %{"read" => "read", "write" => "write", "follow" => "follow"}
}
}
}
}
}
}
# discover request/response schemas from path specs
|> OpenApiSpex.resolve_schema_modules()
end
end

View file

@ -0,0 +1,27 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Helpers do
def request_body(description, schema_ref, opts \\ []) do
media_types = ["application/json", "multipart/form-data", "application/x-www-form-urlencoded"]
content =
media_types
|> Enum.map(fn type ->
{type,
%OpenApiSpex.MediaType{
schema: schema_ref,
example: opts[:example],
examples: opts[:examples]
}}
end)
|> Enum.into(%{})
%OpenApiSpex.RequestBody{
description: description,
content: content,
required: opts[:required] || false
}
end
end

View file

@ -0,0 +1,96 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AppOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
@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 create_operation() :: Operation.t()
def create_operation do
%Operation{
tags: ["apps"],
summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create",
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true),
responses: %{
200 => Operation.response("App", "application/json", AppCreateResponse),
422 =>
Operation.response(
"Unprocessable Entity",
"application/json",
%Schema{
type: :object,
description:
"If a required parameter is missing or improperly formatted, the request will fail.",
properties: %{
error: %Schema{type: :string}
},
example: %{
"error" => "Validation failed: Redirect URI must be an absolute URI."
}
}
)
}
}
end
def verify_credentials_operation do
%Operation{
tags: ["apps"],
summary: "Verify your app works",
description: "Confirm that the app's OAuth2 credentials work.",
operationId: "AppController.verify_credentials",
security: [
%{
"oAuth" => ["read"]
}
],
responses: %{
200 =>
Operation.response("App", "application/json", %Schema{
type: :object,
description:
"If the Authorization header was provided with a valid token, you should see your app returned as an Application entity.",
properties: %{
name: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"name" => "My App",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
}),
422 =>
Operation.response(
"Unauthorized",
"application/json",
%Schema{
type: :object,
description:
"If the Authorization header contains an invalid token, is malformed, or is not present, an error will be returned indicating an authorization failure.",
properties: %{
error: %Schema{type: :string}
},
example: %{
"error" => "The access token is invalid."
}
}
)
}
}
end
end

View file

@ -0,0 +1,64 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Fetch domain blocks",
description: "View domains the user has blocked.",
security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index",
responses: %{
200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse)
}
}
end
def create_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Block a domain",
description: """
Block a domain to:
- hide all public posts from it
- hide all notifications from it
- remove all followers from it
- prevent following new users from it (but does not remove existing follows)
""",
operationId: "DomainBlockController.create",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
def delete_operation do
%Operation{
tags: ["domain_blocks"],
summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true),
security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
}
}
end
end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes. If none is provided, defaults to `read`."
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
})
end

View file

@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
})
end

View file

@ -0,0 +1,20 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "DomainBlockRequest",
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain],
example: %{
"domain" => "facebook.com"
}
})
end

View file

@ -0,0 +1,16 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "DomainBlocksResponse",
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
end

View file

@ -187,7 +187,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
end
defp preview?(draft) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"]) || false
preview? = Pleroma.Web.ControllerHelper.truthy_param?(draft.params["preview"])
%__MODULE__{draft | preview?: preview?}
end

View file

@ -12,6 +12,8 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.ActivityPub.Visibility
@ -19,6 +21,7 @@ defmodule Pleroma.Web.CommonAPI do
import Pleroma.Web.CommonAPI.Utils
require Pleroma.Constants
require Logger
def follow(follower, followed) do
timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout])
@ -42,7 +45,7 @@ defmodule Pleroma.Web.CommonAPI do
with {:ok, follower} <- User.follow(follower, followed),
%Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "accept"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_accept),
{:ok, _activity} <-
ActivityPub.accept(%{
to: [follower.ap_id],
@ -57,7 +60,7 @@ defmodule Pleroma.Web.CommonAPI do
def reject_follow_request(follower, followed) do
with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed),
{:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, "reject"),
{:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject),
{:ok, _activity} <-
ActivityPub.reject(%{
to: [follower.ap_id],
@ -109,18 +112,51 @@ defmodule Pleroma.Web.CommonAPI do
end
end
def favorite(id_or_ap_id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)},
object <- Object.normalize(activity),
like_activity <- Utils.get_existing_like(user.ap_id, object) do
if like_activity do
{:ok, like_activity, object}
else
ActivityPub.like(user, object)
end
@spec favorite(User.t(), binary()) :: {:ok, Activity.t() | :already_liked} | {:error, any()}
def favorite(%User{} = user, id) do
case favorite_helper(user, id) do
{:ok, _} = res ->
res
{:error, :not_found} = res ->
res
{:error, e} ->
Logger.error("Could not favorite #{id}. Error: #{inspect(e, pretty: true)}")
{:error, dgettext("errors", "Could not favorite")}
end
end
def favorite_helper(user, id) do
with {_, %Activity{object: object}} <- {:find_object, Activity.get_by_id_with_object(id)},
{_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity}
else
{:find_activity, _} -> {:error, :not_found}
_ -> {:error, dgettext("errors", "Could not favorite")}
{:find_object, _} ->
{:error, :not_found}
{:common_pipeline,
{
:error,
{
:validate_object,
{
:error,
changeset
}
}
}} = e ->
if {:object, {"already liked by this actor", []}} in changeset.errors do
{:ok, :already_liked}
else
{:error, e}
end
e ->
{:error, e}
end
end

View file

@ -5,10 +5,18 @@
defmodule Pleroma.Web.ControllerHelper do
use Pleroma.Web, :controller
# As in MastoAPI, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
alias Pleroma.Config
# As in Mastodon API, per https://api.rubyonrails.org/classes/ActiveModel/Type/Boolean.html
@falsy_param_values [false, 0, "0", "f", "F", "false", "False", "FALSE", "off", "OFF"]
def truthy_param?(blank_value) when blank_value in [nil, ""], do: nil
def truthy_param?(value), do: value not in @falsy_param_values
def explicitly_falsy_param?(value), do: value in @falsy_param_values
# Note: `nil` and `""` are considered falsy values in Pleroma
def falsy_param?(value),
do: explicitly_falsy_param?(value) or value in [nil, ""]
def truthy_param?(value), do: not falsy_param?(value)
def json_response(conn, status, json) do
conn
@ -96,4 +104,14 @@ defmodule Pleroma.Web.ControllerHelper do
def put_if_exist(map, _key, nil), do: map
def put_if_exist(map, key, value), do: Map.put(map, key, value)
@doc "Whether to skip rendering `[:account][:pleroma][:relationship]`for statuses/notifications"
def skip_relationships?(params) do
if Config.get([:extensions, :output_relationships_in_statuses_by_default]) do
false
else
# BREAKING: older PleromaFE versions do not send this param but _do_ expect relationships.
not truthy_param?(params["with_relationships"])
end
end
end

View file

@ -6,7 +6,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
only: [
add_link_headers: 2,
truthy_param?: 1,
assign_account_by_id: 2,
json_response: 3,
skip_relationships?: 1
]
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
@ -240,7 +246,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: reading_user, as: :activity)
|> render("index.json",
activities: activities,
for: reading_user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else
_e -> render_error(conn, :not_found, "Can't find user")
end

View file

@ -14,17 +14,20 @@ defmodule Pleroma.Web.MastodonAPI.AppController do
action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
plug(OAuthScopesPlug, %{scopes: ["read"]} when action == :verify_credentials)
plug(OpenApiSpex.Plug.CastAndValidate)
@local_mastodon_name "Mastodon-Local"
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.AppOperation
@doc "POST /api/v1/apps"
def create(conn, params) do
def create(%{body_params: params} = conn, _params) do
scopes = Scopes.fetch_scopes(params, ["read"])
app_attrs =
params
|> Map.drop(["scope", "scopes"])
|> Map.put("scopes", scopes)
|> Map.take([:client_name, :redirect_uris, :website])
|> Map.put(:scopes, scopes)
with cs <- App.register_changeset(%App{}, app_attrs),
false <- cs.changes[:client_name] == @local_mastodon_name,

View file

@ -8,6 +8,9 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
plug(OpenApiSpex.Plug.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.DomainBlockOperation
plug(
OAuthScopesPlug,
%{scopes: ["follow", "read:blocks"]} when action == :index
@ -26,13 +29,13 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockController do
end
@doc "POST /api/v1/domain_blocks"
def create(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
def create(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.block_domain(blocker, domain)
json(conn, %{})
end
@doc "DELETE /api/v1/domain_blocks"
def delete(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
def delete(%{assigns: %{user: blocker}, body_params: %{domain: domain}} = conn, _params) do
User.unblock_domain(blocker, domain)
json(conn, %{})
end

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.NotificationController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug
@ -45,7 +45,11 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
conn
|> add_link_headers(notifications)
|> render("index.json", notifications: notifications, for: user)
|> render("index.json",
notifications: notifications,
for: user,
skip_relationships: skip_relationships?(params)
)
end
# GET /api/v1/notifications/:id
@ -66,7 +70,8 @@ defmodule Pleroma.Web.MastodonAPI.NotificationController do
json(conn, %{})
end
# POST /api/v1/notifications/dismiss
# POST /api/v1/notifications/:id/dismiss
# POST /api/v1/notifications/dismiss (deprecated)
def dismiss(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
with {:ok, _notif} <- Notification.dismiss(user, id) do
json(conn, %{})

View file

@ -5,13 +5,14 @@
defmodule Pleroma.Web.MastodonAPI.SearchController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [fetch_integer_param: 2, skip_relationships?: 1]
alias Pleroma.Activity
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ControllerHelper
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView
@ -66,10 +67,11 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp search_options(params, user) do
[
skip_relationships: skip_relationships?(params),
resolve: params["resolve"] == "true",
following: params["following"] == "true",
limit: ControllerHelper.fetch_integer_param(params, "limit"),
offset: ControllerHelper.fetch_integer_param(params, "offset"),
limit: fetch_integer_param(params, "limit"),
offset: fetch_integer_param(params, "offset"),
type: params["type"],
author: get_author(params),
for_user: user
@ -79,12 +81,24 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do
defp resource_search(_, "accounts", query, options) do
accounts = with_fallback(fn -> User.search(query, options) end)
AccountView.render("index.json", users: accounts, for: options[:for_user], as: :user)
AccountView.render("index.json",
users: accounts,
for: options[:for_user],
as: :user,
skip_relationships: false
)
end
defp resource_search(_, "statuses", query, options) do
statuses = with_fallback(fn -> Activity.search(options[:for_user], query, options) end)
StatusView.render("index.json", activities: statuses, for: options[:for_user], as: :activity)
StatusView.render("index.json",
activities: statuses,
for: options[:for_user],
as: :activity,
skip_relationships: options[:skip_relationships]
)
end
defp resource_search(:v2, "hashtags", query, _options) do

View file

@ -5,7 +5,8 @@
defmodule Pleroma.Web.MastodonAPI.StatusController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [try_render: 3, add_link_headers: 2]
import Pleroma.Web.ControllerHelper,
only: [try_render: 3, add_link_headers: 2, skip_relationships?: 1]
require Ecto.Query
@ -101,7 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
`ids` query param is required
"""
def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
def index(%{assigns: %{user: user}} = conn, %{"ids" => ids} = params) do
limit = 100
activities =
@ -110,7 +111,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
|> Activity.all_by_ids_with_object()
|> Enum.filter(&Visibility.visible_for_user?(&1, user))
render(conn, "index.json", activities: activities, for: user, as: :activity)
render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
@doc """
@ -207,9 +213,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
end
@doc "POST /api/v1/statuses/:id/favourite"
def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
def favourite(%{assigns: %{user: user}} = conn, %{"id" => activity_id}) do
with {:ok, _fav} <- CommonAPI.favorite(user, activity_id),
%Activity{} = activity <- Activity.get_by_id(activity_id) do
try_render(conn, "show.json", activity: activity, for: user, as: :activity)
end
end
@ -360,7 +366,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
@doc "GET /api/v1/bookmarks"
@ -378,6 +389,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
conn
|> add_link_headers(bookmarks)
|> render("index.json", %{activities: activities, for: user, as: :activity})
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1]
only: [add_link_headers: 2, add_link_headers: 3, truthy_param?: 1, skip_relationships?: 1]
alias Pleroma.Pagination
alias Pleroma.Plugs.OAuthScopesPlug
@ -14,9 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
# TODO: Replace with a macro when there is a Phoenix release with
# TODO: Replace with a macro when there is a Phoenix release with the following commit in it:
# https://github.com/phoenixframework/phoenix/commit/2e8c63c01fec4dde5467dbbbf9705ff9e780735e
# in it
plug(RateLimiter, [name: :timeline, bucket_name: :direct_timeline] when action == :direct)
plug(RateLimiter, [name: :timeline, bucket_name: :public_timeline] when action == :public)
@ -49,7 +48,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
# GET /api/v1/timelines/direct
@ -68,7 +72,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities)
|> render("index.json", activities: activities, for: user, as: :activity)
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
# GET /api/v1/timelines/public
@ -95,7 +104,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else
render_error(conn, :unauthorized, "authorization required for timeline view")
end
@ -140,7 +154,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
conn
|> add_link_headers(activities, %{"local" => local_only})
|> render("index.json", activities: activities, for: user, as: :activity)
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
# GET /api/v1/timelines/list/:list_id
@ -164,7 +183,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> ActivityPub.fetch_activities_bounded(following, params)
|> Enum.reverse()
render(conn, "index.json", activities: activities, for: user, as: :activity)
render(conn, "index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else
_e -> render_error(conn, :forbidden, "Error.")
end

View file

@ -15,6 +15,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
def render("index.json", %{users: users} = opts) do
reading_user = opts[:for]
# Note: :skip_relationships option is currently intentionally not supported for accounts
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
@ -73,7 +74,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
followed_by =
if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: "accept"} -> true
%{state: :follow_accept} -> true
_ -> false
end
else
@ -83,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{
id: to_string(target.id),
following: follow_state == "accept",
following: follow_state == :follow_accept,
followed_by: followed_by,
blocking:
UserRelationship.exists?(
@ -125,7 +126,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
reading_user,
&User.subscribed_to?(&2, &1)
),
requested: follow_state == "pending",
requested: follow_state == :follow_pending,
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
not UserRelationship.exists?(
@ -192,11 +193,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
end)
relationship =
render("relationship.json", %{
user: opts[:for],
target: user,
relationships: opts[:relationships]
})
if opts[:skip_relationships] do
%{}
else
render("relationship.json", %{
user: opts[:for],
target: user,
relationships: opts[:relationships]
})
end
%{
id: to_string(user.id),

View file

@ -51,14 +51,15 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
|> Enum.filter(& &1)
|> Kernel.++(move_activities_targets)
UserRelationship.view_relationships_option(reading_user, actors)
UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
)
end
opts = %{
for: reading_user,
parent_activities: parent_activities,
relationships: relationships_opt
}
opts =
opts
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
safe_render_many(notifications, NotificationView, "show.json", opts)
end
@ -82,12 +83,16 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
mastodon_type = Activity.mastodon_notification_type(activity)
render_opts = %{
relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}
with %{id: _} = account <-
AccountView.render("show.json", %{
user: actor,
for: reading_user,
relationships: opts[:relationships]
}) do
AccountView.render(
"show.json",
Map.merge(render_opts, %{user: actor, for: reading_user})
) do
response = %{
id: to_string(notification.id),
type: mastodon_type,
@ -98,8 +103,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
}
}
render_opts = %{relationships: opts[:relationships]}
case mastodon_type do
"mention" ->
put_status(response, activity, reading_user, render_opts)
@ -111,6 +114,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
put_status(response, parent_activity_fn.(), reading_user, render_opts)
"move" ->
# Note: :skip_relationships option being applied to _account_ rendering (here)
put_target(response, activity, reading_user, render_opts)
"follow" ->

View file

@ -99,7 +99,9 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
UserRelationship.view_relationships_option(reading_user, actors)
UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships]
)
end
opts =
@ -153,7 +155,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}),
in_reply_to_id: nil,
in_reply_to_account_id: nil,
@ -301,6 +304,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
_ -> []
end
# Status muted state (would do 1 request per status unless user mutes are preloaded)
muted =
thread_muted? ||
UserRelationship.exists?(
@ -319,7 +323,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
relationships: opts[:relationships],
skip_relationships: opts[:skip_relationships]
}),
in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),

View file

@ -75,7 +75,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do
end,
if Config.get([:instance, :safe_dm_mentions]) do
"safe_dm_mentions"
end
end,
"pleroma_emoji_reactions"
]
|> Enum.filter(& &1)

View file

@ -15,7 +15,12 @@ defmodule Pleroma.Web.OAuth.Scopes do
Note: `scopes` is used by Mastodon supporting it but sticking to
OAuth's standard `scope` wherever we control it
"""
@spec fetch_scopes(map(), list()) :: list()
@spec fetch_scopes(map() | struct(), list()) :: list()
def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
parse_scopes(scopes, default)
end
def fetch_scopes(params, default) do
parse_scopes(params["scope"] || params["scopes"], default)
end

View file

@ -6,7 +6,7 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2]
only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2, skip_relationships?: 1]
alias Ecto.Changeset
alias Pleroma.Plugs.OAuthScopesPlug
@ -139,7 +139,12 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: for_user, as: :activity)
|> render("index.json",
activities: activities,
for: for_user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
end
@doc "POST /api/v1/pleroma/accounts/:id/subscribe"

View file

@ -5,7 +5,7 @@
defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2, skip_relationships?: 1]
alias Pleroma.Activity
alias Pleroma.Conversation.Participation
@ -110,12 +110,11 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
def conversation_statuses(
%{assigns: %{user: user}} = conn,
%{assigns: %{user: %{id: user_id} = user}} = conn,
%{"id" => participation_id} = params
) do
with %Participation{} = participation <-
Participation.get(participation_id, preload: [:conversation]),
true <- user.id == participation.user_id do
with %Participation{user_id: ^user_id} = participation <-
Participation.get(participation_id, preload: [:conversation]) do
params =
params
|> Map.put("blocking_user", user)
@ -124,13 +123,19 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context(params)
|> ActivityPub.fetch_activities_for_context_query(params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
|> Enum.reverse()
conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", %{activities: activities, for: user, as: :activity})
|> render("index.json",
activities: activities,
for: user,
as: :activity,
skip_relationships: skip_relationships?(params)
)
else
_error ->
conn
@ -184,13 +189,17 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
end
end
def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
def read_notification(%{assigns: %{user: user}} = conn, %{"max_id" => max_id} = params) do
with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80)
conn
|> put_view(NotificationView)
|> render("index.json", %{notifications: notifications, for: user})
|> render("index.json",
notifications: notifications,
for: user,
skip_relationships: skip_relationships?(params)
)
end
end
end

View file

@ -64,5 +64,8 @@ defmodule Pleroma.Web.RichMedia.Helpers do
def fetch_data_for_activity(_), do: %{}
def perform(:fetch, %Activity{} = activity), do: fetch_data_for_activity(activity)
def perform(:fetch, %Activity{} = activity) do
fetch_data_for_activity(activity)
:ok
end
end

View file

@ -29,6 +29,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureUserKeyPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :authenticated_api do
@ -45,6 +46,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.SetUserSessionIdPlug)
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :admin_api do
@ -62,6 +64,7 @@ defmodule Pleroma.Web.Router do
plug(Pleroma.Plugs.EnsureAuthenticatedPlug)
plug(Pleroma.Plugs.UserIsAdminPlug)
plug(Pleroma.Plugs.IdempotencyPlug)
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mastodon_html do
@ -95,10 +98,12 @@ defmodule Pleroma.Web.Router do
pipeline :config do
plug(:accepts, ["json", "xml"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :pleroma_api do
plug(:accepts, ["html", "json"])
plug(OpenApiSpex.Plug.PutApiSpec, module: Pleroma.Web.ApiSpec)
end
pipeline :mailbox_preview do
@ -348,9 +353,11 @@ defmodule Pleroma.Web.Router do
get("/notifications", NotificationController, :index)
get("/notifications/:id", NotificationController, :show)
post("/notifications/:id/dismiss", NotificationController, :dismiss)
post("/notifications/clear", NotificationController, :clear)
post("/notifications/dismiss", NotificationController, :dismiss)
delete("/notifications/destroy_multiple", NotificationController, :destroy_multiple)
# Deprecated: was removed in Mastodon v3, use `/notifications/:id/dismiss` instead
post("/notifications/dismiss", NotificationController, :dismiss)
get("/scheduled_statuses", ScheduledActivityController, :index)
get("/scheduled_statuses/:id", ScheduledActivityController, :show)
@ -501,6 +508,12 @@ defmodule Pleroma.Web.Router do
)
end
scope "/api" do
pipe_through(:api)
get("/openapi", OpenApiSpex.Plug.RenderSpec, [])
end
scope "/api", Pleroma.Web, as: :authenticated_twitter_api do
pipe_through(:authenticated_api)