diff options
Diffstat (limited to 'lib/pleroma/web')
87 files changed, 2545 insertions, 1132 deletions
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index aa0229db7..31397b09f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -4,9 +4,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Conversation alias Pleroma.Notification alias Pleroma.Object + alias Pleroma.Object.Containment alias Pleroma.Object.Fetcher alias Pleroma.Pagination alias Pleroma.Repo @@ -25,19 +27,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. defp get_recipients(%{"type" => "Announce"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) actor = User.get_cached_by_ap_id(data["actor"]) recipients = - (to ++ cc) - |> Enum.filter(fn recipient -> + Enum.filter(Enum.concat([to, cc, bcc]), fn recipient -> case User.get_cached_by_ap_id(recipient) do - nil -> - true - - user -> - User.following?(user, actor) + nil -> true + user -> User.following?(user, actor) end end) @@ -45,17 +44,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp get_recipients(%{"type" => "Create"} = data) do - to = data["to"] || [] - cc = data["cc"] || [] - actor = data["actor"] || [] - recipients = (to ++ cc ++ [actor]) |> Enum.uniq() + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + actor = Map.get(data, "actor", []) + recipients = [to, cc, bcc, [actor]] |> Enum.concat() |> Enum.uniq() {recipients, to, cc} end defp get_recipients(data) do - to = data["to"] || [] - cc = data["cc"] || [] - recipients = to ++ cc + to = Map.get(data, "to", []) + cc = Map.get(data, "cc", []) + bcc = Map.get(data, "bcc", []) + recipients = Enum.concat([to, cc, bcc]) {recipients, to, cc} end @@ -73,7 +74,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end defp check_remote_limit(%{"object" => %{"content" => content}}) when not is_nil(content) do - limit = Pleroma.Config.get([:instance, :remote_limit]) + limit = Config.get([:instance, :remote_limit]) String.length(content) <= limit end @@ -108,6 +109,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def decrease_replies_count_if_reply(_object), do: :noop + def increase_poll_votes_if_vote(%{ + "object" => %{"inReplyTo" => reply_ap_id, "name" => name}, + "type" => "Create" + }) do + Object.increase_vote_count(reply_ap_id, name) + end + + def increase_poll_votes_if_vote(_create_data), do: :noop + def insert(map, local \\ true, fake \\ false) when is_map(map) do with nil <- Activity.normalize(map), map <- lazy_put_activity_defaults(map, fake), @@ -116,6 +126,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, map} <- MRF.filter(map), {recipients, _, _} = get_recipients(map), {:fake, false, map, recipients} <- {:fake, fake, map, recipients}, + :ok <- Containment.contain_child(map), {:ok, map, object} <- insert_full_object(map) do {:ok, activity} = Repo.insert(%Activity{ @@ -179,44 +190,62 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end) end + def stream_out_participations(%Object{data: %{"context" => context}}, user) do + with %Conversation{} = conversation <- Conversation.get_for_ap_id(context), + conversation = Repo.preload(conversation, :participations), + last_activity_id = + fetch_latest_activity_id_for_context(conversation.ap_id, %{ + "user" => user, + "blocking_user" => user + }) do + if last_activity_id do + stream_out_participations(conversation.participations) + end + end + end + + def stream_out_participations(_, _), do: :noop + def stream_out(activity) do public = "https://www.w3.org/ns/activitystreams#Public" if activity.data["type"] in ["Create", "Announce", "Delete"] do - Pleroma.Web.Streamer.stream("user", activity) - Pleroma.Web.Streamer.stream("list", activity) - - if Enum.member?(activity.data["to"], public) do - Pleroma.Web.Streamer.stream("public", activity) + object = Object.normalize(activity) + # Do not stream out poll replies + unless object.data["type"] == "Answer" do + Pleroma.Web.Streamer.stream("user", activity) + Pleroma.Web.Streamer.stream("list", activity) - if activity.local do - Pleroma.Web.Streamer.stream("public:local", activity) - end + if Enum.member?(activity.data["to"], public) do + Pleroma.Web.Streamer.stream("public", activity) - if activity.data["type"] in ["Create"] do - object = Object.normalize(activity) + if activity.local do + Pleroma.Web.Streamer.stream("public:local", activity) + end - object.data - |> Map.get("tag", []) - |> Enum.filter(fn tag -> is_bitstring(tag) end) - |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) + if activity.data["type"] in ["Create"] do + object.data + |> Map.get("tag", []) + |> Enum.filter(fn tag -> is_bitstring(tag) end) + |> Enum.each(fn tag -> Pleroma.Web.Streamer.stream("hashtag:" <> tag, activity) end) - if object.data["attachment"] != [] do - Pleroma.Web.Streamer.stream("public:media", activity) + if object.data["attachment"] != [] do + Pleroma.Web.Streamer.stream("public:media", activity) - if activity.local do - Pleroma.Web.Streamer.stream("public:local:media", activity) + if activity.local do + Pleroma.Web.Streamer.stream("public:local:media", activity) + end end end + else + # TODO: Write test, replace with visibility test + if !Enum.member?(activity.data["cc"] || [], public) && + !Enum.member?( + activity.data["to"], + User.get_cached_by_ap_id(activity.data["actor"]).follower_address + ), + do: Pleroma.Web.Streamer.stream("direct", activity) end - else - # TODO: Write test, replace with visibility test - if !Enum.member?(activity.data["cc"] || [], public) && - !Enum.member?( - activity.data["to"], - User.get_cached_by_ap_id(activity.data["actor"]).follower_address - ), - do: Pleroma.Web.Streamer.stream("direct", activity) end end end @@ -235,6 +264,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do {:ok, activity} <- insert(create_data, local, fake), {:fake, false, activity} <- {:fake, fake, activity}, _ <- increase_replies_count_if_reply(create_data), + _ <- increase_poll_votes_if_vote(create_data), # Changing note count prior to enqueuing federation task in order to avoid # race conditions on updating user.info {:ok, _actor} <- increase_note_count_if_public(actor, activity), @@ -376,6 +406,19 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end + def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do + with data <- %{ + "to" => [follower_address], + "type" => "Delete", + "actor" => ap_id, + "object" => %{"type" => "Person", "id" => ap_id} + }, + {:ok, activity} <- insert(data, true, true), + :ok <- maybe_federate(activity) do + {:ok, user} + end + end + def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) @@ -388,7 +431,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do "to" => to, "deleted_activity_id" => activity && activity.id }, - {:ok, activity} <- insert(data, local), + {:ok, activity} <- insert(data, local, false), + stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), # Changing note count prior to enqueuing federation task in order to avoid # race conditions on updating user.info @@ -399,16 +443,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end def block(blocker, blocked, activity_id \\ nil, local \\ true) do - ap_config = Application.get_env(:pleroma, :activitypub) - unfollow_blocked = Keyword.get(ap_config, :unfollow_blocked) - outgoing_blocks = Keyword.get(ap_config, :outgoing_blocks) + outgoing_blocks = Config.get([:activitypub, :outgoing_blocks]) + unfollow_blocked = Config.get([:activitypub, :unfollow_blocked]) - with true <- unfollow_blocked do + if unfollow_blocked do follow_activity = fetch_latest_follow(blocker, blocked) - - if follow_activity do - unfollow(blocker, blocked, nil, local) - end + if follow_activity, do: unfollow(blocker, blocked, nil, local) end with true <- outgoing_blocks, @@ -480,6 +520,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public from(activity in Activity) + |> maybe_preload_objects(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts["user"]) |> where( @@ -492,6 +533,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do ^context ) ) + |> exclude_poll_votes(opts) |> order_by([activity], desc: activity.id) end @@ -499,7 +541,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def fetch_activities_for_context(context, opts \\ %{}) do context |> fetch_activities_for_context_query(opts) - |> Activity.with_preloaded_object() |> Repo.all() end @@ -507,7 +548,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do Pleroma.FlakeId.t() | nil def fetch_latest_activity_id_for_context(context, opts \\ %{}) do context - |> fetch_activities_for_context_query(opts) + |> fetch_activities_for_context_query(Map.merge(%{"skip_preload" => true}, opts)) |> limit(1) |> select([a], a.id) |> Repo.one() @@ -548,14 +589,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_visibility(query, %{visibility: visibility}) when visibility in @valid_visibilities do - query = - from( - a in query, - where: - fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) - ) - - query + from( + a in query, + where: + fragment("activity_visibility(?, ?, ?) = ?", a.actor, a.recipients, a.data, ^visibility) + ) end defp restrict_visibility(_query, %{visibility: visibility}) @@ -565,17 +603,24 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_visibility(query, _visibility), do: query - defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}) do - query = - from( - a in query, - where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) - ) + defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _), + do: query - query + defp restrict_thread_visibility( + query, + %{"user" => %User{info: %{skip_thread_containment: true}}}, + _ + ), + do: query + + defp restrict_thread_visibility(query, %{"user" => %User{ap_id: ap_id}}, _) do + from( + a in query, + where: fragment("thread_visibility(?, (?)->>'id') = true", ^ap_id, a.data) + ) end - defp restrict_thread_visibility(query, _), do: query + defp restrict_thread_visibility(query, _, _), do: query def fetch_user_activities(user, reading_user, params \\ %{}) do params = @@ -653,20 +698,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_tag(query, _), do: query - defp restrict_to_cc(query, recipients_to, recipients_cc) do - from( - activity in query, - where: - fragment( - "(?->'to' \\?| ?) or (?->'cc' \\?| ?)", - activity.data, - ^recipients_to, - activity.data, - ^recipients_cc - ) - ) - end - defp restrict_recipients(query, [], _user), do: query defp restrict_recipients(query, recipients, nil) do @@ -820,6 +851,18 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_muted_reblogs(query, _), do: query + defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query + + defp exclude_poll_votes(query, _) do + if has_named_binding?(query, :object) do + from([activity, object: o] in query, + where: fragment("not(?->>'type' = ?)", o.data, "Answer") + ) + else + query + end + end + defp maybe_preload_objects(query, %{"skip_preload" => true}), do: query defp maybe_preload_objects(query, _) do @@ -854,9 +897,11 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp maybe_order(query, _), do: query def fetch_activities_query(recipients, opts \\ %{}) do - base_query = from(activity in Activity) + config = %{ + skip_thread_containment: Config.get([:instance, :skip_thread_containment]) + } - base_query + Activity |> maybe_preload_objects(opts) |> maybe_preload_bookmarks(opts) |> maybe_set_thread_muted_field(opts) @@ -875,23 +920,53 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_muted(opts) |> restrict_media(opts) |> restrict_visibility(opts) - |> restrict_thread_visibility(opts) + |> restrict_thread_visibility(opts, config) |> restrict_replies(opts) |> restrict_reblogs(opts) |> restrict_pinned(opts) |> restrict_muted_reblogs(opts) |> Activity.restrict_deactivated_users() + |> exclude_poll_votes(opts) end def fetch_activities(recipients, opts \\ %{}) do - fetch_activities_query(recipients, opts) + list_memberships = Pleroma.List.memberships(opts["user"]) + + fetch_activities_query(recipients ++ list_memberships, opts) |> Pagination.fetch_paginated(opts) |> Enum.reverse() + |> maybe_update_cc(list_memberships, opts["user"]) + end + + defp maybe_update_cc(activities, list_memberships, %User{ap_id: user_ap_id}) + when is_list(list_memberships) and length(list_memberships) > 0 do + Enum.map(activities, fn + %{data: %{"bcc" => bcc}} = activity when is_list(bcc) and length(bcc) > 0 -> + if Enum.any?(bcc, &(&1 in list_memberships)) do + update_in(activity.data["cc"], &[user_ap_id | &1]) + else + activity + end + + activity -> + activity + end) + end + + defp maybe_update_cc(activities, _, _), do: activities + + def fetch_activities_bounded_query(query, recipients, recipients_with_public) do + from(activity in query, + where: + fragment("? && ?", activity.recipients, ^recipients) or + (fragment("? && ?", activity.recipients, ^recipients_with_public) and + "https://www.w3.org/ns/activitystreams#Public" in activity.recipients) + ) end - def fetch_activities_bounded(recipients_to, recipients_cc, opts \\ %{}) do + def fetch_activities_bounded(recipients, recipients_with_public, opts \\ %{}) do fetch_activities_query([], opts) - |> restrict_to_cc(recipients_to, recipients_cc) + |> fetch_activities_bounded_query(recipients, recipients_with_public) |> Pagination.fetch_paginated(opts) |> Enum.reverse() end @@ -938,6 +1013,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do avatar: avatar, name: data["name"], follower_address: data["followers"], + following_address: data["following"], bio: data["summary"] } diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex index ad2ca1e54..e2af4ad1a 100644 --- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex +++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex @@ -27,13 +27,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do plug(:relay_active? when action in [:relay]) def relay_active?(conn, _) do - if Keyword.get(Application.get_env(:pleroma, :instance), :allow_relay) do + if Pleroma.Config.get([:instance, :allow_relay]) do conn else conn - |> put_status(404) - |> json(%{error: "not found"}) - |> halt + |> render_error(:not_found, "not found") + |> halt() end end @@ -104,43 +103,57 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do end end - def following(conn, %{"nickname" => nickname, "page" => page}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_follows, true} <- + {:show_follows, (for_user && for_user == user) || !user.info.hide_follows} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user, page: page})) + |> json(UserView.render("following.json", %{user: user, page: page, for: for_user})) + else + {:show_follows, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def following(conn, %{"nickname" => nickname}) do + def following(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("following.json", %{user: user})) + |> json(UserView.render("following.json", %{user: user, for: for_user})) end end - def followers(conn, %{"nickname" => nickname, "page" => page}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname, "page" => page}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user), + {:show_followers, true} <- + {:show_followers, (for_user && for_user == user) || !user.info.hide_followers} do {page, _} = Integer.parse(page) conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user, page: page})) + |> json(UserView.render("followers.json", %{user: user, page: page, for: for_user})) + else + {:show_followers, _} -> + conn + |> put_resp_header("content-type", "application/activity+json") + |> send_resp(403, "") end end - def followers(conn, %{"nickname" => nickname}) do + def followers(%{assigns: %{user: for_user}} = conn, %{"nickname" => nickname}) do with %User{} = user <- User.get_cached_by_nickname(nickname), - {:ok, user} <- User.ensure_keys_present(user) do + {user, for_user} <- ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do conn |> put_resp_header("content-type", "application/activity+json") - |> json(UserView.render("followers.json", %{user: user})) + |> json(UserView.render("followers.json", %{user: user, for: for_user})) end end @@ -190,7 +203,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do Logger.info(inspect(conn.req_headers)) end - json(conn, "error") + json(conn, dgettext("errors", "error")) end def relay(conn, _params) do @@ -218,9 +231,15 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> put_resp_header("content-type", "application/activity+json") |> json(UserView.render("inbox.json", %{user: user, max_id: params["max_id"]})) else + err = + dgettext("errors", "can't read inbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + conn |> put_status(:forbidden) - |> json("can't read inbox of #{nickname} as #{user.nickname}") + |> json(err) end end @@ -246,7 +265,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:ok, delete} <- ActivityPub.delete(object) do {:ok, delete} else - _ -> {:error, "Can't delete object"} + _ -> {:error, dgettext("errors", "Can't delete object")} end end @@ -255,12 +274,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do {:ok, activity, _object} <- ActivityPub.like(user, object) do {:ok, activity} else - _ -> {:error, "Can't like object"} + _ -> {:error, dgettext("errors", "Can't like object")} end end def handle_user_activity(_, _) do - {:error, "Unhandled activity type"} + {:error, dgettext("errors", "Unhandled activity type")} end def update_outbox( @@ -288,22 +307,28 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do |> json(message) end else + err = + dgettext("errors", "can't update outbox of %{nickname} as %{as_nickname}", + nickname: nickname, + as_nickname: user.nickname + ) + conn |> put_status(:forbidden) - |> json("can't update outbox of #{nickname} as #{user.nickname}") + |> json(err) end end def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, _e) do conn - |> put_status(500) - |> json("error") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "error")) end defp set_requester_reachable(%Plug.Conn{} = conn, _) do @@ -314,4 +339,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do conn end + + defp ensure_user_keys_present_and_maybe_refresh_for_user(user, for_user) do + {:ok, new_user} = User.ensure_keys_present(user) + + for_user = + if new_user != user and match?(%User{}, for_user) do + User.get_cached_by_nickname(for_user.nickname) + else + for_user + end + + {new_user, for_user} + end end diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index 1aaa20050..10ceef715 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -5,8 +5,8 @@ defmodule Pleroma.Web.ActivityPub.MRF do @callback filter(Map.t()) :: {:ok | :reject, Map.t()} - def filter(object) do - get_policies() + def filter(policies, %{} = object) do + policies |> Enum.reduce({:ok, object}, fn policy, {:ok, object} -> policy.filter(object) @@ -16,10 +16,10 @@ defmodule Pleroma.Web.ActivityPub.MRF do end) end + def filter(%{} = object), do: get_policies() |> filter(object) + def get_policies do - Application.get_env(:pleroma, :instance, []) - |> Keyword.get(:rewrite_policy, []) - |> get_policies() + Pleroma.Config.get([:instance, :rewrite_policy], []) |> get_policies() end defp get_policies(policy) when is_atom(policy), do: [policy] diff --git a/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex new file mode 100644 index 000000000..2da3eac2f --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/anti_link_spam_policy.ex @@ -0,0 +1,48 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.AntiLinkSpamPolicy do + alias Pleroma.User + + require Logger + + # has the user successfully posted before? + defp old_user?(%User{} = u) do + u.info.note_count > 0 || u.info.follower_count > 0 + end + + # does the post contain links? + defp contains_links?(%{"content" => content} = _object) do + content + |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"],a.zrl") + |> Floki.attribute("a", "href") + |> length() > 0 + end + + defp contains_links?(_), do: false + + def filter(%{"type" => "Create", "actor" => actor, "object" => object} = message) do + with {:ok, %User{} = u} <- User.get_or_fetch_by_ap_id(actor), + {:contains_links, true} <- {:contains_links, contains_links?(object)}, + {:old_user, true} <- {:old_user, old_user?(u)} do + {:ok, message} + else + {:contains_links, false} -> + {:ok, message} + + {:old_user, false} -> + {:reject, nil} + + {:error, _} -> + {:reject, nil} + + e -> + Logger.warn("[MRF anti-link-spam] WTF: unhandled error #{inspect(e)}") + {:reject, nil} + end + end + + # in all other cases, pass through + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex index 15d8514be..2d03df68a 100644 --- a/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex +++ b/lib/pleroma/web/activity_pub/mrf/ensure_re_prepended.ex @@ -9,8 +9,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do @behaviour Pleroma.Web.ActivityPub.MRF @reply_prefix Regex.compile!("^re:[[:space:]]*", [:caseless]) + def filter_by_summary( - %{"summary" => parent_summary} = _parent, + %{data: %{"summary" => parent_summary}} = _in_reply_to, %{"summary" => child_summary} = child ) when not is_nil(child_summary) and byte_size(child_summary) > 0 and @@ -24,17 +25,13 @@ defmodule Pleroma.Web.ActivityPub.MRF.EnsureRePrepended do end end - def filter_by_summary(_parent, child), do: child - - def filter(%{"type" => activity_type} = object) when activity_type == "Create" do - child = object["object"] - in_reply_to = Object.normalize(child["inReplyTo"]) + def filter_by_summary(_in_reply_to, child), do: child + def filter(%{"type" => "Create", "object" => child_object} = object) do child = - if(in_reply_to, - do: filter_by_summary(in_reply_to.data, child), - else: child - ) + child_object["inReplyTo"] + |> Object.normalize(child_object["inReplyTo"]) + |> filter_by_summary(child_object) object = Map.put(object, "object", child) diff --git a/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex new file mode 100644 index 000000000..01d21a299 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/mediaproxy_warming_policy.ex @@ -0,0 +1,56 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy do + @moduledoc "Preloads any attachments in the MediaProxy cache by prefetching them" + @behaviour Pleroma.Web.ActivityPub.MRF + + alias Pleroma.HTTP + alias Pleroma.Web.MediaProxy + + require Logger + + @hackney_options [ + pool: :media, + recv_timeout: 10_000 + ] + + def perform(:prefetch, url) do + Logger.info("Prefetching #{inspect(url)}") + + url + |> MediaProxy.url() + |> HTTP.get([], adapter: @hackney_options) + end + + def perform(:preload, %{"object" => %{"attachment" => attachments}} = _message) do + Enum.each(attachments, fn + %{"url" => url} when is_list(url) -> + url + |> Enum.each(fn + %{"href" => href} -> + PleromaJobQueue.enqueue(:background, __MODULE__, [:prefetch, href]) + + x -> + Logger.debug("Unhandled attachment URL object #{inspect(x)}") + end) + + x -> + Logger.debug("Unhandled attachment #{inspect(x)}") + end) + end + + @impl true + def filter( + %{"type" => "Create", "object" => %{"attachment" => attachments} = _object} = message + ) + when is_list(attachments) and length(attachments) > 0 do + PleromaJobQueue.enqueue(:background, __MODULE__, [:preload, message]) + + {:ok, message} + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex index f30fee0d5..86a48bda5 100644 --- a/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/no_placeholder_text_policy.ex @@ -10,19 +10,11 @@ defmodule Pleroma.Web.ActivityPub.MRF.NoPlaceholderTextPolicy do def filter( %{ "type" => "Create", - "object" => %{"content" => content, "attachment" => _attachment} = child_object + "object" => %{"content" => content, "attachment" => _} = _child_object } = object ) when content in [".", "<p>.</p>"] do - child_object = - child_object - |> Map.put("content", "") - - object = - object - |> Map.put("object", child_object) - - {:ok, object} + {:ok, put_in(object, ["object", "content"], "")} end @impl true diff --git a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex index 9c87c6963..c269d0f89 100644 --- a/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex +++ b/lib/pleroma/web/activity_pub/mrf/normalize_markup.ex @@ -8,18 +8,14 @@ defmodule Pleroma.Web.ActivityPub.MRF.NormalizeMarkup do @behaviour Pleroma.Web.ActivityPub.MRF - def filter(%{"type" => activity_type} = object) when activity_type == "Create" do + def filter(%{"type" => "Create", "object" => child_object} = object) do scrub_policy = Pleroma.Config.get([:mrf_normalize_markup, :scrub_policy]) - child = object["object"] - content = - child["content"] + child_object["content"] |> HTML.filter_tags(scrub_policy) - child = Map.put(child, "content", content) - - object = Map.put(object, "object", child) + object = put_in(object, ["object", "content"], content) {:ok, object} end diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index ea3df1b4d..da13fd7c7 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -3,46 +3,42 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do - alias Pleroma.User @moduledoc "Rejects non-public (followers-only, direct) activities" + + alias Pleroma.Config + alias Pleroma.User + @behaviour Pleroma.Web.ActivityPub.MRF + @public "https://www.w3.org/ns/activitystreams#Public" + @impl true def filter(%{"type" => "Create"} = object) do user = User.get_cached_by_ap_id(object["actor"]) - public = "https://www.w3.org/ns/activitystreams#Public" # Determine visibility visibility = cond do - public in object["to"] -> "public" - public in object["cc"] -> "unlisted" + @public in object["to"] -> "public" + @public in object["cc"] -> "unlisted" user.follower_address in object["to"] -> "followers" true -> "direct" end - policy = Pleroma.Config.get(:mrf_rejectnonpublic) + policy = Config.get(:mrf_rejectnonpublic) + + cond do + visibility in ["public", "unlisted"] -> + {:ok, object} - case visibility do - "public" -> + visibility == "followers" and Keyword.get(policy, :allow_followersonly) -> {:ok, object} - "unlisted" -> + visibility == "direct" and Keyword.get(policy, :allow_direct) -> {:ok, object} - "followers" -> - with true <- Keyword.get(policy, :allow_followersonly) do - {:ok, object} - else - _e -> {:reject, nil} - end - - "direct" -> - with true <- Keyword.get(policy, :allow_direct) do - {:ok, object} - else - _e -> {:reject, nil} - end + true -> + {:reject, nil} end end diff --git a/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex new file mode 100644 index 000000000..765704389 --- /dev/null +++ b/lib/pleroma/web/activity_pub/mrf/subchain_policy.ex @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.MRF.SubchainPolicy do + alias Pleroma.Config + alias Pleroma.Web.ActivityPub.MRF + + require Logger + + @behaviour MRF + + defp lookup_subchain(actor) do + with matches <- Config.get([:mrf_subchain, :match_actor]), + {match, subchain} <- Enum.find(matches, fn {k, _v} -> String.match?(actor, k) end) do + {:ok, match, subchain} + else + _e -> {:error, :notfound} + end + end + + @impl true + def filter(%{"actor" => actor} = message) do + with {:ok, match, subchain} <- lookup_subchain(actor) do + Logger.debug( + "[SubchainPolicy] Matched #{actor} against #{inspect(match)} with subchain #{ + inspect(subchain) + }" + ) + + subchain + |> MRF.filter(message) + else + _e -> {:ok, message} + end + end + + @impl true + def filter(message), do: {:ok, message} +end diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index 6683b8d8e..b42c4ed76 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -19,12 +19,17 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do - `mrf_tag:disable-any-subscription`: Reject any follow requests """ + @public "https://www.w3.org/ns/activitystreams#Public" + defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(_), do: [] defp process_tag( "mrf_tag:media-force-nsfw", - %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message + %{ + "type" => "Create", + "object" => %{"attachment" => child_attachment} = object + } = message ) when length(child_attachment) > 0 do tags = (object["tag"] || []) ++ ["nsfw"] @@ -41,7 +46,10 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:media-strip", - %{"type" => "Create", "object" => %{"attachment" => child_attachment} = object} = message + %{ + "type" => "Create", + "object" => %{"attachment" => child_attachment} = object + } = message ) when length(child_attachment) > 0 do object = Map.delete(object, "attachment") @@ -52,19 +60,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:force-unlisted", - %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") do - to = - List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] - - cc = - List.delete(cc, user.follower_address) ++ ["https://www.w3.org/ns/activitystreams#Public"] + if Enum.member?(to, @public) do + to = List.delete(to, @public) ++ [user.follower_address] + cc = List.delete(cc, user.follower_address) ++ [@public] object = - message["object"] + object |> Map.put("to", to) |> Map.put("cc", cc) @@ -82,19 +93,22 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do defp process_tag( "mrf_tag:sandbox", - %{"type" => "Create", "to" => to, "cc" => cc, "actor" => actor} = message + %{ + "type" => "Create", + "to" => to, + "cc" => cc, + "actor" => actor, + "object" => object + } = message ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, "https://www.w3.org/ns/activitystreams#Public") or - Enum.member?(cc, "https://www.w3.org/ns/activitystreams#Public") do - to = - List.delete(to, "https://www.w3.org/ns/activitystreams#Public") ++ [user.follower_address] - - cc = List.delete(cc, "https://www.w3.org/ns/activitystreams#Public") + if Enum.member?(to, @public) or Enum.member?(cc, @public) do + to = List.delete(to, @public) ++ [user.follower_address] + cc = List.delete(cc, @public) object = - message["object"] + object |> Map.put("to", to) |> Map.put("cc", cc) @@ -123,7 +137,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do end end - defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), do: {:reject, nil} + defp process_tag("mrf_tag:disable-any-subscription", %{"type" => "Follow"}), + do: {:reject, nil} defp process_tag(_, message), do: {:ok, message} diff --git a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex index 47663414a..e35d2c422 100644 --- a/lib/pleroma/web/activity_pub/mrf/user_allowlist.ex +++ b/lib/pleroma/web/activity_pub/mrf/user_allowlist_policy.ex @@ -21,7 +21,12 @@ defmodule Pleroma.Web.ActivityPub.MRF.UserAllowListPolicy do @impl true def filter(%{"actor" => actor} = object) do actor_info = URI.parse(actor) - allow_list = Config.get([:mrf_user_allowlist, String.to_atom(actor_info.host)], []) + + allow_list = + Config.get( + [:mrf_user_allowlist, String.to_atom(actor_info.host)], + [] + ) filter_by_list(object, allow_list) end diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 8f1399ce6..18145e45f 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -88,22 +88,72 @@ defmodule Pleroma.Web.ActivityPub.Publisher do true else inbox_info = URI.parse(inbox) - !Enum.member?(Pleroma.Config.get([:instance, :quarantined_instances], []), inbox_info.host) + !Enum.member?(Config.get([:instance, :quarantined_instances], []), inbox_info.host) end end - @doc """ - Publishes an activity to all relevant peers. - """ - def publish(%User{} = actor, %Activity{} = activity) do - remote_followers = + defp recipients(actor, activity) do + followers = if actor.follower_address in activity.recipients do {:ok, followers} = User.get_followers(actor) - followers |> Enum.filter(&(!&1.local)) + Enum.filter(followers, &(!&1.local)) else [] end + Pleroma.Web.Salmon.remote_users(actor, activity) ++ followers + end + + defp get_cc_ap_ids(ap_id, recipients) do + host = Map.get(URI.parse(ap_id), :host) + + recipients + |> Enum.filter(fn %User{ap_id: ap_id} -> Map.get(URI.parse(ap_id), :host) == host end) + |> Enum.map(& &1.ap_id) + end + + @doc """ + Publishes an activity with BCC to all relevant peers. + """ + + def publish(actor, %{data: %{"bcc" => bcc}} = activity) when is_list(bcc) and bcc != [] do + public = is_public?(activity) + {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) + + recipients = recipients(actor, activity) + + recipients + |> Enum.filter(&User.ap_enabled?/1) + |> Enum.map(fn %{info: %{source_data: data}} -> data["inbox"] end) + |> Enum.filter(fn inbox -> should_federate?(inbox, public) end) + |> Instances.filter_reachable() + |> Enum.each(fn {inbox, unreachable_since} -> + %User{ap_id: ap_id} = + Enum.find(recipients, fn %{info: %{source_data: data}} -> data["inbox"] == inbox end) + + # Get all the recipients on the same host and add them to cc. Otherwise it a remote + # instance would only accept a first message for the first recipient and ignore the rest. + cc = get_cc_ap_ids(ap_id, recipients) + + json = + data + |> Map.put("cc", cc) + |> Jason.encode!() + + Pleroma.Web.Federator.Publisher.enqueue_one(__MODULE__, %{ + inbox: inbox, + json: json, + actor: actor, + id: activity.data["id"], + unreachable_since: unreachable_since + }) + end) + end + + @doc """ + Publishes an activity to all relevant peers. + """ + def publish(%User{} = actor, %Activity{} = activity) do public = is_public?(activity) if public && Config.get([:instance, :allow_relay]) do @@ -114,7 +164,7 @@ defmodule Pleroma.Web.ActivityPub.Publisher do {:ok, data} = Transmogrifier.prepare_outgoing(activity.data) json = Jason.encode!(data) - (Pleroma.Web.Salmon.remote_users(activity) ++ remote_followers) + recipients(actor, activity) |> Enum.filter(fn user -> User.ap_enabled?(user) end) |> Enum.map(fn %{info: %{source_data: data}} -> (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 5edd8ccc7..602ae48e1 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -14,6 +14,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Federator import Ecto.Query @@ -22,19 +23,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do @doc """ Modifies an incoming AP object (mastodon format) to our internal format. """ - def fix_object(object) do + def fix_object(object, options \\ []) do object |> fix_actor |> fix_url |> fix_attachments |> fix_context - |> fix_in_reply_to + |> fix_in_reply_to(options) |> fix_emoji |> fix_tag |> fix_content_map |> fix_likes |> fix_addressing |> fix_summary + |> fix_type(options) end def fix_summary(%{"summary" => nil} = object) do @@ -65,7 +67,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def fix_explicit_addressing(%{"to" => to, "cc" => cc} = object, explicit_mentions) do + def fix_explicit_addressing( + %{"to" => to, "cc" => cc} = object, + explicit_mentions, + follower_collection + ) do explicit_to = to |> Enum.filter(fn x -> x in explicit_mentions end) @@ -76,6 +82,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do final_cc = (cc ++ explicit_cc) + |> Enum.reject(fn x -> String.ends_with?(x, "/followers") and x != follower_collection end) |> Enum.uniq() object @@ -83,7 +90,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> Map.put("cc", final_cc) end - def fix_explicit_addressing(object, _explicit_mentions), do: object + def fix_explicit_addressing(object, _explicit_mentions, _followers_collection), do: object # if directMessage flag is set to true, leave the addressing alone def fix_explicit_addressing(%{"directMessage" => true} = object), do: object @@ -93,10 +100,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do object |> Utils.determine_explicit_mentions() - explicit_mentions = explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public"] + follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address - object - |> fix_explicit_addressing(explicit_mentions) + explicit_mentions = + explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection] + + fix_explicit_addressing(object, explicit_mentions, follower_collection) end # if as:Public is addressed, then make sure the followers collection is also addressed @@ -133,7 +142,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> fix_addressing_list("cc") |> fix_addressing_list("bto") |> fix_addressing_list("bcc") - |> fix_explicit_addressing + |> fix_explicit_addressing() |> fix_implicit_addressing(followers_collection) end @@ -156,7 +165,9 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do object end - def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object) + def fix_in_reply_to(object, options \\ []) + + def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) when not is_nil(in_reply_to) do in_reply_to_id = cond do @@ -174,28 +185,34 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "" end - case get_obj_helper(in_reply_to_id) do - {:ok, replied_object} -> - with %Activity{} = _activity <- - Activity.get_create_by_object_ap_id(replied_object.data["id"]) do - object - |> Map.put("inReplyTo", replied_object.data["id"]) - |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) - |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) - |> Map.put("context", replied_object.data["context"] || object["conversation"]) - else - e -> - Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object = Map.put(object, "inReplyToAtomUri", in_reply_to_id) + + if Federator.allowed_incoming_reply_depth?(options[:depth]) do + case get_obj_helper(in_reply_to_id, options) do + {:ok, replied_object} -> + with %Activity{} = _activity <- + Activity.get_create_by_object_ap_id(replied_object.data["id"]) do object - end + |> Map.put("inReplyTo", replied_object.data["id"]) + |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id) + |> Map.put("conversation", replied_object.data["context"] || object["conversation"]) + |> Map.put("context", replied_object.data["context"] || object["conversation"]) + else + e -> + Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object + end - e -> - Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") - object + e -> + Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}") + object + end + else + object end end - def fix_in_reply_to(object), do: object + def fix_in_reply_to(object, _options), do: object def fix_context(object) do context = object["context"] || object["conversation"] || Utils.generate_context_id() @@ -328,6 +345,23 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_content_map(object), do: object + def fix_type(object, options \\ []) + + def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do + reply = + if Federator.allowed_incoming_reply_depth?(options[:depth]) do + Object.normalize(reply_id, true) + end + + if reply && (reply.data["type"] == "Question" and object["name"]) do + Map.put(object, "type", "Answer") + else + object + end + end + + def fix_type(object, _), do: object + defp mastodon_follow_hack(%{"id" => id, "actor" => follower_id}, followed) do with true <- id =~ "follows", %User{local: true} = follower <- User.get_cached_by_ap_id(follower_id), @@ -354,9 +388,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end + def handle_incoming(data, options \\ []) + # Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them # with nil ID. - def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data) do + def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} = data, _options) do with context <- data["context"] || Utils.generate_context_id(), content <- data["content"] || "", %User{} = actor <- User.get_cached_by_ap_id(actor), @@ -389,16 +425,20 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end # disallow objects with bogus IDs - def handle_incoming(%{"id" => nil}), do: :error - def handle_incoming(%{"id" => ""}), do: :error + def handle_incoming(%{"id" => nil}, _options), do: :error + def handle_incoming(%{"id" => ""}, _options), do: :error # length of https:// = 8, should validate better, but good enough for now. - def handle_incoming(%{"id" => id}) when not (is_binary(id) and length(id) > 8), do: :error + def handle_incoming(%{"id" => id}, _options) when not (is_binary(id) and length(id) > 8), + do: :error # TODO: validate those with a Ecto scheme # - tags # - emoji - def handle_incoming(%{"type" => "Create", "object" => %{"type" => objtype} = object} = data) - when objtype in ["Article", "Note", "Video", "Page"] do + def handle_incoming( + %{"type" => "Create", "object" => %{"type" => objtype} = object} = data, + options + ) + when objtype in ["Article", "Note", "Video", "Page", "Question", "Answer"] do actor = Containment.get_actor(data) data = @@ -407,7 +447,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with nil <- Activity.get_create_by_object_ap_id(object["id"]), {:ok, %User{} = user} <- User.get_or_fetch_by_ap_id(data["actor"]) do - object = fix_object(data["object"]) + options = Keyword.put(options, :depth, (options[:depth] || 0) + 1) + object = fix_object(data["object"], options) params = %{ to: data["to"], @@ -432,16 +473,19 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data + %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, + _options ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), - {:user_blocked, false} <- + {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, - {:user_locked, false} <- {:user_locked, User.locked?(followed)}, - {:follow, {:ok, follower}} <- {:follow, User.follow(follower, followed)} do + {_, false} <- {:user_locked, User.locked?(followed)}, + {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, + {_, {:ok, _}} <- + {:follow_state_update, Utils.update_follow_state_for_all(activity, "accept")} do ActivityPub.accept(%{ to: [follower.ap_id], actor: followed, @@ -450,7 +494,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do }) else {:user_blocked, true} -> - {:ok, _} = Utils.update_follow_state(activity, "reject") + {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -460,7 +504,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do }) {:follow, {:error, _}} -> - {:ok, _} = Utils.update_follow_state(activity, "reject") + {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") ActivityPub.reject(%{ to: [follower.ap_id], @@ -481,38 +525,35 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data + %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {: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, activity} <- - ActivityPub.accept(%{ - to: follow_activity.data["to"], - type: "Accept", - actor: followed, - object: follow_activity.data["id"], - local: false - }) do - if not User.following?(follower, followed) do - {:ok, _follower} = User.follow(follower, followed) - end - - {:ok, activity} + {:ok, _follower} = User.follow(follower, followed) do + ActivityPub.accept(%{ + to: follow_activity.data["to"], + type: "Accept", + actor: followed, + object: follow_activity.data["id"], + local: false + }) else _e -> :error end end def handle_incoming( - %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data + %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = followed} <- User.get_or_fetch_by_ap_id(actor), {:ok, follow_activity} <- get_follow_activity(follow_object, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), + {: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, activity} <- ActivityPub.reject(%{ @@ -531,7 +572,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Like", "object" => object_id, "actor" => _actor, "id" => id} = data + %{"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), @@ -544,7 +586,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data + %{"type" => "Announce", "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), @@ -559,7 +602,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def handle_incoming( %{"type" => "Update", "object" => %{"type" => object_type} = object, "actor" => actor_id} = - data + data, + _options ) when object_type in ["Person", "Application", "Service", "Organization"] do with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do @@ -597,7 +641,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do # an error or a tombstone. This would allow us to verify that a deletion actually took # place. def handle_incoming( - %{"type" => "Delete", "object" => object_id, "actor" => _actor, "id" => _id} = data + %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data, + _options ) do object_id = Utils.get_ap_id(object_id) @@ -608,7 +653,30 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, activity} <- ActivityPub.delete(object, false) do {:ok, activity} else - _e -> :error + nil -> + case User.get_cached_by_ap_id(object_id) do + %User{ap_id: ^actor} = user -> + {:ok, followers} = User.get_followers(user) + + Enum.each(followers, fn follower -> + User.unfollow(follower, user) + end) + + {:ok, friends} = User.get_friends(user) + + Enum.each(friends, fn followed -> + User.unfollow(user, followed) + end) + + User.invalidate_cache(user) + Repo.delete(user) + + nil -> + :error + end + + _e -> + :error end end @@ -618,7 +686,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Announce", "object" => object_id}, "actor" => _actor, "id" => id - } = data + } = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -636,7 +705,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Follow", "object" => followed}, "actor" => follower, "id" => id - } = _data + } = _data, + _options ) do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), @@ -654,7 +724,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Block", "object" => blocked}, "actor" => blocker, "id" => id - } = _data + } = _data, + _options ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked <- User.get_cached_by_ap_id(blocked), @@ -668,7 +739,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data + %{"type" => "Block", "object" => blocked, "actor" => blocker, "id" => id} = _data, + _options ) do with true <- Pleroma.Config.get([:activitypub, :accept_blocks]), %User{local: true} = blocked = User.get_cached_by_ap_id(blocked), @@ -688,7 +760,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do "object" => %{"type" => "Like", "object" => object_id}, "actor" => _actor, "id" => id - } = data + } = data, + _options ) do with actor <- Containment.get_actor(data), {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), @@ -700,10 +773,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end end - def handle_incoming(_), do: :error + def handle_incoming(_, _), do: :error - def get_obj_helper(id) do - if object = Object.normalize(id), do: {:ok, object}, else: nil + def get_obj_helper(id, options \\ []) do + if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil end def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do @@ -731,6 +804,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do |> set_reply_to_uri |> strip_internal_fields |> strip_internal_tags + |> set_type end # @doc @@ -740,13 +814,16 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def prepare_outgoing(%{"type" => "Create", "object" => object_id} = data) do object = - Object.normalize(object_id).data + object_id + |> Object.normalize() + |> Map.get(:data) |> prepare_object data = data |> Map.put("object", object) |> Map.merge(Utils.make_json_ld_header()) + |> Map.delete("bcc") {:ok, data} end @@ -895,6 +972,12 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do Map.put(object, "sensitive", "nsfw" in tags) end + def set_type(%{"type" => "Answer"} = object) do + Map.put(object, "type", "Note") + end + + def set_type(object), do: object + def add_attributed_to(object) do attributed_to = object["attributedTo"] || object["actor"] @@ -1007,6 +1090,10 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user]) end + if Pleroma.Config.get([:instance, :external_user_synchronization]) do + update_following_followers_counters(user) + end + {:ok, user} else %User{} = user -> {:ok, user} @@ -1039,4 +1126,27 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do data |> maybe_fix_user_url end + + def update_following_followers_counters(user) do + info = %{} + + following = fetch_counter(user.following_address) + info = if following, do: Map.put(info, :following_count, following), else: info + + followers = fetch_counter(user.follower_address) + info = if followers, do: Map.put(info, :follower_count, followers), else: info + + User.set_info_cache(user, info) + end + + defp fetch_counter(url) do + with {:ok, %{body: body, status: code}} when code in 200..299 <- + Pleroma.HTTP.get( + url, + [{:Accept, "application/activity+json"}] + ), + {:ok, data} <- Jason.decode(body) do + data["totalItems"] + end + end end diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index ca8a0844b..c146f59d4 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -19,18 +19,14 @@ defmodule Pleroma.Web.ActivityPub.Utils do require Logger - @supported_object_types ["Article", "Note", "Video", "Page"] + @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"] @supported_report_states ~w(open closed resolved) @valid_visibilities ~w(public unlisted private direct) # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. - def get_ap_id(object) do - case object do - %{"id" => id} -> id - id -> id - end - end + def get_ap_id(%{"id" => id} = _), do: id + def get_ap_id(id), do: id def normalize_params(params) do Map.put(params, "actor", get_ap_id(params["actor"])) @@ -151,16 +147,18 @@ defmodule Pleroma.Web.ActivityPub.Utils do def create_context(context) do context = context || generate_id("contexts") - changeset = Object.context_mapping(context) - case Repo.insert(changeset) do - {:ok, object} -> - object + # Ecto has problems accessing the constraint inside the jsonb, + # so we explicitly check for the existed object before insert + object = Object.get_cached_by_ap_id(context) - # This should be solved by an upsert, but it seems ecto - # has problems accessing the constraint inside the jsonb. - {:error, _} -> - Object.get_cached_by_ap_id(context) + with true <- is_nil(object), + changeset <- Object.context_mapping(context), + {:ok, inserted_object} <- Repo.insert(changeset) do + inserted_object + else + _ -> + object end end @@ -168,14 +166,17 @@ defmodule Pleroma.Web.ActivityPub.Utils do Enqueues an activity for federation if it's local """ def maybe_federate(%Activity{local: true} = activity) do - priority = - case activity.data["type"] do - "Delete" -> 10 - "Create" -> 1 - _ -> 5 - end + if Pleroma.Config.get!([:instance, :federating]) do + priority = + case activity.data["type"] do + "Delete" -> 10 + "Create" -> 1 + _ -> 5 + end + + Pleroma.Web.Federator.publish(activity, priority) + end - Pleroma.Web.Federator.publish(activity, priority) :ok end @@ -376,8 +377,8 @@ defmodule Pleroma.Web.ActivityPub.Utils do @doc """ Updates a follow activity's state (for locked accounts). """ - def update_follow_state( - %Activity{data: %{"actor" => actor, "object" => object, "state" => "pending"}} = activity, + def update_follow_state_for_all( + %Activity{data: %{"actor" => actor, "object" => object}} = activity, state ) do try do @@ -789,4 +790,22 @@ defmodule Pleroma.Web.ActivityPub.Utils do [to, cc, recipients] end end + + def get_existing_votes(actor, %{data: %{"id" => id}}) do + query = + from( + [activity, object: object] in Activity.with_preloaded_object(Activity), + where: fragment("(?)->>'type' = 'Create'", activity.data), + where: fragment("(?)->>'actor' = ?", activity.data, ^actor), + where: + fragment( + "(?)->>'inReplyTo' = ?", + object.data, + ^to_string(id) + ), + where: fragment("(?)->>'type' = 'Answer'", object.data) + ) + + Repo.all(query) + end end diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 327e0e05b..d9c1bcb2c 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -98,29 +98,31 @@ defmodule Pleroma.Web.ActivityPub.UserView do |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user, page: page}) do + def render("following.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 end - collection(following, "#{user.ap_id}/following", page, !user.info.hide_follows, total) + collection(following, "#{user.ap_id}/following", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("following.json", %{user: user}) do + def render("following.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows query = User.get_friends_query(user) query = from(user in query, select: [:ap_id]) following = Repo.all(query) total = - if !user.info.hide_follows do + if showing do length(following) else 0 @@ -130,34 +132,43 @@ defmodule Pleroma.Web.ActivityPub.UserView do "id" => "#{user.ap_id}/following", "type" => "OrderedCollection", "totalItems" => total, - "first" => collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + "first" => + if showing do + collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows) + else + "#{user.ap_id}/following?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user, page: page}) do + def render("followers.json", %{user: user, page: page} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 end - collection(followers, "#{user.ap_id}/followers", page, !user.info.hide_followers, total) + collection(followers, "#{user.ap_id}/followers", page, showing, total) |> Map.merge(Utils.make_json_ld_header()) end - def render("followers.json", %{user: user}) do + def render("followers.json", %{user: user} = opts) do + showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers + query = User.get_followers_query(user) query = from(user in query, select: [:ap_id]) followers = Repo.all(query) total = - if !user.info.hide_followers do + if showing do length(followers) else 0 @@ -168,7 +179,11 @@ defmodule Pleroma.Web.ActivityPub.UserView do "type" => "OrderedCollection", "totalItems" => total, "first" => - collection(followers, "#{user.ap_id}/followers", 1, !user.info.hide_followers, total) + if showing do + collection(followers, "#{user.ap_id}/followers", 1, showing, total) + else + "#{user.ap_id}/followers?page=1" + end } |> Map.merge(Utils.make_json_ld_header()) end diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 93b50ee47..2666edc7c 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.ActivityPub.Visibility do alias Pleroma.Activity alias Pleroma.Object @@ -30,6 +34,20 @@ defmodule Pleroma.Web.ActivityPub.Visibility do !is_public?(activity) && !is_private?(activity) end + def is_list?(%{data: %{"listMessage" => _}}), do: true + def is_list?(_), do: false + + def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true + + def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do + user.ap_id in activity.data["to"] || + list_ap_id + |> Pleroma.List.get_by_ap_id() + |> Pleroma.List.member?(user) + end + + def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false + def visible_for_user?(activity, nil) do is_public?(activity) end @@ -66,6 +84,12 @@ defmodule Pleroma.Web.ActivityPub.Visibility do Enum.any?(to, &String.contains?(&1, "/followers")) -> "private" + object.data["directMessage"] == true -> + "direct" + + is_binary(object.data["listMessage"]) -> + "list" + length(cc) > 0 -> "private" diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index de2a13c01..4a0bf4823 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.AdminAPI.Config + alias Pleroma.Web.AdminAPI.ConfigView alias Pleroma.Web.AdminAPI.ReportView alias Pleroma.Web.AdminAPI.Search alias Pleroma.Web.CommonAPI @@ -72,7 +74,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def user_show(conn, %{"nickname" => nickname}) do - with %User{} = user <- User.get_cached_by_nickname(nickname) do + with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do conn |> json(AccountView.render("show.json", %{user: user})) else @@ -158,9 +160,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_add(conn, _) do - conn - |> put_status(404) - |> json(%{error: "No such permission_group"}) + render_error(conn, :not_found, "No such permission_group") end def right_get(conn, %{"nickname" => nickname}) do @@ -182,9 +182,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do ) when permission_group in ["moderator", "admin"] do if admin_nickname == nickname do - conn - |> put_status(403) - |> json(%{error: "You can't revoke your own admin status."}) + render_error(conn, :forbidden, "You can't revoke your own admin status.") else user = User.get_cached_by_nickname(nickname) @@ -205,9 +203,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end def right_delete(conn, _) do - conn - |> put_status(404) - |> json(%{error: "No such permission_group"}) + render_error(conn, :not_found, "No such permission_group") end def set_activation_status(conn, %{"nickname" => nickname, "status" => status}) do @@ -362,28 +358,63 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do end end + def config_show(conn, _params) do + configs = Pleroma.Repo.all(Config) + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: configs}) + end + + def config_update(conn, %{"configs" => configs}) do + updated = + if Pleroma.Config.get([:instance, :dynamic_configuration]) do + updated = + Enum.map(configs, fn + %{"group" => group, "key" => key, "delete" => "true"} -> + {:ok, _} = Config.delete(%{group: group, key: key}) + nil + + %{"group" => group, "key" => key, "value" => value} -> + {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) + config + end) + |> Enum.reject(&is_nil(&1)) + + Pleroma.Config.TransferTask.load_and_update_env() + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "false"]) + updated + else + [] + end + + conn + |> put_view(ConfigView) + |> render("index.json", %{configs: updated}) + end + def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, {:error, reason}) do conn - |> put_status(400) + |> put_status(:bad_request) |> json(reason) end def errors(conn, {:param_cast, _}) do conn - |> put_status(400) - |> json("Invalid parameters") + |> put_status(:bad_request) + |> json(dgettext("errors", "Invalid parameters")) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end defp page_params(params) do diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex new file mode 100644 index 000000000..b4eb8e002 --- /dev/null +++ b/lib/pleroma/web/admin_api/config.ex @@ -0,0 +1,152 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.Config do + use Ecto.Schema + import Ecto.Changeset + import Pleroma.Web.Gettext + alias __MODULE__ + alias Pleroma.Repo + + @type t :: %__MODULE__{} + + schema "config" do + field(:key, :string) + field(:group, :string) + field(:value, :binary) + + timestamps() + end + + @spec get_by_params(map()) :: Config.t() | nil + def get_by_params(params), do: Repo.get_by(Config, params) + + @spec changeset(Config.t(), map()) :: Changeset.t() + def changeset(config, params \\ %{}) do + config + |> cast(params, [:key, :group, :value]) + |> validate_required([:key, :group, :value]) + |> unique_constraint(:key, name: :config_group_key_index) + end + + @spec create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def create(params) do + %Config{} + |> changeset(Map.put(params, :value, transform(params[:value]))) + |> Repo.insert() + end + + @spec update(Config.t(), map()) :: {:ok, Config} | {:error, Changeset.t()} + def update(%Config{} = config, %{value: value}) do + config + |> change(value: transform(value)) + |> Repo.update() + end + + @spec update_or_create(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def update_or_create(params) do + with %Config{} = config <- Config.get_by_params(Map.take(params, [:group, :key])) do + Config.update(config, params) + else + nil -> Config.create(params) + end + end + + @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} + def delete(params) do + with %Config{} = config <- Config.get_by_params(params) do + Repo.delete(config) + else + nil -> + err = + dgettext("errors", "Config with params %{params} not found", params: inspect(params)) + + {:error, err} + end + end + + @spec from_binary(binary()) :: term() + def from_binary(binary), do: :erlang.binary_to_term(binary) + + @spec from_binary_with_convert(binary()) :: any() + def from_binary_with_convert(binary) do + from_binary(binary) + |> do_convert() + end + + defp do_convert(entity) when is_list(entity) do + for v <- entity, into: [], do: do_convert(v) + end + + defp do_convert(entity) when is_map(entity) do + for {k, v} <- entity, into: %{}, do: {do_convert(k), do_convert(v)} + end + + defp do_convert({:dispatch, [entity]}), do: %{"tuple" => [":dispatch", [inspect(entity)]]} + + defp do_convert(entity) when is_tuple(entity), + do: %{"tuple" => do_convert(Tuple.to_list(entity))} + + defp do_convert(entity) when is_boolean(entity) or is_number(entity) or is_nil(entity), + do: entity + + defp do_convert(entity) when is_atom(entity) do + string = to_string(entity) + + if String.starts_with?(string, "Elixir."), + do: do_convert(string), + else: ":" <> string + end + + defp do_convert("Elixir." <> module_name), do: module_name + + defp do_convert(entity) when is_binary(entity), do: entity + + @spec transform(any()) :: binary() + def transform(entity) when is_binary(entity) or is_map(entity) or is_list(entity) do + :erlang.term_to_binary(do_transform(entity)) + end + + def transform(entity), do: :erlang.term_to_binary(entity) + + defp do_transform(%Regex{} = entity) when is_map(entity), do: entity + + defp do_transform(%{"tuple" => [":dispatch", [entity]]}) do + cleaned_string = String.replace(entity, ~r/[^\w|^{:,[|^,|^[|^\]^}|^\/|^\.|^"]^\s/, "") + {dispatch_settings, []} = Code.eval_string(cleaned_string, [], requires: [], macros: []) + {:dispatch, [dispatch_settings]} + end + + defp do_transform(%{"tuple" => entity}) do + Enum.reduce(entity, {}, fn val, acc -> Tuple.append(acc, do_transform(val)) end) + end + + defp do_transform(entity) when is_map(entity) do + for {k, v} <- entity, into: %{}, do: {do_transform(k), do_transform(v)} + end + + defp do_transform(entity) when is_list(entity) do + for v <- entity, into: [], do: do_transform(v) + end + + defp do_transform(entity) when is_binary(entity) do + String.trim(entity) + |> do_transform_string() + end + + defp do_transform(entity), do: entity + + defp do_transform_string("~r/" <> pattern) do + pattern = String.trim_trailing(pattern, "/") + ~r/#{pattern}/ + end + + defp do_transform_string(":" <> atom), do: String.to_atom(atom) + + defp do_transform_string(value) do + if String.starts_with?(value, "Pleroma") or String.starts_with?(value, "Phoenix"), + do: String.to_existing_atom("Elixir." <> value), + else: value + end +end diff --git a/lib/pleroma/web/admin_api/views/account_view.ex b/lib/pleroma/web/admin_api/views/account_view.ex index 28bb667d8..7e1b9c431 100644 --- a/lib/pleroma/web/admin_api/views/account_view.ex +++ b/lib/pleroma/web/admin_api/views/account_view.ex @@ -5,8 +5,11 @@ defmodule Pleroma.Web.AdminAPI.AccountView do use Pleroma.Web, :view + alias Pleroma.HTML + alias Pleroma.User alias Pleroma.User.Info alias Pleroma.Web.AdminAPI.AccountView + alias Pleroma.Web.MediaProxy def render("index.json", %{users: users, count: count, page_size: page_size}) do %{ @@ -17,9 +20,14 @@ defmodule Pleroma.Web.AdminAPI.AccountView do end def render("show.json", %{user: user}) do + avatar = User.avatar_url(user) |> MediaProxy.url() + display_name = HTML.strip_tags(user.name || user.nickname) + %{ "id" => user.id, + "avatar" => avatar, "nickname" => user.nickname, + "display_name" => display_name, "deactivated" => user.info.deactivated, "local" => user.local, "roles" => Info.roles(user.info), diff --git a/lib/pleroma/web/admin_api/views/config_view.ex b/lib/pleroma/web/admin_api/views/config_view.ex new file mode 100644 index 000000000..49add0b6e --- /dev/null +++ b/lib/pleroma/web/admin_api/views/config_view.ex @@ -0,0 +1,21 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.AdminAPI.ConfigView do + use Pleroma.Web, :view + + def render("index.json", %{configs: configs}) do + %{ + configs: render_many(configs, __MODULE__, "show.json", as: :config) + } + end + + def render("show.json", %{config: config}) do + %{ + key: config.key, + group: config.group, + value: Pleroma.Web.AdminAPI.Config.from_binary_with_convert(config.value) + } + end +end diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex index 47a73dc7e..a25f3f1fe 100644 --- a/lib/pleroma/web/admin_api/views/report_view.ex +++ b/lib/pleroma/web/admin_api/views/report_view.ex @@ -5,9 +5,9 @@ defmodule Pleroma.Web.AdminAPI.ReportView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.HTML alias Pleroma.User alias Pleroma.Web.CommonAPI.Utils - alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView def render("index.json", %{reports: reports}) do @@ -23,6 +23,13 @@ defmodule Pleroma.Web.AdminAPI.ReportView do [account_ap_id | status_ap_ids] = report.data["object"] account = User.get_cached_by_ap_id(account_ap_id) + content = + unless is_nil(report.data["content"]) do + HTML.filter_tags(report.data["content"]) + else + nil + end + statuses = Enum.map(status_ap_ids, fn ap_id -> Activity.get_by_ap_id_with_object(ap_id) @@ -30,12 +37,19 @@ defmodule Pleroma.Web.AdminAPI.ReportView do %{ id: report.id, - account: AccountView.render("account.json", %{user: account}), - actor: AccountView.render("account.json", %{user: user}), - content: report.data["content"], + account: merge_account_views(account), + actor: merge_account_views(user), + content: content, created_at: created_at, statuses: StatusView.render("index.json", %{activities: statuses, as: :activity}), state: report.data["state"] } end + + defp merge_account_views(%User{} = user) do + Pleroma.Web.MastodonAPI.AccountView.render("account.json", %{user: user}) + |> Map.merge(Pleroma.Web.AdminAPI.AccountView.render("show.json", %{user: user})) + end + + defp merge_account_views(_), do: %{} end diff --git a/lib/pleroma/web/auth/pleroma_authenticator.ex b/lib/pleroma/web/auth/pleroma_authenticator.ex index c4a6fce08..f4234b743 100644 --- a/lib/pleroma/web/auth/pleroma_authenticator.ex +++ b/lib/pleroma/web/auth/pleroma_authenticator.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.Auth.PleromaAuthenticator do - alias Comeonin.Pbkdf2 + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User @@ -16,7 +16,7 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_user(%Plug.Conn{} = conn) do with {:ok, {name, password}} <- fetch_credentials(conn), {_, %User{} = user} <- {:user, fetch_user(name)}, - {_, true} <- {:checkpw, Pbkdf2.checkpw(password, user.password_hash)} do + {_, true} <- {:checkpw, AuthenticationPlug.checkpw(password, user.password_hash)} do {:ok, user} else error -> @@ -24,6 +24,14 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do end end + @doc """ + Gets or creates Pleroma.Registration record from Ueberauth assigns. + Note: some strategies (like `keycloak`) might need extra configuration to fill `uid` from callback response — + see [`docs/config.md`](docs/config.md). + """ + def get_registration(%Plug.Conn{assigns: %{ueberauth_auth: %{uid: nil}}}), + do: {:error, :missing_uid} + def get_registration(%Plug.Conn{ assigns: %{ueberauth_auth: %{provider: provider, uid: uid} = auth} }) do @@ -51,9 +59,10 @@ defmodule Pleroma.Web.Auth.PleromaAuthenticator do def get_registration(%Plug.Conn{} = _conn), do: {:error, :missing_credentials} + @doc "Creates Pleroma.User record basing on params and Pleroma.Registration record." def create_from_registration( %Plug.Conn{params: %{"authorization" => registration_attrs}}, - registration + %Registration{} = registration ) do nickname = value([registration_attrs["nickname"], Registration.nickname(registration)]) email = value([registration_attrs["email"], Registration.email(registration)]) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 5a312d673..44af6a773 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,14 +4,15 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity - alias Pleroma.Bookmark alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils + alias Pleroma.Web.ActivityPub.Visibility + import Pleroma.Web.Gettext import Pleroma.Web.CommonAPI.Utils def follow(follower, followed) do @@ -29,15 +30,16 @@ defmodule Pleroma.Web.CommonAPI do def unfollow(follower, unfollowed) do with {:ok, follower, _follow_activity} <- User.unfollow(follower, unfollowed), - {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed) do + {:ok, _activity} <- ActivityPub.unfollow(follower, unfollowed), + {:ok, _unfollowed} <- User.unsubscribe(follower, unfollowed) do {:ok, follower} end end def accept_follow_request(follower, followed) do - with {:ok, follower} <- User.maybe_follow(follower, followed), + with {:ok, follower} <- User.follow(follower, followed), %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), - {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "accept"), {:ok, _activity} <- ActivityPub.accept(%{ to: [follower.ap_id], @@ -51,7 +53,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(follow_activity, "reject"), + {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], @@ -73,7 +75,7 @@ defmodule Pleroma.Web.CommonAPI do {:ok, delete} else _ -> - {:error, "Could not delete"} + {:error, dgettext("errors", "Could not delete")} end end @@ -84,7 +86,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.announce(user, object) else _ -> - {:error, "Could not repeat"} + {:error, dgettext("errors", "Could not repeat")} end end @@ -94,7 +96,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.unannounce(user, object) else _ -> - {:error, "Could not unrepeat"} + {:error, dgettext("errors", "Could not unrepeat")} end end @@ -105,7 +107,7 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.like(user, object) else _ -> - {:error, "Could not favorite"} + {:error, dgettext("errors", "Could not favorite")} end end @@ -115,14 +117,69 @@ defmodule Pleroma.Web.CommonAPI do ActivityPub.unlike(user, object) else _ -> - {:error, "Could not unfavorite"} + {:error, dgettext("errors", "Could not unfavorite")} end end + def vote(user, object, choices) do + with "Question" <- object.data["type"], + {:author, false} <- {:author, object.data["actor"] == user.ap_id}, + {:existing_votes, []} <- {:existing_votes, Utils.get_existing_votes(user.ap_id, object)}, + {options, max_count} <- get_options_and_max_count(object), + option_count <- Enum.count(options), + {:choice_check, {choices, true}} <- + {:choice_check, normalize_and_validate_choice_indices(choices, option_count)}, + {:count_check, true} <- {:count_check, Enum.count(choices) <= max_count} do + answer_activities = + Enum.map(choices, fn index -> + answer_data = make_answer_data(user, object, Enum.at(options, index)["name"]) + + {:ok, activity} = + ActivityPub.create(%{ + to: answer_data["to"], + actor: user, + context: object.data["context"], + object: answer_data, + additional: %{"cc" => answer_data["cc"]} + }) + + activity + end) + + object = Object.get_cached_by_ap_id(object.data["id"]) + {:ok, answer_activities, object} + else + {:author, _} -> {:error, dgettext("errors", "Poll's author can't vote")} + {:existing_votes, _} -> {:error, dgettext("errors", "Already voted")} + {:choice_check, {_, false}} -> {:error, dgettext("errors", "Invalid indices")} + {:count_check, false} -> {:error, dgettext("errors", "Too many choices")} + end + end + + defp get_options_and_max_count(object) do + if Map.has_key?(object.data, "anyOf") do + {object.data["anyOf"], Enum.count(object.data["anyOf"])} + else + {object.data["oneOf"], 1} + end + end + + defp normalize_and_validate_choice_indices(choices, count) do + Enum.map_reduce(choices, true, fn index, valid -> + index = if is_binary(index), do: String.to_integer(index), else: index + {index, if(valid, do: index < count, else: valid)} + end) + end + def get_visibility(%{"visibility" => visibility}, in_reply_to) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} + def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do + visibility = {:list, String.to_integer(list_id)} + {visibility, get_replied_to_visibility(in_reply_to)} + end + def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do visibility = get_replied_to_visibility(in_reply_to) {visibility, visibility} @@ -154,12 +211,15 @@ defmodule Pleroma.Web.CommonAPI do data, visibility ), - {to, cc} <- to_for_user_and_mentions(user, mentions, in_reply_to, visibility), + mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), + addressed_users <- get_addressed_users(mentioned_users, data["to"]), + {poll, poll_emoji} <- make_poll_data(data), + {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility), context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), full_payload <- String.trim(status <> cw), - length when length in 1..limit <- String.length(full_payload), + :ok <- validate_character_limit(full_payload, attachments, limit), object <- make_note_data( user.ap_id, @@ -171,29 +231,36 @@ defmodule Pleroma.Web.CommonAPI do tags, cw, cc, - sensitive + sensitive, + poll ), object <- Map.put( object, "emoji", - Formatter.get_emoji_map(full_payload) + Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji) ) do - res = - ActivityPub.create( - %{ - to: to, - actor: user, - context: context, - object: object, - additional: %{"cc" => cc, "directMessage" => visibility == "direct"} - }, - Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false - ) - - res + preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false + direct? = visibility == "direct" + + %{ + to: to, + actor: user, + context: context, + object: object, + additional: %{"cc" => cc, "directMessage" => direct?} + } + |> maybe_add_list_data(user, visibility) + |> ActivityPub.create(preview?) else - e -> {:error, e} + {:private_to_public, true} -> + {:error, dgettext("errors", "The message visibility must be direct")} + + {:error, _} = e -> + e + + e -> + {:error, e} end end @@ -228,12 +295,11 @@ defmodule Pleroma.Web.CommonAPI do }, object: %Object{ data: %{ - "to" => object_to, "type" => "Note" } } } = activity <- get_by_id_or_ap_id(id_or_ap_id), - true <- Enum.member?(object_to, "https://www.w3.org/ns/activitystreams#Public"), + true <- Visibility.is_public?(activity), %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity), changeset <- @@ -245,7 +311,7 @@ defmodule Pleroma.Web.CommonAPI do {:error, err} _ -> - {:error, "Could not pin"} + {:error, dgettext("errors", "Could not pin")} end end @@ -262,7 +328,7 @@ defmodule Pleroma.Web.CommonAPI do {:error, err} _ -> - {:error, "Could not unpin"} + {:error, dgettext("errors", "Could not unpin")} end end @@ -270,7 +336,7 @@ defmodule Pleroma.Web.CommonAPI do with {:ok, _} <- ThreadMute.add_mute(user.id, activity.data["context"]) do {:ok, activity} else - {:error, _} -> {:error, "conversation is already muted"} + {:error, _} -> {:error, dgettext("errors", "conversation is already muted")} end end @@ -289,15 +355,6 @@ defmodule Pleroma.Web.CommonAPI do end end - def bookmarked?(user, activity) do - with %Bookmark{} <- Bookmark.get(user.id, activity.id) do - true - else - _ -> - false - end - end - def report(user, data) do with {:account_id, %{"account_id" => account_id}} <- {:account_id, data}, {:account, %User{} = account} <- {:account, User.get_cached_by_id(account_id)}, @@ -315,8 +372,8 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} else {:error, err} -> {:error, err} - {:account_id, %{}} -> {:error, "Valid `account_id` required"} - {:account, nil} -> {:error, "Account not found"} + {:account_id, %{}} -> {:error, dgettext("errors", "Valid `account_id` required")} + {:account, nil} -> {:error, dgettext("errors", "Account not found")} end end @@ -325,14 +382,9 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} <- Utils.update_report_state(activity, state) do {:ok, activity} else - nil -> - {:error, :not_found} - - {:error, reason} -> - {:error, reason} - - _ -> - {:error, "Could not update state"} + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} + _ -> {:error, dgettext("errors", "Could not update state")} end end @@ -342,11 +394,8 @@ defmodule Pleroma.Web.CommonAPI do {:ok, activity} <- set_visibility(activity, opts) do {:ok, activity} else - nil -> - {:error, :not_found} - - {:error, reason} -> - {:error, reason} + nil -> {:error, :not_found} + {:error, reason} -> {:error, reason} end end diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d93c0d46e..fcc000969 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -3,12 +3,14 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.CommonAPI.Utils do + import Pleroma.Web.Gettext + alias Calendar.Strftime - alias Comeonin.Pbkdf2 alias Pleroma.Activity alias Pleroma.Config alias Pleroma.Formatter alias Pleroma.Object + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.Utils @@ -61,9 +63,9 @@ defmodule Pleroma.Web.CommonAPI.Utils do end) end - def to_for_user_and_mentions(user, mentions, inReplyTo, "public") do - mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - + @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: + {list(String.t()), list(String.t())} + def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users] cc = [user.follower_address] @@ -74,9 +76,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def to_for_user_and_mentions(user, mentions, inReplyTo, "unlisted") do - mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - + def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do to = [user.follower_address | mentioned_users] cc = ["https://www.w3.org/ns/activitystreams#Public"] @@ -87,14 +87,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end - def to_for_user_and_mentions(user, mentions, inReplyTo, "private") do - {to, cc} = to_for_user_and_mentions(user, mentions, inReplyTo, "direct") + def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do + {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct") {[user.follower_address | to], cc} end - def to_for_user_and_mentions(_user, mentions, inReplyTo, "direct") do - mentioned_users = Enum.map(mentions, fn {_, %{ap_id: ap_id}} -> ap_id end) - + def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do if inReplyTo do {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []} else @@ -102,6 +100,95 @@ defmodule Pleroma.Web.CommonAPI.Utils do end end + def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + + def get_addressed_users(_, to) when is_list(to) do + User.get_ap_ids_by_nicknames(to) + end + + def get_addressed_users(mentioned_users, _), do: mentioned_users + + def maybe_add_list_data(activity_params, user, {:list, list_id}) do + case Pleroma.List.get(list_id, user) do + %Pleroma.List{} = list -> + activity_params + |> put_in([:additional, "bcc"], [list.ap_id]) + |> put_in([:additional, "listMessage"], list.ap_id) + |> put_in([:object, "listMessage"], list.ap_id) + + _ -> + activity_params + end + end + + def maybe_add_list_data(activity_params, _, _), do: activity_params + + def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_in}} = data) + when is_list(options) do + %{max_expiration: max_expiration, min_expiration: min_expiration} = + limits = Pleroma.Config.get([:instance, :poll_limits]) + + # XXX: There is probably a cleaner way of doing this + try do + # In some cases mastofe sends out strings instead of integers + expires_in = if is_binary(expires_in), do: String.to_integer(expires_in), else: expires_in + + if Enum.count(options) > limits.max_options do + raise ArgumentError, message: "Poll can't contain more than #{limits.max_options} options" + end + + {poll, emoji} = + Enum.map_reduce(options, %{}, fn option, emoji -> + if String.length(option) > limits.max_option_chars do + raise ArgumentError, + message: + "Poll options cannot be longer than #{limits.max_option_chars} characters each" + end + + {%{ + "name" => option, + "type" => "Note", + "replies" => %{"type" => "Collection", "totalItems" => 0} + }, Map.merge(emoji, Formatter.get_emoji_map(option))} + end) + + case expires_in do + expires_in when expires_in > max_expiration -> + raise ArgumentError, message: "Expiration date is too far in the future" + + expires_in when expires_in < min_expiration -> + raise ArgumentError, message: "Expiration date is too soon" + + _ -> + :noop + end + + end_time = + NaiveDateTime.utc_now() + |> NaiveDateTime.add(expires_in) + |> NaiveDateTime.to_iso8601() + + poll = + if Pleroma.Web.ControllerHelper.truthy_param?(data["poll"]["multiple"]) do + %{"type" => "Question", "anyOf" => poll, "closed" => end_time} + else + %{"type" => "Question", "oneOf" => poll, "closed" => end_time} + end + + {poll, emoji} + rescue + e in ArgumentError -> e.message + end + end + + def make_poll_data(%{"poll" => poll}) when is_map(poll) do + "Invalid poll" + end + + def make_poll_data(_data) do + {%{}, %{}} + end + def make_content_html( status, attachments, @@ -224,7 +311,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do tags, cw \\ nil, cc \\ [], - sensitive \\ false + sensitive \\ false, + merge \\ %{} ) do object = %{ "type" => "Note", @@ -239,12 +327,15 @@ defmodule Pleroma.Web.CommonAPI.Utils do "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() } - with false <- is_nil(in_reply_to), - %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do - Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) - else - _ -> object - end + object = + with false <- is_nil(in_reply_to), + %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do + Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) + else + _ -> object + end + + Map.merge(object, merge) end def format_naive_asctime(date) do @@ -297,10 +388,10 @@ defmodule Pleroma.Web.CommonAPI.Utils do def confirm_current_password(user, password) do with %User{local: true} = db_user <- User.get_cached_by_id(user.id), - true <- Pbkdf2.checkpw(password, db_user.password_hash) do + true <- AuthenticationPlug.checkpw(password, db_user.password_hash) do {:ok, db_user} else - _ -> {:error, "Invalid password."} + _ -> {:error, dgettext("errors", "Invalid password.")} end end @@ -383,7 +474,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do if String.length(comment) <= max_size do {:ok, format_input(comment, "text/plain")} else - {:error, "Comment must be up to #{max_size} characters"} + {:error, + dgettext("errors", "Comment must be up to %{max_size} characters", max_size: max_size)} end end @@ -418,7 +510,32 @@ defmodule Pleroma.Web.CommonAPI.Utils do context else _e -> - {:error, "No such conversation"} + {:error, dgettext("errors", "No such conversation")} + end + end + + def make_answer_data(%User{ap_id: ap_id}, object, name) do + %{ + "type" => "Answer", + "actor" => ap_id, + "cc" => [object.data["actor"]], + "to" => [], + "name" => name, + "inReplyTo" => object.data["id"] + } + end + + def validate_character_limit(full_payload, attachments, limit) do + length = String.length(full_payload) + + if length < limit do + if length > 0 or Enum.count(attachments) > 0 do + :ok + else + {:error, dgettext("errors", "Cannot post an empty status without attachments")} + end + else + {:error, dgettext("errors", "The status is over the character limit")} end end end diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 55706eeb8..8a753bb4f 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -15,4 +15,22 @@ defmodule Pleroma.Web.ControllerHelper do |> put_status(status) |> json(json) end + + @spec fetch_integer_param(map(), String.t(), integer() | nil) :: integer() | nil + def fetch_integer_param(params, name, default \\ nil) do + params + |> Map.get(name, default) + |> param_to_integer(default) + end + + defp param_to_integer(val, _) when is_integer(val), do: val + + defp param_to_integer(val, default) when is_binary(val) do + case Integer.parse(val) do + {res, _} -> res + _ -> default + end + end + + defp param_to_integer(_, default), do: default end diff --git a/lib/pleroma/web/endpoint.ex b/lib/pleroma/web/endpoint.ex index 8cd7a2270..c123530dc 100644 --- a/lib/pleroma/web/endpoint.ex +++ b/lib/pleroma/web/endpoint.ex @@ -7,13 +7,9 @@ defmodule Pleroma.Web.Endpoint do socket("/socket", Pleroma.Web.UserSocket) - # Serve at "/" the static files from "priv/static" directory. - # - # You should set gzip to true if you are running phoenix.digest - # when deploying your static files in production. + plug(Pleroma.Plugs.SetLocalePlug) plug(CORSPlug) plug(Pleroma.Plugs.HTTPSecurityPlug) - plug(Pleroma.Plugs.UploadedMedia) @static_cache_control "public, no-cache" @@ -30,6 +26,10 @@ defmodule Pleroma.Web.Endpoint do } ) + # Serve at "/" the static files from "priv/static" directory. + # + # You should set gzip to true if you are running phoenix.digest + # when deploying your static files in production. plug( Plug.Static, at: "/", @@ -66,7 +66,7 @@ defmodule Pleroma.Web.Endpoint do parsers: [:urlencoded, :multipart, :json], pass: ["*/*"], json_decoder: Jason, - length: Application.get_env(:pleroma, :instance) |> Keyword.get(:upload_limit), + length: Pleroma.Config.get([:instance, :upload_limit]), body_reader: {Pleroma.Web.Plugs.DigestPlug, :read_body, []} ) @@ -91,7 +91,7 @@ defmodule Pleroma.Web.Endpoint do Plug.Session, store: :cookie, key: cookie_name, - signing_salt: {Pleroma.Config, :get, [[__MODULE__, :signing_salt], "CqaoopA2"]}, + signing_salt: Pleroma.Config.get([__MODULE__, :signing_salt], "CqaoopA2"), http_only: true, secure: secure_cookies, extra: extra diff --git a/lib/pleroma/web/federator/federator.ex b/lib/pleroma/web/federator/federator.ex index f4c9fe284..f4f9e83e0 100644 --- a/lib/pleroma/web/federator/federator.ex +++ b/lib/pleroma/web/federator/federator.ex @@ -22,6 +22,18 @@ defmodule Pleroma.Web.Federator do refresh_subscriptions() end + @doc "Addresses [memory leaks on recursive replies fetching](https://git.pleroma.social/pleroma/pleroma/issues/161)" + # credo:disable-for-previous-line Credo.Check.Readability.MaxLineLength + def allowed_incoming_reply_depth?(depth) do + max_replies_depth = Pleroma.Config.get([:instance, :federation_incoming_replies_max_depth]) + + if max_replies_depth do + (depth || 1) <= max_replies_depth + else + true + end + end + # Client API def incoming_doc(doc) do diff --git a/lib/pleroma/web/federator/retry_queue.ex b/lib/pleroma/web/federator/retry_queue.ex index 71e49494f..3db948c2e 100644 --- a/lib/pleroma/web/federator/retry_queue.ex +++ b/lib/pleroma/web/federator/retry_queue.ex @@ -15,7 +15,9 @@ defmodule Pleroma.Web.Federator.RetryQueue do def start_link do enabled = - if Mix.env() == :test, do: true, else: Pleroma.Config.get([__MODULE__, :enabled], false) + if Pleroma.Config.get(:env) == :test, + do: true, + else: Pleroma.Config.get([__MODULE__, :enabled], false) if enabled do Logger.info("Starting retry queue") @@ -219,7 +221,7 @@ defmodule Pleroma.Web.Federator.RetryQueue do {:noreply, state} end - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do defp growth_function(_retries) do _shutit = Pleroma.Config.get([__MODULE__, :initial_timeout]) DateTime.to_unix(DateTime.utc_now()) - 1 diff --git a/lib/pleroma/web/mastodon_api/mastodon_api.ex b/lib/pleroma/web/mastodon_api/mastodon_api.ex index 3a3ec7c2a..46944dcbc 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.MastodonAPI do import Ecto.Query import Ecto.Changeset @@ -49,7 +53,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do options = cast_params(params) user - |> Notification.for_user_query() + |> Notification.for_user_query(options) |> restrict(:exclude_types, options) |> Pagination.fetch_paginated(params) end @@ -63,7 +67,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPI do defp cast_params(params) do param_types = %{ exclude_types: {:array, :string}, - reblogs: :boolean + reblogs: :boolean, + with_muted: :boolean } changeset = cast({%{}, param_types}, params, Map.keys(param_types)) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index bc75ab35a..29b1391d3 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -14,8 +14,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.HTTP alias Pleroma.Notification alias Pleroma.Object - alias Pleroma.Object.Fetcher alias Pleroma.Pagination + alias Pleroma.Plugs.RateLimiter alias Pleroma.Repo alias Pleroma.ScheduledActivity alias Pleroma.Stats @@ -47,15 +47,25 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do require Logger + @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status + post_status delete_status)a + plug( - Pleroma.Plugs.RateLimitPlug, - %{ - max_requests: Config.get([:app_account_creation, :max_requests]), - interval: Config.get([:app_account_creation, :interval]) - } - when action in [:account_register] + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} + when action in ~w(reblog_status unreblog_status)a + ) + + plug( + RateLimiter, + {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} + when action in ~w(fav_status unfav_status)a ) + plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions) + plug(RateLimiter, :app_account_creation when action == :account_register) + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + @local_mastodon_name "Mastodon-Local" action_fallback(:errors) @@ -117,13 +127,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> Enum.dedup() info_params = - [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role] + [ + :no_rich_text, + :locked, + :hide_followers, + :hide_follows, + :hide_favorites, + :show_role, + :skip_thread_containment + ] |> Enum.reduce(%{}, fn key, acc -> add_if_present(acc, params, to_string(key), key, fn value -> {:ok, ControllerHelper.truthy_param?(value)} end) end) |> add_if_present(params, "default_scope", :default_scope) + |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> + {:ok, Map.merge(user.info.pleroma_settings_store, value)} + end) |> add_if_present(params, "header", :banner, fn value -> with %Plug.Upload{} <- value, {:ok, object} <- ActivityPub.upload(value, type: :banner) do @@ -132,6 +153,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do _ -> :error end end) + |> add_if_present(params, "pleroma_background_image", :background, fn value -> + with %Plug.Upload{} <- value, + {:ok, object} <- ActivityPub.upload(value, type: :background) do + {:ok, object.data} + else + _ -> :error + end + end) |> Map.put(:emoji, user_info_emojis) info_cng = User.Info.profile_update(user.info, info_params) @@ -143,17 +172,89 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do CommonAPI.update(user) end - json(conn, AccountView.render("account.json", %{user: user, for: user})) + json( + conn, + AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) + ) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Invalid request"}) + _e -> render_error(conn, :forbidden, "Invalid request") + end + end + + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + + def update_avatar(%{assigns: %{user: user}} = conn, params) do + {:ok, object} = ActivityPub.upload(params, type: :avatar) + change = Changeset.change(user, %{avatar: object.data}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + + json(conn, %{url: nil}) + end + end + + def update_banner(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), + new_info <- %{"banner" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + json(conn, %{url: nil}) + end + end + + def update_background(%{assigns: %{user: user}} = conn, params) do + with {:ok, object} <- ActivityPub.upload(params, type: :background), + new_info <- %{"background" => object.data}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + %{"url" => [%{"href" => href} | _]} = object.data + + json(conn, %{url: href}) end end def verify_credentials(%{assigns: %{user: user}} = conn, _) do - account = AccountView.render("account.json", %{user: user, for: user}) + chat_token = Phoenix.Token.sign(conn, "user socket", user.id) + + account = + AccountView.render("account.json", %{ + user: user, + for: user, + with_pleroma_settings: true, + with_chat_token: chat_token + }) + json(conn, account) end @@ -171,10 +272,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do account = AccountView.render("account.json", %{user: user, for: for_user}) json(conn, account) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Can't find user"}) + _e -> render_error(conn, :not_found, "Can't find user") end end @@ -197,7 +295,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do languages: ["en"], registrations: Pleroma.Config.get([:instance, :registrations_open]), # Extra (not present in Mastodon): - max_toot_chars: Keyword.get(instance, :limit) + max_toot_chars: Keyword.get(instance, :limit), + poll_limits: Keyword.get(instance, :poll_limits) } json(conn, response) @@ -217,7 +316,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do "static_url" => url, "visible_in_picker" => true, "url" => url, - "tags" => tags + "tags" => tags, + # Assuming that a comma is authorized in the category name + "category" => (tags -- ["Custom"]) |> Enum.join(",") } end) end @@ -331,6 +432,10 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do + params = + params + |> Map.put("tag", params["tagged"]) + activities = ActivityPub.fetch_user_activities(user, reading_user, params) conn @@ -409,6 +514,56 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do + with %Object{} = object <- Object.get_by_id(id), + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> render_error(conn, :not_found, "Record not found") + false -> render_error(conn, :not_found, "Record not found") + end + end + + defp get_cached_vote_or_vote(user, object, choices) do + idempotency_key = "polls:#{user.id}:#{object.data["id"]}" + + {_, res} = + Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> + case CommonAPI.vote(user, object, choices) do + {:error, _message} = res -> {:ignore, res} + res -> {:commit, res} + end + end) + + res + end + + def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do + with %Object{} = object <- Object.get_by_id(id), + true <- object.data["type"] == "Question", + %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), + true <- Visibility.visible_for_user?(activity, user), + {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do + conn + |> put_view(StatusView) + |> try_render("poll.json", %{object: object, for: user}) + else + nil -> + render_error(conn, :not_found, "Record not found") + + false -> + render_error(conn, :not_found, "Record not found") + + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) + end + end + def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do conn @@ -458,26 +613,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params) - when length(media_ids) > 0 do - params = - params - |> Map.put("status", ".") - - post_status(conn, params) - end - def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do params = params |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) - idempotency_key = - case get_req_header(conn, "idempotency-key") do - [key] -> key - _ -> Ecto.UUID.generate() - end - scheduled_at = params["scheduled_at"] if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do @@ -490,14 +630,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else params = Map.drop(params, ["scheduled_at"]) - {:ok, activity} = - Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> - CommonAPI.post(user, params) - end) + case CommonAPI.post(user, params) do + {:error, message} -> + conn + |> put_status(:unprocessable_entity) + |> json(%{error: message}) - conn - |> put_view(StatusView) - |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + {:ok, activity} -> + conn + |> put_view(StatusView) + |> try_render("status.json", %{activity: activity, for: user, as: :activity}) + end end end @@ -505,10 +648,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do json(conn, %{}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Can't delete this post"}) + _e -> render_error(conn, :forbidden, "Can't delete this post") end end @@ -553,11 +693,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) end end @@ -598,11 +733,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_view(StatusView) |> try_render("status.json", %{activity: activity, for: user, as: :activity}) - else - {:error, reason} -> - conn - |> put_resp_content_type("application/json") - |> send_resp(:bad_request, Jason.encode!(%{"error" => reason})) end end @@ -633,8 +763,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -649,8 +779,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, reason} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => reason})) + |> put_status(:forbidden) + |> json(%{"error" => reason}) end end @@ -728,9 +858,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> json(rendered) else - conn - |> put_resp_content_type("application/json") - |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"})) + render_error(conn, :unsupported_media_type, "mascots can only be images") end end end @@ -743,21 +871,21 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"likes" => likes}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^likes) users = Repo.all(q) conn |> put_view(AccountView) - |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user}) + |> render("accounts.json", %{for: user, users: users, as: :user}) else _ -> json(conn, []) end end def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do - with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id), + with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id), %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do q = from(u in User, where: u.ap_id in ^announces) users = Repo.all(q) @@ -859,8 +987,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -873,8 +1001,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -891,8 +1019,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -909,8 +1037,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -930,17 +1058,22 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end - def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do + def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do + notifications = + if Map.has_key?(params, "notifications"), + do: params["notifications"] in [true, "True", "true", "1"], + else: true + with %User{} = muted <- User.get_cached_by_id(id), - {:ok, muter} <- User.mute(muter, muted) do + {:ok, muter} <- User.mute(muter, muted, notifications) do conn |> put_view(AccountView) |> render("relationship.json", %{user: muter, target: muted}) else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -953,8 +1086,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -975,8 +1108,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -990,8 +1123,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1025,8 +1158,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) + |> put_status(:forbidden) + |> json(%{error: message}) end end @@ -1039,117 +1172,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, message} -> conn - |> put_resp_content_type("application/json") - |> send_resp(403, Jason.encode!(%{"error" => message})) - end - end - - def status_search_query_with_gin(q, query) do - from([a, o] in q, - where: - fragment( - "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", - o.data, - ^query - ), - order_by: [desc: :id] - ) - end - - def status_search_query_with_rum(q, query) do - from([a, o] in q, - where: - fragment( - "? @@ plainto_tsquery('english', ?)", - o.fts_content, - ^query - ), - order_by: [fragment("? <=> now()::date", o.inserted_at)] - ) - end - - def status_search(user, query) do - fetched = - if Regex.match?(~r/https?:/, query) do - with {:ok, object} <- Fetcher.fetch_object_from_id(query), - %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), - true <- Visibility.visible_for_user?(activity, user) do - [activity] - else - _e -> [] - end - end || [] - - q = - from([a, o] in Activity.with_preloaded_object(Activity), - where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, - limit: 40 - ) - - q = - if Pleroma.Config.get([:database, :rum_enabled]) do - status_search_query_with_rum(q, query) - else - status_search_query_with_gin(q, query) - end - - Repo.all(q) ++ fetched - end - - def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags_path = Web.base_url() <> "/tag/" - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - statuses = status_search(user, query) - - tags = - query - |> String.split() - |> Enum.uniq() - |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) - |> Enum.map(fn tag -> String.slice(tag, 1..-1) end) - - res = %{ - "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user), - "statuses" => - StatusView.render("index.json", activities: statuses, for: user, as: :activity), - "hashtags" => tags - } - - json(conn, res) - end - - def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do - accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user) - - res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) - - json(conn, res) + |> put_status(:forbidden) + |> json(%{error: message}) + end end def favourites(%{assigns: %{user: user}} = conn, params) do @@ -1196,13 +1221,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: for_user, as: :activity}) else - nil -> - {:error, :not_found} - - true -> - conn - |> put_status(403) - |> json(%{error: "Can't get favorites"}) + nil -> {:error, :not_found} + true -> render_error(conn, :forbidden, "Can't get favorites") end end @@ -1234,10 +1254,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do res = ListView.render("list.json", list: list) json(conn, res) else - _e -> - conn - |> put_status(404) - |> json(%{error: "Record not found"}) + _e -> render_error(conn, :not_found, "Record not found") end end @@ -1253,7 +1270,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, %{}) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1304,7 +1321,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do json(conn, res) else _e -> - json(conn, "error") + json(conn, dgettext("errors", "error")) end end @@ -1328,10 +1345,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> put_view(StatusView) |> render("index.json", %{activities: activities, for: user, as: :activity}) else - _e -> - conn - |> put_status(403) - |> json(%{error: "Error."}) + _e -> render_error(conn, :forbidden, "Error.") end end @@ -1346,8 +1360,6 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) - flavour = get_user_flavour(user) - initial_state = %{ meta: %{ @@ -1366,6 +1378,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do max_toot_chars: limit, mascot: User.get_mascot(user)["url"] }, + poll_limits: Config.get([:instance, :poll_limits]), rights: %{ delete_others_notice: present?(user.info.is_moderator), admin: present?(user.info.is_admin) @@ -1433,7 +1446,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do conn |> put_layout(false) |> put_view(MastodonView) - |> render("index.html", %{initial_state: initial_state, flavour: flavour}) + |> render("index.html", %{initial_state: initial_state}) else conn |> put_session(:return_to, conn.request_path) @@ -1451,48 +1464,11 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else e -> conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) + |> put_status(:internal_server_error) + |> json(%{error: inspect(e)}) end end - @supported_flavours ["glitch", "vanilla"] - - def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params) - when flavour in @supported_flavours do - flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour) - - with changeset <- Ecto.Changeset.change(user), - changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng), - {:ok, user} <- User.update_and_set_cache(changeset), - flavour <- user.info.flavour do - json(conn, flavour) - else - e -> - conn - |> put_resp_content_type("application/json") - |> send_resp(500, Jason.encode!(%{"error" => inspect(e)})) - end - end - - def set_flavour(conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Unsupported flavour"}) - end - - def get_flavour(%{assigns: %{user: user}} = conn, _params) do - json(conn, get_user_flavour(user)) - end - - defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do - flavour - end - - defp get_user_flavour(_) do - "glitch" - end - def login(%{assigns: %{user: %User{}}} = conn, _params) do redirect(conn, to: local_mastodon_root_path(conn)) end @@ -1657,20 +1633,24 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do |> Enum.map_join(", ", fn {_k, v} -> v end) conn - |> put_status(422) + |> put_status(:unprocessable_entity) |> json(%{error: error_message}) end def errors(conn, {:error, :not_found}) do + render_error(conn, :not_found, "Record not found") + end + + def errors(conn, {:error, error_message}) do conn - |> put_status(404) - |> json(%{error: "Record not found"}) + |> put_status(:bad_request) + |> json(%{error: error_message}) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end def suggestions(%{assigns: %{user: user}} = conn, _) do @@ -1790,21 +1770,17 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do else {:error, errors} -> conn - |> put_status(400) - |> json(Jason.encode!(errors)) + |> put_status(:bad_request) + |> json(errors) end end def account_register(%{assigns: %{app: _app}} = conn, _params) do - conn - |> put_status(400) - |> json(%{error: "Missing parameters"}) + render_error(conn, :bad_request, "Missing parameters") end def account_register(conn, _) do - conn - |> put_status(403) - |> json(%{error: "Invalid credentials"}) + render_error(conn, :forbidden, "Invalid credentials") end def conversations(%{assigns: %{user: user}} = conn, params) do @@ -1834,21 +1810,14 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do def try_render(conn, target, params) when is_binary(target) do - res = render(conn, target, params) - - if res == nil do - conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) - else - res + case render(conn, target, params) do + nil -> render_error(conn, :not_implemented, "Can't display this activity") + res -> res end end def try_render(conn, _, _) do - conn - |> put_status(501) - |> json(%{error: "Can't display this activity"}) + render_error(conn, :not_implemented, "Can't display this activity") end defp present?(nil), do: false diff --git a/lib/pleroma/web/mastodon_api/search_controller.ex b/lib/pleroma/web/mastodon_api/search_controller.ex new file mode 100644 index 000000000..9072aa7a4 --- /dev/null +++ b/lib/pleroma/web/mastodon_api/search_controller.ex @@ -0,0 +1,120 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.SearchController do + use Pleroma.Web, :controller + + alias Pleroma.Activity + 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 + + require Logger + plug(RateLimiter, :search when action in [:search, :search2, :account_search]) + + def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do + accounts = User.search(query, search_options(params, user)) + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + + json(conn, res) + end + + def search2(conn, params), do: do_search(:v2, conn, params) + def search(conn, params), do: do_search(:v1, conn, params) + + defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = params) do + options = search_options(params, user) + timeout = Keyword.get(Repo.config(), :timeout, 15_000) + default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} + + result = + default_values + |> Enum.map(fn {resource, default_value} -> + if params["type"] == nil or params["type"] == resource do + {resource, fn -> resource_search(version, resource, query, options) end} + else + {resource, fn -> default_value end} + end + end) + |> Task.async_stream(fn {resource, f} -> {resource, with_fallback(f)} end, + timeout: timeout, + on_timeout: :kill_task + ) + |> Enum.reduce(default_values, fn + {:ok, {resource, result}}, acc -> + Map.put(acc, resource, result) + + _error, acc -> + acc + end) + + json(conn, result) + end + + defp search_options(params, user) do + [ + resolve: params["resolve"] == "true", + following: params["following"] == "true", + limit: ControllerHelper.fetch_integer_param(params, "limit"), + offset: ControllerHelper.fetch_integer_param(params, "offset"), + type: params["type"], + author: get_author(params), + for_user: user + ] + |> Enum.filter(&elem(&1, 1)) + end + + defp resource_search(_, "accounts", query, options) do + accounts = with_fallback(fn -> User.search(query, options) end) + AccountView.render("accounts.json", users: accounts, for: options[:for_user], as: :user) + 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) + end + + defp resource_search(:v2, "hashtags", query, _options) do + tags_path = Web.base_url() <> "/tag/" + + query + |> prepare_tags() + |> Enum.map(fn tag -> + tag = String.trim_leading(tag, "#") + %{name: tag, url: tags_path <> tag} + end) + end + + defp resource_search(:v1, "hashtags", query, _options) do + query + |> prepare_tags() + |> Enum.map(fn tag -> String.trim_leading(tag, "#") end) + end + + defp prepare_tags(query) do + query + |> String.split() + |> Enum.uniq() + |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end) + end + + defp with_fallback(f, fallback \\ []) do + try do + f.() + rescue + error -> + Logger.error("#{__MODULE__} search error: #{inspect(error)}") + fallback + end + end + + defp get_author(%{"account_id" => account_id}) when is_binary(account_id), + do: User.get_cached_by_id(account_id) + + defp get_author(_params), do: nil +end diff --git a/lib/pleroma/web/mastodon_api/subscription_controller.ex b/lib/pleroma/web/mastodon_api/subscription_controller.ex index b6c8ff808..255ee2f18 100644 --- a/lib/pleroma/web/mastodon_api/subscription_controller.ex +++ b/lib/pleroma/web/mastodon_api/subscription_controller.ex @@ -59,13 +59,13 @@ defmodule Pleroma.Web.MastodonAPI.SubscriptionController do # def errors(conn, {:error, :not_found}) do conn - |> put_status(404) - |> json("Not found") + |> put_status(:not_found) + |> json(dgettext("errors", "Not found")) end def errors(conn, _) do conn - |> put_status(500) - |> json("Something went wrong") + |> put_status(:internal_server_error) + |> json(dgettext("errors", "Something went wrong")) end end diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b82d3319b..65bab4062 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -52,7 +52,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do followed_by: User.following?(target, user), blocking: User.blocks?(user, target), muting: User.mutes?(user, target), - muting_notifications: false, + muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), requested: requested, domain_blocking: false, @@ -66,6 +66,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end defp do_render("account.json", %{user: user} = opts) do + display_name = HTML.strip_tags(user.name || user.nickname) + image = User.avatar_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url() user_info = User.get_cached_user_info(user) @@ -96,7 +98,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do id: to_string(user.id), username: username_from_nickname(user.nickname), acct: user.nickname, - display_name: user.name || user.nickname, + display_name: display_name, locked: user_info.locked, created_at: Utils.to_masto_date(user.inserted_at), followers_count: user_info.follower_count, @@ -124,12 +126,16 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do hide_followers: user.info.hide_followers, hide_follows: user.info.hide_follows, hide_favorites: user.info.hide_favorites, - relationship: relationship + relationship: relationship, + skip_thread_containment: user.info.skip_thread_containment, + background_image: image_url(user.info.background) |> MediaProxy.url() } } |> maybe_put_role(user, opts[:for]) |> maybe_put_settings(user, opts[:for], user_info) |> maybe_put_notification_settings(user, opts[:for]) + |> maybe_put_settings_store(user, opts[:for], opts) + |> maybe_put_chat_token(user, opts[:for], opts) end defp username_from_nickname(string) when is_binary(string) do @@ -152,6 +158,24 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do defp maybe_put_settings(data, _, _, _), do: data + defp maybe_put_settings_store(data, %User{info: info, id: id}, %User{id: id}, %{ + with_pleroma_settings: true + }) do + data + |> Kernel.put_in([:pleroma, :settings_store], info.pleroma_settings_store) + end + + defp maybe_put_settings_store(data, _, _, _), do: data + + defp maybe_put_chat_token(data, %User{id: id}, %User{id: id}, %{ + with_chat_token: token + }) do + data + |> Kernel.put_in([:pleroma, :chat_token], token) + end + + defp maybe_put_chat_token(data, _, _, _), do: data + defp maybe_put_role(data, %User{info: %{show_role: true}} = user, _) do data |> Kernel.put_in([:pleroma, :is_admin], user.info.is_admin) @@ -171,4 +195,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do end defp maybe_put_notification_settings(data, _, _), do: data + + defp image_url(%{"url" => [%{"href" => href} | _]}), do: href + defp image_url(_), do: nil end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 8e8f7cf31..38bdec737 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.MastodonAPI.ConversationView do use Pleroma.Web, :view @@ -22,9 +26,14 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do last_status = StatusView.render("status.json", %{activity: activity, for: user}) + # Conversations return all users except the current user. + users = + participation.conversation.users + |> Enum.reject(&(&1.id == user.id)) + accounts = AccountView.render("accounts.json", %{ - users: participation.conversation.users, + users: users, as: :user }) diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index e55f9b96e..06a7251d8 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -19,6 +19,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do import Pleroma.Web.ActivityPub.Visibility, only: [get_visibility: 1] # TODO: Add cached version. + defp get_replied_to_activities([]), do: %{} + defp get_replied_to_activities(activities) do activities |> Enum.map(fn @@ -104,7 +106,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do id: to_string(activity.id), uri: activity_object.data["id"], url: activity_object.data["id"], - account: AccountView.render("account.json", %{user: user}), + account: AccountView.render("account.json", %{user: user, for: opts[:for]}), in_reply_to_id: nil, in_reply_to_account_id: nil, reblog: reblogged, @@ -147,8 +149,14 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do tags = object.data["tag"] || [] sensitive = object.data["sensitive"] || Enum.member?(tags, "nsfw") + tag_mentions = + tags + |> Enum.filter(fn tag -> is_map(tag) and tag["type"] == "Mention" end) + |> Enum.map(fn tag -> tag["href"] end) + mentions = - activity.recipients + (object.data["to"] ++ tag_mentions) + |> Enum.uniq() |> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end) |> Enum.filter(& &1) |> Enum.map(fn user -> AccountView.render("mention.json", %{user: user}) end) @@ -221,7 +229,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do id: to_string(activity.id), uri: object.data["id"], url: url, - account: AccountView.render("account.json", %{user: user}), + account: AccountView.render("account.json", %{user: user, for: opts[:for]}), 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), reblog: nil, @@ -240,6 +248,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do spoiler_text: summary_html, visibility: get_visibility(object), media_attachments: attachments, + poll: render("poll.json", %{object: object, for: opts[:for]}), mentions: mentions, tags: build_tags(tags), application: %{ @@ -290,8 +299,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do provider_url: page_url_data.scheme <> "://" <> page_url_data.host, url: page_url, image: image_url |> MediaProxy.url(), - title: rich_media[:title], - description: rich_media[:description], + title: rich_media[:title] || "", + description: rich_media[:description] || "", pleroma: %{ opengraph: rich_media } @@ -329,6 +338,64 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do } end + def render("poll.json", %{object: object} = opts) do + {multiple, options} = + case object.data do + %{"anyOf" => options} when is_list(options) -> {true, options} + %{"oneOf" => options} when is_list(options) -> {false, options} + _ -> {nil, nil} + end + + if options do + end_time = + (object.data["closed"] || object.data["endTime"]) + |> NaiveDateTime.from_iso8601!() + + expired = + end_time + |> NaiveDateTime.compare(NaiveDateTime.utc_now()) + |> case do + :lt -> true + _ -> false + end + + voted = + if opts[:for] do + existing_votes = + Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) + + existing_votes != [] or opts[:for].ap_id == object.data["actor"] + else + false + end + + {options, votes_count} = + Enum.map_reduce(options, 0, fn %{"name" => name} = option, count -> + current_count = option["replies"]["totalItems"] || 0 + + {%{ + title: HTML.strip_tags(name), + votes_count: current_count + }, current_count + count} + end) + + %{ + # Mastodon uses separate ids for polls, but an object can't have + # more than one poll embedded so object id is fine + id: object.id, + expires_at: Utils.to_masto_date(end_time), + expired: expired, + multiple: multiple, + votes_count: votes_count, + options: options, + voted: voted, + emojis: build_emojis(object.data["emoji"]) + } + else + nil + end + end + def get_reply_to(activity, %{replied_to_activities: replied_to_activities}) do object = Object.normalize(activity) diff --git a/lib/pleroma/web/mastodon_api/websocket_handler.ex b/lib/pleroma/web/mastodon_api/websocket_handler.ex index abfa26754..dbd3542ea 100644 --- a/lib/pleroma/web/mastodon_api/websocket_handler.ex +++ b/lib/pleroma/web/mastodon_api/websocket_handler.ex @@ -17,6 +17,7 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do "public:media", "public:local:media", "user", + "user:notification", "direct", "list", "hashtag" @@ -28,9 +29,10 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do def init(%{qs: qs} = req, state) do with params <- :cow_qs.parse_qs(qs), + sec_websocket <- :cowboy_req.header("sec-websocket-protocol", req, nil), access_token <- List.keyfind(params, "access_token", 0), {_, stream} <- List.keyfind(params, "stream", 0), - {:ok, user} <- allow_request(stream, access_token), + {:ok, user} <- allow_request(stream, [access_token, sec_websocket]), topic when is_binary(topic) <- expand_topic(stream, params) do {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}} else @@ -83,13 +85,21 @@ defmodule Pleroma.Web.MastodonAPI.WebsocketHandler do end # Public streams without authentication. - defp allow_request(stream, nil) when stream in @anonymous_streams do + defp allow_request(stream, [nil, nil]) when stream in @anonymous_streams do {:ok, nil} end # Authenticated streams. - defp allow_request(stream, {"access_token", access_token}) when stream in @streams do - with %Token{user_id: user_id} <- Repo.get_by(Token, token: access_token), + defp allow_request(stream, [access_token, sec_websocket]) when stream in @streams do + token = + with {"access_token", token} <- access_token do + token + else + _ -> sec_websocket + end + + with true <- is_bitstring(token), + %Token{user_id: user_id} <- Repo.get_by(Token, token: token), user = %User{} <- User.get_cached_by_id(user_id) do {:ok, user} else diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index 5762e767b..a661e9bb7 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -3,87 +3,71 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.MediaProxy do - @base64_opts [padding: false] - - def url(nil), do: nil + alias Pleroma.Config + alias Pleroma.Web - def url(""), do: nil + @base64_opts [padding: false] + def url(url) when is_nil(url) or url == "", do: nil def url("/" <> _ = url), do: url def url(url) do - config = Application.get_env(:pleroma, :media_proxy, []) - domain = URI.parse(url).host + if disabled?() or local?(url) or whitelisted?(url) do + url + else + encode_url(url) + end + end - cond do - !Keyword.get(config, :enabled, false) or String.starts_with?(url, Pleroma.Web.base_url()) -> - url + defp disabled?, do: !Config.get([:media_proxy, :enabled], false) - Enum.any?(Pleroma.Config.get([:media_proxy, :whitelist]), fn pattern -> - String.equivalent?(domain, pattern) - end) -> - url + defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) - true -> - encode_url(url) - end + defp whitelisted?(url) do + %{host: domain} = URI.parse(url) + + Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern -> + String.equivalent?(domain, pattern) + end) end def encode_url(url) do - secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] - - # Must preserve `%2F` for compatibility with S3 - # https://git.pleroma.social/pleroma/pleroma/issues/580 - replacement = get_replacement(url, ":2F:") + base64 = Base.url_encode64(url, @base64_opts) - # The URL is url-decoded and encoded again to ensure it is correctly encoded and not twice. - base64 = - url - |> String.replace("%2F", replacement) - |> URI.decode() - |> URI.encode() - |> String.replace(replacement, "%2F") + sig64 = + base64 + |> signed_url |> Base.url_encode64(@base64_opts) - sig = :crypto.hmac(:sha, secret, base64) - sig64 = sig |> Base.url_encode64(@base64_opts) - build_url(sig64, base64, filename(url)) end def decode_url(sig, url) do - secret = Application.get_env(:pleroma, Pleroma.Web.Endpoint)[:secret_key_base] - sig = Base.url_decode64!(sig, @base64_opts) - local_sig = :crypto.hmac(:sha, secret, url) - - if local_sig == sig do + with {:ok, sig} <- Base.url_decode64(sig, @base64_opts), + signature when signature == sig <- signed_url(url) do {:ok, Base.url_decode64!(url, @base64_opts)} else - {:error, :invalid_signature} + _ -> {:error, :invalid_signature} end end + defp signed_url(url) do + :crypto.hmac(:sha, Config.get([Web.Endpoint, :secret_key_base]), url) + end + def filename(url_or_path) do if path = URI.parse(url_or_path).path, do: Path.basename(path) end def build_url(sig_base64, url_base64, filename \\ nil) do [ - Pleroma.Config.get([:media_proxy, :base_url], Pleroma.Web.base_url()), + Pleroma.Config.get([:media_proxy, :base_url], Web.base_url()), "proxy", sig_base64, url_base64, filename ] - |> Enum.filter(fn value -> value end) + |> Enum.filter(& &1) |> Path.join() end - - defp get_replacement(url, replacement) do - if String.contains?(url, replacement) do - get_replacement(url, replacement <> replacement) - else - replacement - end - end end diff --git a/lib/pleroma/web/media_proxy/controller.ex b/lib/pleroma/web/media_proxy/media_proxy_controller.ex index c0552d89f..1e9520d46 100644 --- a/lib/pleroma/web/media_proxy/controller.ex +++ b/lib/pleroma/web/media_proxy/media_proxy_controller.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do with config <- Pleroma.Config.get([:media_proxy], []), true <- Keyword.get(config, :enabled, false), {:ok, url} <- MediaProxy.decode_url(sig64, url64), - :ok <- filename_matches(Map.has_key?(params, "filename"), conn.request_path, url) do + :ok <- filename_matches(params, conn.request_path, url) do ReverseProxy.call(conn, url, Keyword.get(config, :proxy_opts, @default_proxy_opts)) else false -> @@ -27,18 +27,15 @@ defmodule Pleroma.Web.MediaProxy.MediaProxyController do end end - def filename_matches(has_filename, path, url) do - filename = - url - |> MediaProxy.filename() - |> URI.decode() + def filename_matches(%{"filename" => _} = _, path, url) do + filename = MediaProxy.filename(url) - path = URI.decode(path) - - if has_filename && filename && Path.basename(path) != filename do + if filename && Path.basename(path) != filename do {:wrong_filename, filename} else :ok end end + + def filename_matches(_, _, _), do: :ok end diff --git a/lib/pleroma/web/metadata/opengraph.ex b/lib/pleroma/web/metadata/opengraph.ex index 357b80a2d..e7fa7f408 100644 --- a/lib/pleroma/web/metadata/opengraph.ex +++ b/lib/pleroma/web/metadata/opengraph.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider def build_tags(%{ @@ -81,26 +82,19 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. # Also, Whatsapp only wants JPEG or PNGs. It seems that if we add a second og:image # object when a Video or GIF is attached it will display that in Whatsapp Rich Preview. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:audio", content: Utils.attachment_url(url["href"])], []} | acc ] "image" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []}, + {:meta, [property: "og:image", content: Utils.attachment_url(url["href"])], []}, {:meta, [property: "og:image:width", content: 150], []}, {:meta, [property: "og:image:height", content: 150], []} | acc @@ -108,8 +102,7 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do "video" -> [ - {:meta, - [property: "og:" <> media_type, content: Utils.attachment_url(url["href"])], []} + {:meta, [property: "og:video", content: Utils.attachment_url(url["href"])], []} | acc ] @@ -121,4 +114,6 @@ defmodule Pleroma.Web.Metadata.Providers.OpenGraph do acc ++ rendered_tags end) end + + defp build_attachments(_), do: [] end diff --git a/lib/pleroma/web/metadata/player_view.ex b/lib/pleroma/web/metadata/player_view.ex index e9a8cfc8d..4289ebdbd 100644 --- a/lib/pleroma/web/metadata/player_view.ex +++ b/lib/pleroma/web/metadata/player_view.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Metadata.PlayerView do use Pleroma.Web, :view import Phoenix.HTML.Tag, only: [content_tag: 3, tag: 2] diff --git a/lib/pleroma/web/metadata/rel_me.ex b/lib/pleroma/web/metadata/rel_me.ex index 03af899c4..f87fc1973 100644 --- a/lib/pleroma/web/metadata/rel_me.ex +++ b/lib/pleroma/web/metadata/rel_me.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.Metadata.Providers.RelMe do alias Pleroma.Web.Metadata.Providers.Provider @behaviour Provider diff --git a/lib/pleroma/web/metadata/twitter_card.ex b/lib/pleroma/web/metadata/twitter_card.ex index 040b872e7..d6a6049b3 100644 --- a/lib/pleroma/web/metadata/twitter_card.ex +++ b/lib/pleroma/web/metadata/twitter_card.ex @@ -1,4 +1,5 @@ # Pleroma: A lightweight social networking server + # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> # SPDX-License-Identifier: AGPL-3.0-only @@ -9,13 +10,10 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do alias Pleroma.Web.Metadata.Utils @behaviour Provider + @media_types ["image", "audio", "video"] @impl Provider - def build_tags(%{ - activity_id: id, - object: object, - user: user - }) do + def build_tags(%{activity_id: id, object: object, user: user}) do attachments = build_attachments(id, object) scrubbed_content = Utils.scrub_html_and_truncate(object) # Zero width space @@ -27,21 +25,12 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, - {:meta, - [ - property: "twitter:description", - content: content - ], []} + title_tag(user), + {:meta, [property: "twitter:description", content: content], []} ] ++ if attachments == [] or Metadata.activity_nsfw?(object) do [ - {:meta, - [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary_large_image"], []} ] else @@ -53,30 +42,28 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do def build_tags(%{user: user}) do with truncated_bio = Utils.scrub_html_and_truncate(user.bio || "") do [ - {:meta, - [ - property: "twitter:title", - content: Utils.user_name_string(user) - ], []}, + title_tag(user), {:meta, [property: "twitter:description", content: truncated_bio], []}, - {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], - []}, + image_tag(user), {:meta, [property: "twitter:card", content: "summary"], []} ] end end + defp title_tag(user) do + {:meta, [property: "twitter:title", content: Utils.user_name_string(user)], []} + end + + def image_tag(user) do + {:meta, [property: "twitter:image", content: Utils.attachment_url(User.avatar_url(user))], []} + end + defp build_attachments(id, %{data: %{"attachment" => attachments}}) do Enum.reduce(attachments, [], fn attachment, acc -> rendered_tags = Enum.reduce(attachment["url"], [], fn url, acc -> - media_type = - Enum.find(["image", "audio", "video"], fn media_type -> - String.starts_with?(url["mediaType"], media_type) - end) - # TODO: Add additional properties to objects when we have the data available. - case media_type do + case Utils.fetch_media_type(@media_types, url["mediaType"]) do "audio" -> [ {:meta, [property: "twitter:card", content: "player"], []}, @@ -117,6 +104,8 @@ defmodule Pleroma.Web.Metadata.Providers.TwitterCard do end) end + defp build_attachments(_id, _object), do: [] + defp player_url(id) do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice_player, id) end diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex index 58385a3d1..720bd4519 100644 --- a/lib/pleroma/web/metadata/utils.ex +++ b/lib/pleroma/web/metadata/utils.ex @@ -39,4 +39,11 @@ defmodule Pleroma.Web.Metadata.Utils do "(@#{user.nickname})" end end + + @spec fetch_media_type(list(String.t()), String.t()) :: String.t() | nil + def fetch_media_type(supported_types, media_type) do + Enum.find(supported_types, fn support_type -> + String.starts_with?(media_type, support_type) + end) + end end diff --git a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex index 489d5d3a5..b786a521b 100644 --- a/lib/pleroma/web/mongooseim/mongoose_im_controller.ex +++ b/lib/pleroma/web/mongooseim/mongoose_im_controller.ex @@ -29,7 +29,7 @@ defmodule Pleroma.Web.MongooseIM.MongooseIMController do else false -> conn - |> put_status(403) + |> put_status(:forbidden) |> json(false) _ -> diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 45f90c579..a1d7fcc7d 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -32,20 +32,18 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do # returns a nodeinfo 2.0 map, since 2.1 just adds a repository field # under software. def raw_nodeinfo do - instance = Application.get_env(:pleroma, :instance) - media_proxy = Application.get_env(:pleroma, :media_proxy) - suggestions = Application.get_env(:pleroma, :suggestions) - chat = Application.get_env(:pleroma, :chat) - gopher = Application.get_env(:pleroma, :gopher) stats = Stats.get_stats() + exclusions = Config.get([:instance, :mrf_transparency_exclusions]) + mrf_simple = - Application.get_env(:pleroma, :mrf_simple) + Config.get(:mrf_simple) + |> Enum.map(fn {k, v} -> {k, Enum.reject(v, fn v -> v in exclusions end)} end) |> Enum.into(%{}) # This horror is needed to convert regex sigils to strings mrf_keyword = - Application.get_env(:pleroma, :mrf_keyword, []) + Config.get(:mrf_keyword, []) |> Enum.map(fn {key, value} -> {key, Enum.map(value, fn @@ -74,14 +72,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do MRF.get_policies() |> Enum.map(fn policy -> to_string(policy) |> String.split(".") |> List.last() end) - quarantined = Keyword.get(instance, :quarantined_instances) - - quarantined = - if is_list(quarantined) do - quarantined - else - [] - end + quarantined = Config.get([:instance, :quarantined_instances], []) staff_accounts = User.all_superusers() @@ -92,13 +83,14 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do |> Enum.into(%{}, fn {k, v} -> {k, length(v)} end) federation_response = - if Keyword.get(instance, :mrf_transparency) do + if Config.get([:instance, :mrf_transparency]) do %{ mrf_policies: mrf_policies, mrf_simple: mrf_simple, mrf_keyword: mrf_keyword, mrf_user_allowlist: mrf_user_allowlist, - quarantined_instances: quarantined + quarantined_instances: quarantined, + exclusions: length(exclusions) > 0 } else %{} @@ -109,22 +101,24 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do "pleroma_api", "mastodon_api", "mastodon_api_streaming", - if Keyword.get(media_proxy, :enabled) do + "polls", + "pleroma_explicit_addressing", + if Config.get([:media_proxy, :enabled]) do "media_proxy" end, - if Keyword.get(gopher, :enabled) do + if Config.get([:gopher, :enabled]) do "gopher" end, - if Keyword.get(chat, :enabled) do + if Config.get([:chat, :enabled]) do "chat" end, - if Keyword.get(suggestions, :enabled) do + if Config.get([:suggestions, :enabled]) do "suggestions" end, - if Keyword.get(instance, :allow_relay) do + if Config.get([:instance, :allow_relay]) do "relay" end, - if Keyword.get(instance, :safe_dm_mentions) do + if Config.get([:instance, :safe_dm_mentions]) do "safe_dm_mentions" end ] @@ -141,7 +135,7 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do inbound: [], outbound: [] }, - openRegistrations: Keyword.get(instance, :registrations_open), + openRegistrations: Config.get([:instance, :registrations_open]), usage: %{ users: %{ total: stats.user_count || 0 @@ -149,29 +143,31 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do localPosts: stats.status_count || 0 }, metadata: %{ - nodeName: Keyword.get(instance, :name), - nodeDescription: Keyword.get(instance, :description), - private: !Keyword.get(instance, :public, true), + nodeName: Config.get([:instance, :name]), + nodeDescription: Config.get([:instance, :description]), + private: !Config.get([:instance, :public], true), suggestions: %{ - enabled: Keyword.get(suggestions, :enabled, false), - thirdPartyEngine: Keyword.get(suggestions, :third_party_engine, ""), - timeout: Keyword.get(suggestions, :timeout, 5000), - limit: Keyword.get(suggestions, :limit, 23), - web: Keyword.get(suggestions, :web, "") + enabled: Config.get([:suggestions, :enabled], false), + thirdPartyEngine: Config.get([:suggestions, :third_party_engine], ""), + timeout: Config.get([:suggestions, :timeout], 5000), + limit: Config.get([:suggestions, :limit], 23), + web: Config.get([:suggestions, :web], "") }, staffAccounts: staff_accounts, federation: federation_response, - postFormats: Keyword.get(instance, :allowed_post_formats), + pollLimits: Config.get([:instance, :poll_limits]), + postFormats: Config.get([:instance, :allowed_post_formats]), uploadLimits: %{ - general: Keyword.get(instance, :upload_limit), - avatar: Keyword.get(instance, :avatar_upload_limit), - banner: Keyword.get(instance, :banner_upload_limit), - background: Keyword.get(instance, :background_upload_limit) + general: Config.get([:instance, :upload_limit]), + avatar: Config.get([:instance, :avatar_upload_limit]), + banner: Config.get([:instance, :banner_upload_limit]), + background: Config.get([:instance, :background_upload_limit]) }, - accountActivationRequired: Keyword.get(instance, :account_activation_required, false), - invitesEnabled: Keyword.get(instance, :invites_enabled, false), + accountActivationRequired: Config.get([:instance, :account_activation_required], false), + invitesEnabled: Config.get([:instance, :invites_enabled], false), features: features, - restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) + restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), + skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) } } end @@ -209,8 +205,6 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do end def nodeinfo(conn, _) do - conn - |> put_status(404) - |> json(%{error: "Nodeinfo schema version not handled"}) + render_error(conn, :not_found, "Nodeinfo schema version not handled") end end diff --git a/lib/pleroma/web/oauth/authorization.ex b/lib/pleroma/web/oauth/authorization.ex index 18973413e..d53e20d12 100644 --- a/lib/pleroma/web/oauth/authorization.ex +++ b/lib/pleroma/web/oauth/authorization.ex @@ -76,14 +76,16 @@ defmodule Pleroma.Web.OAuth.Authorization do def use_token(%Authorization{used: true}), do: {:error, "already used"} @spec delete_user_authorizations(User.t()) :: {integer(), any()} - def delete_user_authorizations(%User{id: user_id}) do - from( - a in Pleroma.Web.OAuth.Authorization, - where: a.user_id == ^user_id - ) + def delete_user_authorizations(%User{} = user) do + user + |> delete_by_user_query |> Repo.delete_all() end + def delete_by_user_query(%User{id: user_id}) do + from(a in __MODULE__, where: a.user_id == ^user_id) + end + @doc "gets auth for app by token" @spec get_by_token(App.t(), String.t()) :: {:ok, t()} | {:error, :not_found} def get_by_token(%App{id: app_id} = _app, token) do diff --git a/lib/pleroma/web/oauth/fallback_controller.ex b/lib/pleroma/web/oauth/fallback_controller.ex index e3984f009..dd7f08bf1 100644 --- a/lib/pleroma/web/oauth/fallback_controller.ex +++ b/lib/pleroma/web/oauth/fallback_controller.ex @@ -9,21 +9,24 @@ defmodule Pleroma.Web.OAuth.FallbackController do def call(conn, {:register, :generic_error}) do conn |> put_status(:internal_server_error) - |> put_flash(:error, "Unknown error, please check the details and try again.") + |> put_flash( + :error, + dgettext("errors", "Unknown error, please check the details and try again.") + ) |> OAuthController.registration_details(conn.params) end def call(conn, {:register, _error}) do conn |> put_status(:unauthorized) - |> put_flash(:error, "Invalid Username/Password") + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) |> OAuthController.registration_details(conn.params) end def call(conn, _error) do conn |> put_status(:unauthorized) - |> put_flash(:error, "Invalid Username/Password") + |> put_flash(:error, dgettext("errors", "Invalid Username/Password")) |> OAuthController.authorize(conn.params) end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ae2b80d95..ef53b7ae3 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do use Pleroma.Web, :controller + alias Pleroma.Helpers.UriHelper alias Pleroma.Registration alias Pleroma.Repo alias Pleroma.User @@ -17,6 +18,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.Token.Strategy.Revoke, as: RevokeToken alias Pleroma.Web.OAuth.Scopes + require Logger + if Pleroma.Config.oauth_consumer_enabled?(), do: plug(Ueberauth) plug(:fetch_session) @@ -24,34 +27,25 @@ defmodule Pleroma.Web.OAuth.OAuthController do action_fallback(Pleroma.Web.OAuth.FallbackController) + @oob_token_redirect_uri "urn:ietf:wg:oauth:2.0:oob" + # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg - def authorize(conn, %{"authorization" => _} = params) do + def authorize(%Plug.Conn{} = conn, %{"authorization" => _} = params) do {auth_attrs, params} = Map.pop(params, "authorization") authorize(conn, Map.merge(params, auth_attrs)) end - def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do + def authorize(%Plug.Conn{assigns: %{token: %Token{}}} = conn, params) do if ControllerHelper.truthy_param?(params["force_login"]) do do_authorize(conn, params) else - redirect_uri = - if is_binary(params["redirect_uri"]) do - params["redirect_uri"] - else - app = Repo.preload(token, :app).app - - app.redirect_uris - |> String.split() - |> Enum.at(0) - end - - redirect(conn, external: redirect_uri(conn, redirect_uri)) + handle_existing_authorization(conn, params) end end - def authorize(conn, params), do: do_authorize(conn, params) + def authorize(%Plug.Conn{} = conn, params), do: do_authorize(conn, params) - defp do_authorize(conn, params) do + defp do_authorize(%Plug.Conn{} = conn, params) do app = Repo.get_by(App, client_id: params["client_id"]) available_scopes = (app && app.scopes) || [] scopes = Scopes.fetch_scopes(params, available_scopes) @@ -68,8 +62,41 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) end + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{"redirect_uri" => @oob_token_redirect_uri} + ) do + render(conn, "oob_token_exists.html", %{token: token}) + end + + defp handle_existing_authorization( + %Plug.Conn{assigns: %{token: %Token{} = token}} = conn, + %{} = params + ) do + app = Repo.preload(token, :app).app + + redirect_uri = + if is_binary(params["redirect_uri"]) do + params["redirect_uri"] + else + default_redirect_uri(app) + end + + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{access_token: token.token} + url_params = UriHelper.append_param_if_present(url_params, :state, params["state"]) + url = UriHelper.append_uri_params(redirect_uri, url_params) + redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) + end + end + def create_authorization( - conn, + %Plug.Conn{} = conn, %{"authorization" => _} = params, opts \\ [] ) do @@ -81,35 +108,33 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def after_create_authorization(conn, auth, %{ - "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => @oob_token_redirect_uri} }) do - redirect_uri = redirect_uri(conn, redirect_uri) - - if redirect_uri == "urn:ietf:wg:oauth:2.0:oob" do - render(conn, "results.html", %{ - auth: auth - }) - else - connector = if String.contains?(redirect_uri, "?"), do: "&", else: "?" - url = "#{redirect_uri}#{connector}" - url_params = %{:code => auth.token} - - url_params = - if auth_attrs["state"] do - Map.put(url_params, :state, auth_attrs["state"]) - else - url_params - end - - url = "#{url}#{Plug.Conn.Query.encode(url_params)}" + render(conn, "oob_authorization_created.html", %{auth: auth}) + end + def after_create_authorization(%Plug.Conn{} = conn, %Authorization{} = auth, %{ + "authorization" => %{"redirect_uri" => redirect_uri} = auth_attrs + }) do + app = Repo.preload(auth, :app).app + + # An extra safety measure before we redirect (also done in `do_create_authorization/2`) + if redirect_uri in String.split(app.redirect_uris) do + redirect_uri = redirect_uri(conn, redirect_uri) + url_params = %{code: auth.token} + url_params = UriHelper.append_param_if_present(url_params, :state, auth_attrs["state"]) + url = UriHelper.append_uri_params(redirect_uri, url_params) redirect(conn, external: url) + else + conn + |> put_flash(:error, dgettext("errors", "Unlisted redirect_uri.")) + |> redirect(external: redirect_uri(conn, redirect_uri)) end end defp handle_create_authorization_error( - conn, + %Plug.Conn{} = conn, {:error, scopes_issue}, %{"authorization" => _} = params ) @@ -117,31 +142,31 @@ defmodule Pleroma.Web.OAuth.OAuthController do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L39 conn - |> put_flash(:error, "This action is outside the authorized scopes") + |> put_flash(:error, dgettext("errors", "This action is outside the authorized scopes")) |> put_status(:unauthorized) |> authorize(params) end defp handle_create_authorization_error( - conn, + %Plug.Conn{} = conn, {:auth_active, false}, %{"authorization" => _} = params ) do # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 conn - |> put_flash(:error, "Your login is missing a confirmed e-mail address") + |> put_flash(:error, dgettext("errors", "Your login is missing a confirmed e-mail address")) |> put_status(:forbidden) |> authorize(params) end - defp handle_create_authorization_error(conn, error, %{"authorization" => _}) do + defp handle_create_authorization_error(%Plug.Conn{} = conn, error, %{"authorization" => _}) do Authenticator.handle_error(conn, error) end @doc "Renew access_token with refresh_token" def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "refresh_token", "refresh_token" => token} = _params ) do with {:ok, app} <- Token.Utils.fetch_app(conn), @@ -151,13 +176,11 @@ defmodule Pleroma.Web.OAuth.OAuthController do json(conn, Token.Response.build(user, token, response_attrs)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end - def token_exchange(conn, %{"grant_type" => "authorization_code"} = params) do + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "authorization_code"} = params) do with {:ok, app} <- Token.Utils.fetch_app(conn), fixed_token = Token.Utils.fix_padding(params["code"]), {:ok, auth} <- Authorization.get_by_token(app, fixed_token), @@ -167,14 +190,12 @@ defmodule Pleroma.Web.OAuth.OAuthController do json(conn, Token.Response.build(user, token, response_attrs)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "password"} = params ) do with {:ok, %User{} = user} <- Authenticator.get_user(conn), @@ -189,23 +210,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do {:auth_active, false} -> # Per https://github.com/tootsuite/mastodon/blob/ # 51e154f5e87968d6bb115e053689767ab33e80cd/app/controllers/api/base_controller.rb#L76 - conn - |> put_status(:forbidden) - |> json(%{error: "Your login is missing a confirmed e-mail address"}) + render_error(conn, :forbidden, "Your login is missing a confirmed e-mail address") {:user_active, false} -> - conn - |> put_status(:forbidden) - |> json(%{error: "Your account is currently disabled"}) + render_error(conn, :forbidden, "Your account is currently disabled") _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + render_invalid_credentials_error(conn) end end def token_exchange( - conn, + %Plug.Conn{} = conn, %{"grant_type" => "password", "name" => name, "password" => _password} = params ) do params = @@ -216,22 +232,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do token_exchange(conn, params) end - def token_exchange(conn, %{"grant_type" => "client_credentials"} = _params) do + def token_exchange(%Plug.Conn{} = conn, %{"grant_type" => "client_credentials"} = _params) do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, auth} <- Authorization.create_authorization(app, %User{}), {:ok, token} <- Token.exchange_token(app, auth) do json(conn, Token.Response.build_for_client_credentials(token)) else - _error -> - put_status(conn, 400) - |> json(%{error: "Invalid credentials"}) + _error -> render_invalid_credentials_error(conn) end end # Bad request - def token_exchange(conn, params), do: bad_request(conn, params) + def token_exchange(%Plug.Conn{} = conn, params), do: bad_request(conn, params) - def token_revoke(conn, %{"token" => _token} = params) do + def token_revoke(%Plug.Conn{} = conn, %{"token" => _token} = params) do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, _token} <- RevokeToken.revoke(app, params) do json(conn, %{}) @@ -242,17 +256,18 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def token_revoke(conn, params), do: bad_request(conn, params) + def token_revoke(%Plug.Conn{} = conn, params), do: bad_request(conn, params) # Response for bad request - defp bad_request(conn, _) do - conn - |> put_status(500) - |> json(%{error: "Bad request"}) + defp bad_request(%Plug.Conn{} = conn, _) do + render_error(conn, :internal_server_error, "Bad request") end @doc "Prepares OAuth request to provider for Ueberauth" - def prepare_request(conn, %{"provider" => provider, "authorization" => auth_attrs}) do + def prepare_request(%Plug.Conn{} = conn, %{ + "provider" => provider, + "authorization" => auth_attrs + }) do scope = auth_attrs |> Scopes.fetch_scopes([]) @@ -273,12 +288,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do redirect(conn, to: o_auth_path(conn, :request, provider, params)) end - def request(conn, params) do + def request(%Plug.Conn{} = conn, params) do message = if params["provider"] do - "Unsupported OAuth provider: #{params["provider"]}." + dgettext("errors", "Unsupported OAuth provider: %{provider}.", + provider: params["provider"] + ) else - "Bad OAuth request." + dgettext("errors", "Bad OAuth request.") end conn @@ -286,17 +303,20 @@ defmodule Pleroma.Web.OAuth.OAuthController do |> redirect(to: "/") end - def callback(%{assigns: %{ueberauth_failure: failure}} = conn, params) do + def callback(%Plug.Conn{assigns: %{ueberauth_failure: failure}} = conn, params) do params = callback_params(params) messages = for e <- Map.get(failure, :errors, []), do: e.message message = Enum.join(messages, "; ") conn - |> put_flash(:error, "Failed to authenticate: #{message}.") + |> put_flash( + :error, + dgettext("errors", "Failed to authenticate: %{message}.", message: message) + ) |> redirect(external: redirect_uri(conn, params["redirect_uri"])) end - def callback(conn, params) do + def callback(%Plug.Conn{} = conn, params) do params = callback_params(params) with {:ok, registration} <- Authenticator.get_registration(conn) do @@ -314,13 +334,15 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) conn - |> put_session(:registration_id, registration.id) + |> put_session_registration_id(registration.id) |> registration_details(%{"authorization" => registration_params}) end else - _ -> + error -> + Logger.debug(inspect(["OAUTH_ERROR", error, conn.assigns])) + conn - |> put_flash(:error, "Failed to set up user account.") + |> put_flash(:error, dgettext("errors", "Failed to set up user account.")) |> redirect(external: redirect_uri(conn, params["redirect_uri"])) end end @@ -329,7 +351,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do Map.merge(params, Jason.decode!(state)) end - def registration_details(conn, %{"authorization" => auth_attrs}) do + def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) do render(conn, "register.html", %{ client_id: auth_attrs["client_id"], redirect_uri: auth_attrs["redirect_uri"], @@ -340,7 +362,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do }) end - def register(conn, %{"authorization" => _, "op" => "connect"} = params) do + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), {_, {:ok, auth}} <- @@ -359,7 +381,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do end end - def register(conn, %{"authorization" => _, "op" => "register"} = params) do + def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "register"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), {:ok, user} <- Authenticator.create_from_registration(conn, registration) do @@ -395,7 +417,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do end defp do_create_authorization( - conn, + %Plug.Conn{} = conn, %{ "authorization" => %{ @@ -416,13 +438,13 @@ defmodule Pleroma.Web.OAuth.OAuthController do end # Special case: Local MastodonFE - defp redirect_uri(conn, "."), do: mastodon_api_url(conn, :login) + defp redirect_uri(%Plug.Conn{} = conn, "."), do: mastodon_api_url(conn, :login) - defp redirect_uri(_conn, redirect_uri), do: redirect_uri + defp redirect_uri(%Plug.Conn{}, redirect_uri), do: redirect_uri - defp get_session_registration_id(conn), do: get_session(conn, :registration_id) + defp get_session_registration_id(%Plug.Conn{} = conn), do: get_session(conn, :registration_id) - defp put_session_registration_id(conn, registration_id), + defp put_session_registration_id(%Plug.Conn{} = conn, registration_id), do: put_session(conn, :registration_id, registration_id) @spec validate_scopes(App.t(), map()) :: @@ -432,4 +454,14 @@ defmodule Pleroma.Web.OAuth.OAuthController do |> Scopes.fetch_scopes(app.scopes) |> Scopes.validates(app.scopes) end + + def default_redirect_uri(%App{} = app) do + app.redirect_uris + |> String.split() + |> Enum.at(0) + end + + defp render_invalid_credentials_error(conn) do + render_error(conn, :bad_request, "Invalid credentials") + end end diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index f412f7eb2..90c304487 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -14,7 +14,6 @@ defmodule Pleroma.Web.OAuth.Token do alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Query - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) @type t :: %__MODULE__{} schema "oauth_tokens" do @@ -78,7 +77,7 @@ defmodule Pleroma.Web.OAuth.Token do defp put_valid_until(changeset, attrs) do expires_in = - Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), @expires_in)) + Map.get(attrs, :valid_until, NaiveDateTime.add(NaiveDateTime.utc_now(), expires_in())) changeset |> change(%{valid_until: expires_in}) @@ -123,4 +122,6 @@ defmodule Pleroma.Web.OAuth.Token do end def is_expired?(_), do: false + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/response.ex b/lib/pleroma/web/oauth/token/response.ex index 64e78b183..266110814 100644 --- a/lib/pleroma/web/oauth/token/response.ex +++ b/lib/pleroma/web/oauth/token/response.ex @@ -1,18 +1,20 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Response do @moduledoc false alias Pleroma.User alias Pleroma.Web.OAuth.Token.Utils - @expires_in Pleroma.Config.get([:oauth2, :token_expires_in], 600) - @doc false def build(%User{} = user, token, opts \\ %{}) do %{ token_type: "Bearer", access_token: token.token, refresh_token: token.refresh_token, - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " "), me: user.ap_id } @@ -25,8 +27,10 @@ defmodule Pleroma.Web.OAuth.Token.Response do access_token: token.token, refresh_token: token.refresh_token, created_at: Utils.format_created_at(token), - expires_in: @expires_in, + expires_in: expires_in(), scope: Enum.join(token.scopes, " ") } end + + defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) end diff --git a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex index 7df0be14e..c620050c8 100644 --- a/lib/pleroma/web/oauth/token/strategy/refresh_token.ex +++ b/lib/pleroma/web/oauth/token/strategy/refresh_token.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Strategy.RefreshToken do @moduledoc """ Functions for dealing with refresh token strategy. diff --git a/lib/pleroma/web/oauth/token/strategy/revoke.ex b/lib/pleroma/web/oauth/token/strategy/revoke.ex index dea63ca54..983f095b4 100644 --- a/lib/pleroma/web/oauth/token/strategy/revoke.ex +++ b/lib/pleroma/web/oauth/token/strategy/revoke.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Strategy.Revoke do @moduledoc """ Functions for dealing with revocation. diff --git a/lib/pleroma/web/oauth/token/utils.ex b/lib/pleroma/web/oauth/token/utils.ex index 7a4fddafd..1e8765e93 100644 --- a/lib/pleroma/web/oauth/token/utils.ex +++ b/lib/pleroma/web/oauth/token/utils.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.OAuth.Token.Utils do @moduledoc """ Auxiliary functions for dealing with tokens. diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index ec6e5cfaf..8e0adad91 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.CommonAPI + alias Pleroma.Web.Federator alias Pleroma.Web.OStatus alias Pleroma.Web.XML @@ -88,14 +89,15 @@ defmodule Pleroma.Web.OStatus.NoteHandler do Map.put(note, "external_url", url) end - def fetch_replied_to_activity(entry, in_reply_to) do + def fetch_replied_to_activity(entry, in_reply_to, options \\ []) do with %Activity{} = activity <- Activity.get_create_by_object_ap_id(in_reply_to) do activity else _e -> - with in_reply_to_href when not is_nil(in_reply_to_href) <- + with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), + in_reply_to_href when not is_nil(in_reply_to_href) <- XML.string_from_xpath("//thr:in-reply-to[1]/@href", entry), - {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href) do + {:ok, [activity | _]} <- OStatus.fetch_activity_from_url(in_reply_to_href, options) do activity else _e -> nil @@ -104,7 +106,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do end # TODO: Clean this up a bit. - def handle_note(entry, doc \\ nil) do + def handle_note(entry, doc \\ nil, options \\ []) do with id <- XML.string_from_xpath("//id", entry), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), [author] <- :xmerl_xpath.string('//author[1]', doc), @@ -112,7 +114,8 @@ defmodule Pleroma.Web.OStatus.NoteHandler do content_html <- OStatus.get_content(entry), cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), - in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to), + options <- Keyword.put(options, :depth, (options[:depth] || 0) + 1), + in_reply_to_activity <- fetch_replied_to_activity(entry, in_reply_to, options), in_reply_to_object <- (in_reply_to_activity && Object.normalize(in_reply_to_activity)) || nil, in_reply_to <- (in_reply_to_object && in_reply_to_object.data["id"]) || in_reply_to, diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 6ed089d84..502410c83 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -54,7 +54,7 @@ defmodule Pleroma.Web.OStatus do "#{Web.base_url()}/ostatus_subscribe?acct={uri}" end - def handle_incoming(xml_string) do + def handle_incoming(xml_string, options \\ []) do with doc when doc != :error <- parse_document(xml_string) do with {:ok, actor_user} <- find_make_or_update_user(doc), do: Pleroma.Instances.set_reachable(actor_user.ap_id) @@ -91,10 +91,12 @@ defmodule Pleroma.Web.OStatus do _ -> case object_type do 'http://activitystrea.ms/schema/1.0/note' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity + with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), + do: activity 'http://activitystrea.ms/schema/1.0/comment' -> - with {:ok, activity} <- NoteHandler.handle_note(entry, doc), do: activity + with {:ok, activity} <- NoteHandler.handle_note(entry, doc, options), + do: activity _ -> Logger.error("Couldn't parse incoming document") @@ -359,7 +361,7 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_atom_url(url) do + def fetch_activity_from_atom_url(url, options \\ []) do with true <- String.starts_with?(url, "http"), {:ok, %{body: body, status: code}} when code in 200..299 <- HTTP.get( @@ -367,7 +369,7 @@ defmodule Pleroma.Web.OStatus do [{:Accept, "application/atom+xml"}] ) do Logger.debug("Got document from #{url}, handling...") - handle_incoming(body) + handle_incoming(body, options) else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -375,13 +377,13 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_html_url(url) do + def fetch_activity_from_html_url(url, options \\ []) do Logger.debug("Trying to fetch #{url}") with true <- String.starts_with?(url, "http"), {:ok, %{body: body}} <- HTTP.get(url, []), {:ok, atom_url} <- get_atom_url(body) do - fetch_activity_from_atom_url(atom_url) + fetch_activity_from_atom_url(atom_url, options) else e -> Logger.debug("Couldn't get #{url}: #{inspect(e)}") @@ -389,11 +391,11 @@ defmodule Pleroma.Web.OStatus do end end - def fetch_activity_from_url(url) do - with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url) do + def fetch_activity_from_url(url, options \\ []) do + with {:ok, [_ | _] = activities} <- fetch_activity_from_atom_url(url, options) do {:ok, activities} else - _e -> fetch_activity_from_html_url(url) + _e -> fetch_activity_from_html_url(url, options) end rescue e -> diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 2fb6ce41b..372d52899 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -245,14 +245,10 @@ defmodule Pleroma.Web.OStatus.OStatusController do end def errors(conn, {:error, :not_found}) do - conn - |> put_status(404) - |> text("Not found") + render_error(conn, :not_found, "Not found") end def errors(conn, _) do - conn - |> put_status(500) - |> text("Something went wrong") + render_error(conn, :internal_server_error, "Something went wrong") end end diff --git a/lib/pleroma/web/rel_me.ex b/lib/pleroma/web/rel_me.ex index 26eb614a6..d376e2069 100644 --- a/lib/pleroma/web/rel_me.ex +++ b/lib/pleroma/web/rel_me.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.RelMe do with_body: true ] - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do def parse(url) when is_binary(url), do: parse_url(url) else def parse(url) when is_binary(url) do diff --git a/lib/pleroma/web/rich_media/helpers.ex b/lib/pleroma/web/rich_media/helpers.ex index 9bc8f2559..6506de46c 100644 --- a/lib/pleroma/web/rich_media/helpers.ex +++ b/lib/pleroma/web/rich_media/helpers.ex @@ -4,25 +4,53 @@ defmodule Pleroma.Web.RichMedia.Helpers do alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Web.RichMedia.Parser + @spec validate_page_url(any()) :: :ok | :error defp validate_page_url(page_url) when is_binary(page_url) do - if AutoLinker.Parser.is_url?(page_url, true) do - URI.parse(page_url) |> validate_page_url - else - :error + validate_tld = Application.get_env(:auto_linker, :opts)[:validate_tld] + + page_url + |> AutoLinker.Parser.url?(scheme: true, validate_tld: validate_tld) + |> parse_uri(page_url) + end + + defp validate_page_url(%URI{host: host, scheme: scheme, authority: authority}) + when scheme == "https" and not is_nil(authority) do + cond do + host in Config.get([:rich_media, :ignore_hosts], []) -> + :error + + get_tld(host) in Config.get([:rich_media, :ignore_tld], []) -> + :error + + true -> + :ok end end - defp validate_page_url(%URI{authority: nil}), do: :error - defp validate_page_url(%URI{scheme: nil}), do: :error - defp validate_page_url(%URI{}), do: :ok defp validate_page_url(_), do: :error + defp parse_uri(true, url) do + url + |> URI.parse() + |> validate_page_url + end + + defp parse_uri(_, _), do: :error + + defp get_tld(host) do + host + |> String.split(".") + |> Enum.reverse() + |> hd + end + def fetch_data_for_activity(%Activity{data: %{"type" => "Create"}} = activity) do - with true <- Pleroma.Config.get([:rich_media, :enabled]), + with true <- Config.get([:rich_media, :enabled]), %Object{} = object <- Object.normalize(activity), false <- object.data["sensitive"] || false, {:ok, page_url} <- HTML.extract_first_external_url(object, object.data["content"]), diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index 62e8fa610..0d2523338 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -3,12 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parser do - @parsers [ - Pleroma.Web.RichMedia.Parsers.OGP, - Pleroma.Web.RichMedia.Parsers.TwitterCard, - Pleroma.Web.RichMedia.Parsers.OEmbed - ] - @hackney_options [ pool: :media, recv_timeout: 2_000, @@ -16,9 +10,13 @@ defmodule Pleroma.Web.RichMedia.Parser do with_body: true ] + defp parsers do + Pleroma.Config.get([:rich_media, :parsers]) + end + def parse(nil), do: {:error, "No URL provided"} - if Mix.env() == :test do + if Pleroma.Config.get(:env) == :test do def parse(url), do: parse_url(url) else def parse(url) do @@ -37,7 +35,10 @@ defmodule Pleroma.Web.RichMedia.Parser do try do {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: @hackney_options) - html |> maybe_parse() |> clean_parsed_data() |> check_parsed_data() + html + |> maybe_parse() + |> clean_parsed_data() + |> check_parsed_data() rescue e -> {:error, "Parsing error: #{inspect(e)}"} @@ -45,7 +46,7 @@ defmodule Pleroma.Web.RichMedia.Parser do end defp maybe_parse(html) do - Enum.reduce_while(@parsers, %{}, fn parser, acc -> + Enum.reduce_while(parsers(), %{}, fn parser, acc -> case parser.parse(html, acc) do {:ok, data} -> {:halt, data} {:error, _msg} -> {:cont, acc} diff --git a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex index 4a7c5eae0..913975616 100644 --- a/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/meta_tags_parser.ex @@ -1,15 +1,23 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do def parse(html, data, prefix, error_message, key_name, value_name \\ "content") do - with elements = [_ | _] <- get_elements(html, key_name, prefix), - meta_data = - Enum.reduce(elements, data, fn el, acc -> - attributes = normalize_attributes(el, prefix, key_name, value_name) + meta_data = + html + |> get_elements(key_name, prefix) + |> Enum.reduce(data, fn el, acc -> + attributes = normalize_attributes(el, prefix, key_name, value_name) - Map.merge(acc, attributes) - end) do - {:ok, meta_data} + Map.merge(acc, attributes) + end) + |> maybe_put_title(html) + + if Enum.empty?(meta_data) do + {:error, error_message} else - _e -> {:error, error_message} + {:ok, meta_data} end end @@ -27,4 +35,19 @@ defmodule Pleroma.Web.RichMedia.Parsers.MetaTagsParser do %{String.to_atom(data[key_name]) => data[value_name]} end + + defp maybe_put_title(%{title: _} = meta, _), do: meta + + defp maybe_put_title(meta, html) when meta != %{} do + case get_page_title(html) do + "" -> meta + title -> Map.put_new(meta, :title, title) + end + end + + defp maybe_put_title(meta, _), do: meta + + defp get_page_title(html) do + Floki.find(html, "title") |> Floki.text() + end end diff --git a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex index 2530b8c9d..875637c4d 100644 --- a/lib/pleroma/web/rich_media/parsers/oembed_parser.ex +++ b/lib/pleroma/web/rich_media/parsers/oembed_parser.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.OEmbed do def parse(html, _data) do with elements = [_ | _] <- get_discovery_data(html), diff --git a/lib/pleroma/web/rich_media/parsers/ogp.ex b/lib/pleroma/web/rich_media/parsers/ogp.ex index 0e1a0e719..d40fa009f 100644 --- a/lib/pleroma/web/rich_media/parsers/ogp.ex +++ b/lib/pleroma/web/rich_media/parsers/ogp.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.OGP do def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex index a317c3e78..e4efe2dd0 100644 --- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do def parse(html, data) do Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 08c74a742..3e5142e8a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureUserKeyPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :authenticated_api do @@ -41,6 +42,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.UserEnabledPlug) plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :admin_api do @@ -57,6 +59,7 @@ defmodule Pleroma.Web.Router do plug(Pleroma.Plugs.SetUserSessionIdPlug) plug(Pleroma.Plugs.EnsureAuthenticatedPlug) plug(Pleroma.Plugs.UserIsAdminPlug) + plug(Pleroma.Plugs.IdempotencyPlug) end pipeline :mastodon_html do @@ -133,8 +136,8 @@ defmodule Pleroma.Web.Router do scope "/api/pleroma", Pleroma.Web.TwitterAPI do pipe_through(:pleroma_api) - get("/password_reset/:token", UtilController, :show_password_reset) - post("/password_reset", UtilController, :password_reset) + get("/password_reset/:token", PasswordController, :reset, as: :reset_password) + post("/password_reset", PasswordController, :do_reset, as: :reset_password) get("/emoji", UtilController, :emoji) get("/captcha", UtilController, :captcha) get("/healthcheck", UtilController, :healthcheck) @@ -202,6 +205,9 @@ defmodule Pleroma.Web.Router do put("/statuses/:id", AdminAPIController, :status_update) delete("/statuses/:id", AdminAPIController, :status_delete) + + get("/config", AdminAPIController, :config_show) + post("/config", AdminAPIController, :config_update) end scope "/", Pleroma.Web.TwitterAPI do @@ -309,8 +315,6 @@ defmodule Pleroma.Web.Router do post("/conversations/:id/read", MastodonAPIController, :conversation_read) get("/endorsements", MastodonAPIController, :empty_array) - - get("/pleroma/flavour", MastodonAPIController, :get_flavour) end scope [] do @@ -335,6 +339,8 @@ defmodule Pleroma.Web.Router do put("/scheduled_statuses/:id", MastodonAPIController, :update_scheduled_status) delete("/scheduled_statuses/:id", MastodonAPIController, :delete_scheduled_status) + post("/polls/:id/votes", MastodonAPIController, :poll_vote) + post("/media", MastodonAPIController, :upload) put("/media/:id", MastodonAPIController, :update_media) @@ -350,7 +356,9 @@ defmodule Pleroma.Web.Router do put("/filters/:id", MastodonAPIController, :update_filter) delete("/filters/:id", MastodonAPIController, :delete_filter) - post("/pleroma/flavour/:flavour", MastodonAPIController, :set_flavour) + patch("/pleroma/accounts/update_avatar", MastodonAPIController, :update_avatar) + patch("/pleroma/accounts/update_banner", MastodonAPIController, :update_banner) + patch("/pleroma/accounts/update_background", MastodonAPIController, :update_background) get("/pleroma/mascot", MastodonAPIController, :get_mascot) put("/pleroma/mascot", MastodonAPIController, :set_mascot) @@ -414,12 +422,7 @@ defmodule Pleroma.Web.Router do get("/trends", MastodonAPIController, :empty_array) - scope [] do - pipe_through(:oauth_read) - - get("/search", MastodonAPIController, :search) - get("/accounts/search", MastodonAPIController, :account_search) - end + get("/accounts/search", SearchController, :account_search) scope [] do pipe_through(:oauth_read_or_public) @@ -431,18 +434,22 @@ defmodule Pleroma.Web.Router do get("/statuses/:id", MastodonAPIController, :get_status) get("/statuses/:id/context", MastodonAPIController, :get_context) + get("/polls/:id", MastodonAPIController, :get_poll) + get("/accounts/:id/statuses", MastodonAPIController, :user_statuses) get("/accounts/:id/followers", MastodonAPIController, :followers) get("/accounts/:id/following", MastodonAPIController, :following) get("/accounts/:id", MastodonAPIController, :user) + get("/search", SearchController, :search) + get("/pleroma/accounts/:id/favourites", MastodonAPIController, :user_favourites) end end scope "/api/v2", Pleroma.Web.MastodonAPI do - pipe_through([:api, :oauth_read]) - get("/search", MastodonAPIController, :search2) + pipe_through([:api, :oauth_read_or_public]) + get("/search", SearchController, :search2) end scope "/api", Pleroma.Web do @@ -483,13 +490,8 @@ defmodule Pleroma.Web.Router do get("/statuses/show/:id", TwitterAPI.Controller, :fetch_status) get("/statusnet/conversation/:id", TwitterAPI.Controller, :fetch_conversation) - get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline) - end - - scope [] do - pipe_through(:oauth_read) - get("/search", TwitterAPI.Controller, :search) + get("/statusnet/tags/timeline/:tag", TwitterAPI.Controller, :public_and_external_timeline) end end @@ -508,7 +510,7 @@ defmodule Pleroma.Web.Router do end scope "/api", Pleroma.Web, as: :twitter_api_search do - pipe_through([:api, :oauth_read]) + pipe_through([:api, :oauth_read_or_public]) get("/pleroma/search_user", TwitterAPI.Controller, :search_user) end @@ -612,12 +614,6 @@ defmodule Pleroma.Web.Router do post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) end - scope "/", Pleroma.Web do - pipe_through(:oembed) - - get("/oembed", OEmbed.OEmbedController, :url) - end - pipeline :activitypub do plug(:accepts, ["activity+json", "json"]) plug(Pleroma.Web.Plugs.HTTPSignaturePlug) @@ -627,8 +623,6 @@ defmodule Pleroma.Web.Router do # XXX: not really ostatus pipe_through(:ostatus) - get("/users/:nickname/followers", ActivityPubController, :followers) - get("/users/:nickname/following", ActivityPubController, :following) get("/users/:nickname/outbox", ActivityPubController, :outbox) get("/objects/:uuid/likes", ActivityPubController, :object_likes) end @@ -660,6 +654,12 @@ defmodule Pleroma.Web.Router do pipe_through(:oauth_write) post("/users/:nickname/outbox", ActivityPubController, :update_outbox) end + + scope [] do + pipe_through(:oauth_read_or_public) + get("/users/:nickname/followers", ActivityPubController, :followers) + get("/users/:nickname/following", ActivityPubController, :following) + end end scope "/relay", Pleroma.Web.ActivityPub do @@ -707,7 +707,7 @@ defmodule Pleroma.Web.Router do get("/:sig/:url/:filename", MediaProxyController, :remote) end - if Mix.env() == :dev do + if Pleroma.Config.get(:env) == :dev do scope "/dev" do pipe_through([:mailbox_preview]) @@ -732,6 +732,7 @@ end defmodule Fallback.RedirectController do use Pleroma.Web, :controller + require Logger alias Pleroma.User alias Pleroma.Web.Metadata @@ -758,7 +759,20 @@ defmodule Fallback.RedirectController do def redirector_with_meta(conn, params) do {:ok, index_content} = File.read(index_file_path()) - tags = Metadata.build_tags(params) + + tags = + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + response = String.replace(index_content, "<!--server-generated-meta-->", tags) conn diff --git a/lib/pleroma/web/salmon/salmon.ex b/lib/pleroma/web/salmon/salmon.ex index 9e91a5a40..9b01ebcc6 100644 --- a/lib/pleroma/web/salmon/salmon.ex +++ b/lib/pleroma/web/salmon/salmon.ex @@ -123,11 +123,26 @@ defmodule Pleroma.Web.Salmon do {:ok, salmon} end - def remote_users(%{data: %{"to" => to} = data}) do - to = to ++ (data["cc"] || []) + def remote_users(%User{id: user_id}, %{data: %{"to" => to} = data}) do + cc = Map.get(data, "cc", []) + + bcc = + data + |> Map.get("bcc", []) + |> Enum.reduce([], fn ap_id, bcc -> + case Pleroma.List.get_by_ap_id(ap_id) do + %Pleroma.List{user_id: ^user_id} = list -> + {:ok, following} = Pleroma.List.get_following(list) + bcc ++ Enum.map(following, & &1.ap_id) + + _ -> + bcc + end + end) - to - |> Enum.map(fn id -> User.get_cached_by_ap_id(id) end) + [to, cc, bcc] + |> Enum.concat() + |> Enum.map(&User.get_cached_by_ap_id/1) |> Enum.filter(fn user -> user && !user.local end) end @@ -146,7 +161,7 @@ defmodule Pleroma.Web.Salmon do do: Instances.set_reachable(url) Logger.debug(fn -> "Pushed to #{url}, code #{code}" end) - :ok + {:ok, code} else e -> unless params[:unreachable_since], do: Instances.set_reachable(url) @@ -191,7 +206,7 @@ defmodule Pleroma.Web.Salmon do {:ok, private, _} = Keys.keys_from_pem(keys) {:ok, feed} = encode(private, feed) - remote_users = remote_users(activity) + remote_users = remote_users(user, activity) salmon_urls = Enum.map(remote_users, & &1.info.salmon) reachable_urls_metadata = Instances.filter_reachable(salmon_urls) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 133decfc4..4f325113a 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Web.Streamer do use GenServer require Logger alias Pleroma.Activity + alias Pleroma.Config alias Pleroma.Conversation.Participation alias Pleroma.Notification alias Pleroma.Object @@ -109,23 +110,18 @@ defmodule Pleroma.Web.Streamer do {:noreply, topics} end - def handle_cast(%{action: :stream, topic: "user", item: %Notification{} = item}, topics) do - topic = "user:#{item.user_id}" - - Enum.each(topics[topic] || [], fn socket -> - json = - %{ - event: "notification", - payload: - NotificationView.render("show.json", %{ - notification: item, - for: socket.assigns["user"] - }) - |> Jason.encode!() - } - |> Jason.encode!() - - send(socket.transport_pid, {:text, json}) + def handle_cast( + %{action: :stream, topic: topic, item: %Notification{} = item}, + topics + ) + when topic in ["user", "user:notification"] do + topics + |> Map.get("#{topic}:#{item.user_id}", []) + |> Enum.each(fn socket -> + send( + socket.transport_pid, + {:text, represent_notification(socket.assigns[:user], item)} + ) end) {:noreply, topics} @@ -215,6 +211,20 @@ defmodule Pleroma.Web.Streamer do |> Jason.encode!() end + @spec represent_notification(User.t(), Notification.t()) :: binary() + defp represent_notification(%User{} = user, %Notification{} = notify) do + %{ + event: "notification", + payload: + NotificationView.render( + "show.json", + %{notification: notify, for: user} + ) + |> Jason.encode!() + } + |> Jason.encode!() + end + def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do Enum.each(topics[topic] || [], fn socket -> # Get the current user so we have up-to-date blocks etc. @@ -224,11 +234,10 @@ defmodule Pleroma.Web.Streamer do mutes = user.info.mutes || [] reblog_mutes = user.info.muted_reblogs || [] - parent = Object.normalize(item) - - unless is_nil(parent) or item.actor in blocks or item.actor in mutes or - item.actor in reblog_mutes or not ActivityPub.contain_activity(item, user) or - parent.data["actor"] in blocks or parent.data["actor"] in mutes do + with parent when not is_nil(parent) <- Object.normalize(item), + true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), + true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + true <- thread_containment(item, user) do send(socket.transport_pid, {:text, represent_update(item, user)}) end else @@ -264,8 +273,8 @@ defmodule Pleroma.Web.Streamer do blocks = user.info.blocks || [] mutes = user.info.mutes || [] - unless item.actor in blocks or item.actor in mutes or - not ActivityPub.contain_activity(item, user) do + with true <- Enum.all?([blocks, mutes], &(item.actor not in &1)), + true <- thread_containment(item, user) do send(socket.transport_pid, {:text, represent_update(item, user)}) end else @@ -274,9 +283,20 @@ defmodule Pleroma.Web.Streamer do end) end - defp internal_topic(topic, socket) when topic in ~w[user direct] do + defp internal_topic(topic, socket) when topic in ~w[user user:notification direct] do "#{topic}:#{socket.assigns[:user].id}" end defp internal_topic(topic, _), do: topic + + @spec thread_containment(Activity.t(), User.t()) :: boolean() + defp thread_containment(_activity, %User{info: %{skip_thread_containment: true}}), do: true + + defp thread_containment(activity, user) do + if Config.get([:instance, :skip_thread_containment]) do + true + else + ActivityPub.contain_activity(activity, user) + end + end end diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index 3389c91cc..b3cf9ed11 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -4,7 +4,7 @@ <meta charset="utf-8" /> <meta name="viewport" content="width=device-width,initial-scale=1,minimal-ui" /> <title> - <%= Application.get_env(:pleroma, :instance)[:name] %> + <%= Pleroma.Config.get([:instance, :name]) %> </title> <style> body { @@ -63,13 +63,14 @@ .scopes-input { display: flex; + flex-direction: column; margin-top: 1em; text-align: left; color: #89898a; } .scopes-input label:first-child { - flex-basis: 40%; + height: 2em; } .scopes { @@ -80,13 +81,22 @@ } .scope { - flex-basis: 100%; display: flex; + flex-basis: 100%; height: 2em; align-items: center; } + .scope:before { + color: #b9b9ba; + content: "✔\fe0e"; + margin-left: 1em; + margin-right: 1em; + } + [type="checkbox"] + label { + display: none; + cursor: pointer; margin: 0.5em; } @@ -95,10 +105,12 @@ } [type="checkbox"] + label:before { + cursor: pointer; display: inline-block; color: white; background-color: #121a24; border: 4px solid #121a24; + box-shadow: 0px 0px 1px 0 #d8a070; box-sizing: border-box; width: 1.2em; height: 1.2em; @@ -128,7 +140,8 @@ border-radius: 4px; border: none; padding: 10px; - margin-top: 30px; + margin-top: 20px; + margin-bottom: 20px; text-transform: uppercase; font-size: 16px; box-shadow: 0px 0px 2px 0px black, @@ -147,8 +160,8 @@ box-sizing: border-box; width: 100%; background-color: #931014; + border: 1px solid #a06060; border-radius: 4px; - border: none; padding: 10px; margin-top: 20px; font-weight: 500; @@ -171,12 +184,27 @@ margin-top: 0 } - .scopes-input { - flex-direction: column; + .scope { + flex-basis: 0%; } - .scope { - flex-basis: 50%; + .scope:before { + content: ""; + margin-left: 0em; + margin-right: 1em; + } + + .scope:first-child:before { + margin-left: 1em; + content: "✔\fe0e"; + } + + .scope:after { + content: ","; + } + + .scope:last-child:after { + content: ""; } } .form-row { @@ -194,7 +222,7 @@ </head> <body> <div class="container"> - <h1><%= Application.get_env(:pleroma, :instance)[:name] %></h1> + <h1><%= Pleroma.Config.get([:instance, :name]) %></h1> <%= render @view_module, @view_template, assigns %> </div> </body> diff --git a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex index 5659c7828..3325beca1 100644 --- a/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex +++ b/lib/pleroma/web/templates/mastodon_api/mastodon/index.html.eex @@ -4,11 +4,11 @@ <meta charset='utf-8'> <meta content='width=device-width, initial-scale=1' name='viewport'> <title> -<%= Application.get_env(:pleroma, :instance)[:name] %> +<%= Pleroma.Config.get([:instance, :name]) %> </title> <link rel="icon" type="image/png" href="/favicon.png"/> <script crossorigin='anonymous' src="/packs/locales.js"></script> -<script crossorigin='anonymous' src="/packs/locales/<%= @flavour %>/en.js"></script> +<script crossorigin='anonymous' src="/packs/locales/glitch/en.js"></script> <link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/getting_started.js'> <link rel='preload' as='script' crossorigin='anonymous' href='/packs/features/compose.js'> @@ -19,10 +19,10 @@ <script src="/packs/core/common.js"></script> <link rel="stylesheet" media="all" href="/packs/core/common.css" /> -<script src="/packs/flavours/<%= @flavour %>/common.js"></script> -<link rel="stylesheet" media="all" href="/packs/flavours/<%= @flavour %>/common.css" /> +<script src="/packs/flavours/glitch/common.js"></script> +<link rel="stylesheet" media="all" href="/packs/flavours/glitch/common.css" /> -<script src="/packs/flavours/<%= @flavour %>/home.js"></script> +<script src="/packs/flavours/glitch/home.js"></script> </head> <body class='app-body no-reduce-motion system-font'> <div class='app-holder' data-props='{"locale":"en"}' id='mastodon'> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex index e6cfe108b..c9ec1ecbf 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/_scopes.html.eex @@ -1,13 +1,19 @@ <div class="scopes-input"> - <%= label @form, :scope, "Permissions" %> - + <%= label @form, :scope, "The following permissions will be granted" %> <div class="scopes"> <%= for scope <- @available_scopes do %> <%# Note: using hidden input with `unchecked_value` in order to distinguish user's empty selection from `scope` param being omitted %> - <div class="scope"> + <%= if scope in @scopes do %> + <div class="scope"> + <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> + <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> + <%= if scope in @scopes && scope do %> + <%= String.capitalize(scope) %> + <% end %> + </div> + <% else %> <%= checkbox @form, :"scope_#{scope}", value: scope in @scopes && scope, checked_value: scope, unchecked_value: "", name: "authorization[scope][]" %> - <%= label @form, :"scope_#{scope}", String.capitalize(scope) %> - </div> + <% end %> <% end %> </div> </div> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex index 4bcda7300..4a0718851 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex @@ -1,7 +1,9 @@ <h2>Sign in with external provider</h2> <%= form_for @conn, o_auth_path(@conn, :prepare_request), [as: "authorization", method: "get"], fn f -> %> - <%= render @view_module, "_scopes.html", Map.put(assigns, :form, f) %> + <div style="display: none"> + <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> + </div> <%= hidden_input f, :client_id, value: @client_id %> <%= hidden_input f, :redirect_uri, value: @redirect_uri %> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex index 8443d906b..8443d906b 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/results.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_authorization_created.html.eex diff --git a/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex new file mode 100644 index 000000000..961aad976 --- /dev/null +++ b/lib/pleroma/web/templates/o_auth/o_auth/oob_token_exists.html.eex @@ -0,0 +1,2 @@ +<h1>Authorization exists</h1> +<h2>Access token is <%= @token.token %></h2> diff --git a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex index 3e360a52c..b17142ff8 100644 --- a/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex +++ b/lib/pleroma/web/templates/o_auth/o_auth/show.html.eex @@ -6,26 +6,38 @@ <% end %> <h2>OAuth Authorization</h2> - <%= form_for @conn, o_auth_path(@conn, :authorize), [as: "authorization"], fn f -> %> -<div class="input"> - <%= label f, :name, "Name or email" %> - <%= text_input f, :name %> -</div> -<div class="input"> - <%= label f, :password, "Password" %> - <%= password_input f, :password %> -</div> -<%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<%= if @params["registration"] in ["true", true] do %> + <h3>This is the first time you visit! Please enter your Pleroma handle.</h3> + <p>Choose carefully! You won't be able to change this later. You will be able to change your display name, though.</p> + <div class="input"> + <%= label f, :nickname, "Pleroma Handle" %> + <%= text_input f, :nickname, placeholder: "lain" %> + </div> + <%= hidden_input f, :name, value: @params["name"] %> + <%= hidden_input f, :password, value: @params["password"] %> + <br> +<% else %> + <div class="input"> + <%= label f, :name, "Username" %> + <%= text_input f, :name %> + </div> + <div class="input"> + <%= label f, :password, "Password" %> + <%= password_input f, :password %> + </div> + <%= submit "Log In" %> + <%= render @view_module, "_scopes.html", Map.merge(assigns, %{form: f}) %> +<% end %> <%= hidden_input f, :client_id, value: @client_id %> <%= hidden_input f, :response_type, value: @response_type %> <%= hidden_input f, :redirect_uri, value: @redirect_uri %> <%= hidden_input f, :state, value: @state %> -<%= submit "Authorize" %> <% end %> <%= if Pleroma.Config.oauth_consumer_enabled?() do %> <%= render @view_module, Pleroma.Web.Auth.Authenticator.oauth_consumer_template(), assigns %> <% end %> + diff --git a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex index ee84750c7..ee84750c7 100644 --- a/lib/pleroma/web/templates/twitter_api/util/invalid_token.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/invalid_token.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex index a3facf017..7d3ef6b0d 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset.html.eex @@ -1,5 +1,5 @@ <h2>Password Reset for <%= @user.nickname %></h2> -<%= form_for @conn, util_path(@conn, :password_reset), [as: "data"], fn f -> %> +<%= form_for @conn, reset_password_path(@conn, :do_reset), [as: "data"], fn f -> %> <div class="form-row"> <%= label f, :password, "Password" %> <%= password_input f, :password %> diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex index df037c01e..df037c01e 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_failed.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_failed.html.eex diff --git a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex index f30ba3274..f30ba3274 100644 --- a/lib/pleroma/web/templates/twitter_api/util/password_reset_success.html.eex +++ b/lib/pleroma/web/templates/twitter_api/password/reset_success.html.eex diff --git a/lib/pleroma/web/translation_helpers.ex b/lib/pleroma/web/translation_helpers.ex new file mode 100644 index 000000000..8f5a43bf6 --- /dev/null +++ b/lib/pleroma/web/translation_helpers.ex @@ -0,0 +1,17 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TranslationHelpers do + defmacro render_error(conn, status, msgid, bindings \\ Macro.escape(%{})) do + quote do + require Pleroma.Web.Gettext + + unquote(conn) + |> Plug.Conn.put_status(unquote(status)) + |> Phoenix.Controller.json(%{ + error: Pleroma.Web.Gettext.dgettext("errors", unquote(msgid), unquote(bindings)) + }) + end + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/password_controller.ex b/lib/pleroma/web/twitter_api/controllers/password_controller.ex new file mode 100644 index 000000000..1941e6143 --- /dev/null +++ b/lib/pleroma/web/twitter_api/controllers/password_controller.ex @@ -0,0 +1,37 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordController do + @moduledoc """ + The module containts functions for reset password. + """ + + use Pleroma.Web, :controller + + require Logger + + alias Pleroma.PasswordResetToken + alias Pleroma.Repo + alias Pleroma.User + + def reset(conn, %{"token" => token}) do + with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), + %User{} = user <- User.get_cached_by_id(token.user_id) do + render(conn, "reset.html", %{ + token: token, + user: user + }) + else + _e -> render(conn, "invalid_token.html") + end + end + + def do_reset(conn, %{"data" => data}) do + with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do + render(conn, "reset_success.html") + else + _e -> render(conn, "reset_failed.html") + end + end +end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 489170d80..c10c66ff2 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -7,12 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do require Logger - alias Comeonin.Pbkdf2 alias Pleroma.Activity alias Pleroma.Emoji alias Pleroma.Notification - alias Pleroma.PasswordResetToken - alias Pleroma.Repo + alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub @@ -20,26 +18,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.OStatus alias Pleroma.Web.WebFinger - def show_password_reset(conn, %{"token" => token}) do - with %{used: false} = token <- Repo.get_by(PasswordResetToken, %{token: token}), - %User{} = user <- User.get_cached_by_id(token.user_id) do - render(conn, "password_reset.html", %{ - token: token, - user: user - }) - else - _e -> render(conn, "invalid_token.html") - end - end - - def password_reset(conn, %{"data" => data}) do - with {:ok, _} <- PasswordResetToken.reset_password(data["token"], data) do - render(conn, "password_reset_success.html") - else - _e -> render(conn, "password_reset_failed.html") - end - end - def help_test(conn, _params) do json(conn, "ok") end @@ -118,7 +96,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do name = followee.nickname with %User{} = user <- User.get_cached_by_nickname(username), - true <- Pbkdf2.checkpw(password, user.password_hash), + true <- AuthenticationPlug.checkpw(password, user.password_hash), %User{} = _followed <- User.get_cached_by_id(id), {:ok, follower} <- User.follow(user, followee), {:ok, _activity} <- ActivityPub.follow(follower, followee) do diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index 31e86685a..0313560a8 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -192,6 +192,13 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end def notifications(%{assigns: %{user: user}} = conn, params) do + params = + if Map.has_key?(params, "with_muted") do + Map.put(params, :with_muted, params["with_muted"] in [true, "True", "true", "1"]) + else + params + end + notifications = Notification.for_user(user, params) conn @@ -456,6 +463,16 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end + def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + change = Changeset.change(user, %{avatar: nil}) + {:ok, user} = User.update_and_set_cache(change) + CommonAPI.update(user) + + conn + |> put_view(UserView) + |> render("show.json", %{user: user, for: user}) + end + def update_avatar(%{assigns: %{user: user}} = conn, params) do {:ok, object} = ActivityPub.upload(params, type: :avatar) change = Changeset.change(user, %{avatar: object.data}) @@ -467,6 +484,19 @@ defmodule Pleroma.Web.TwitterAPI.Controller do |> render("show.json", %{user: user, for: user}) end + def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do + with new_info <- %{"banner" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, user} <- User.update_and_set_cache(changeset) do + CommonAPI.update(user) + response = %{url: nil} |> Jason.encode!() + + conn + |> json_reply(200, response) + end + end + def update_banner(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), new_info <- %{"banner" => object.data}, @@ -482,6 +512,18 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end + def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do + with new_info <- %{"background" => %{}}, + info_cng <- User.Info.profile_update(user.info, new_info), + changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), + {:ok, _user} <- User.update_and_set_cache(changeset) do + response = %{url: nil} |> Jason.encode!() + + conn + |> json_reply(200, response) + end + end + def update_background(%{assigns: %{user: user}} = conn, params) do with {:ok, object} <- ActivityPub.upload(params, type: :background), new_info <- %{"background" => object.data}, @@ -632,7 +674,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do defp build_info_cng(user, params) do info_params = - ["no_rich_text", "locked", "hide_followers", "hide_follows", "hide_favorites", "show_role"] + [ + "no_rich_text", + "locked", + "hide_followers", + "hide_follows", + "hide_favorites", + "show_role", + "skip_thread_containment" + ] |> Enum.reduce(%{}, fn key, res -> if value = params[key] do Map.put(res, key, value == "true") @@ -728,7 +778,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn def only_if_public_instance(conn, _) do - if Keyword.get(Application.get_env(:pleroma, :instance), :public) do + if Pleroma.Config.get([:instance, :public]) do conn else conn diff --git a/lib/pleroma/web/twitter_api/views/password_view.ex b/lib/pleroma/web/twitter_api/views/password_view.ex new file mode 100644 index 000000000..b166b925d --- /dev/null +++ b/lib/pleroma/web/twitter_api/views/password_view.ex @@ -0,0 +1,8 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.TwitterAPI.PasswordView do + use Pleroma.Web, :view + import Phoenix.HTML.Form +end diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex index f0a4ddbd3..8d8892068 100644 --- a/lib/pleroma/web/twitter_api/views/user_view.ex +++ b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -118,9 +118,11 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "pleroma" => %{ "confirmation_pending" => user_info.confirmation_pending, - "tags" => user.tags + "tags" => user.tags, + "skip_thread_containment" => user.info.skip_thread_containment } |> maybe_with_activation_status(user, for_user) + |> with_notification_settings(user, for_user) } |> maybe_with_user_settings(user, for_user) |> maybe_with_role(user, for_user) @@ -132,6 +134,12 @@ defmodule Pleroma.Web.TwitterAPI.UserView do end end + defp with_notification_settings(data, %User{id: user_id} = user, %User{id: user_id}) do + Map.put(data, "notification_settings", user.info.notification_settings) + end + + defp with_notification_settings(data, _, _), do: data + defp maybe_with_activation_status(data, user, %User{info: %{is_admin: true}}) do Map.put(data, "deactivated", user.info.deactivated) end diff --git a/lib/pleroma/web/uploader_controller.ex b/lib/pleroma/web/uploader_controller.ex index 5d8a77346..bf09775e6 100644 --- a/lib/pleroma/web/uploader_controller.ex +++ b/lib/pleroma/web/uploader_controller.ex @@ -1,3 +1,7 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + defmodule Pleroma.Web.UploaderController do use Pleroma.Web, :controller @@ -8,7 +12,7 @@ defmodule Pleroma.Web.UploaderController do end def callbacks(conn, _) do - send_resp(conn, 400, "bad request") + render_error(conn, :bad_request, "bad request") end defp process_callback(conn, pid, params) when is_pid(pid) do @@ -20,6 +24,6 @@ defmodule Pleroma.Web.UploaderController do end defp process_callback(conn, _, _) do - send_resp(conn, 400, "bad request") + render_error(conn, :bad_request, "bad request") end end diff --git a/lib/pleroma/web/views/error_view.ex b/lib/pleroma/web/views/error_view.ex index f4c04131c..5cb8669fe 100644 --- a/lib/pleroma/web/views/error_view.ex +++ b/lib/pleroma/web/views/error_view.ex @@ -13,7 +13,7 @@ defmodule Pleroma.Web.ErrorView do def render("500.json", assigns) do Logger.error("Internal server error: #{inspect(assigns[:reason])}") - if Mix.env() != :prod do + if Pleroma.Config.get(:env) != :prod do %{errors: %{detail: "Internal server error", reason: inspect(assigns[:reason])}} else %{errors: %{detail: "Internal server error"}} diff --git a/lib/pleroma/web/web.ex b/lib/pleroma/web/web.ex index 66813e4dd..b42f6887e 100644 --- a/lib/pleroma/web/web.ex +++ b/lib/pleroma/web/web.ex @@ -23,9 +23,11 @@ defmodule Pleroma.Web do def controller do quote do use Phoenix.Controller, namespace: Pleroma.Web + import Plug.Conn import Pleroma.Web.Gettext import Pleroma.Web.Router.Helpers + import Pleroma.Web.TranslationHelpers plug(:set_put_layout) |