diff options
author | Roman Chvanikov <chvanikoff@pm.me> | 2020-07-09 16:56:06 +0300 |
---|---|---|
committer | Roman Chvanikov <chvanikoff@pm.me> | 2020-07-09 16:56:06 +0300 |
commit | 17a2ff57ac589b6fb996583f38f542b365b01ccf (patch) | |
tree | dd19dc053927147c5b28742b1b2fbb743bcbb650 /lib | |
parent | 3ce21ff4e8cc0d3c1a7481082fe574e1f9ff6466 (diff) | |
parent | 8ca1f3e8c61b906387e9cb6fb8993d2ad496ab3d (diff) | |
download | pleroma-17a2ff57ac589b6fb996583f38f542b365b01ccf.tar.gz |
merge develop
Diffstat (limited to 'lib')
41 files changed, 547 insertions, 497 deletions
diff --git a/lib/pleroma/config/loader.ex b/lib/pleroma/config/loader.ex index 0f3ecf1ed..64e7de6df 100644 --- a/lib/pleroma/config/loader.ex +++ b/lib/pleroma/config/loader.ex @@ -12,6 +12,11 @@ defmodule Pleroma.Config.Loader do :swarm ] + @reject_groups [ + :postgrex, + :tesla + ] + if Code.ensure_loaded?(Config.Reader) do @reader Config.Reader @@ -47,7 +52,8 @@ defmodule Pleroma.Config.Loader do @spec filter_group(atom(), keyword()) :: keyword() def filter_group(group, configs) do Enum.reject(configs[group], fn {key, _v} -> - key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex + key in @reject_keys or group in @reject_groups or + (group == :phoenix and key == :serve_endpoints) end) end end diff --git a/lib/pleroma/filter.ex b/lib/pleroma/filter.ex index 4d61b3650..5d6df9530 100644 --- a/lib/pleroma/filter.ex +++ b/lib/pleroma/filter.ex @@ -34,10 +34,18 @@ defmodule Pleroma.Filter do Repo.one(query) end - def get_filters(%User{id: user_id} = _user) do + def get_active(query) do + from(f in query, where: is_nil(f.expires_at) or f.expires_at > ^NaiveDateTime.utc_now()) + end + + def get_irreversible(query) do + from(f in query, where: f.hide) + end + + def get_filters(query \\ __MODULE__, %User{id: user_id}) do query = from( - f in Pleroma.Filter, + f in query, where: f.user_id == ^user_id, order_by: [desc: :id] ) @@ -95,4 +103,34 @@ defmodule Pleroma.Filter do |> validate_required([:phrase, :context]) |> Repo.update() end + + def compose_regex(user_or_filters, format \\ :postgres) + + def compose_regex(%User{} = user, format) do + __MODULE__ + |> get_active() + |> get_irreversible() + |> get_filters(user) + |> compose_regex(format) + end + + def compose_regex([_ | _] = filters, format) do + phrases = + filters + |> Enum.map(& &1.phrase) + |> Enum.join("|") + + case format do + :postgres -> + "\\y(#{phrases})\\y" + + :re -> + ~r/\b#{phrases}\b/i + + _ -> + nil + end + end + + def compose_regex(_, _), do: nil end diff --git a/lib/pleroma/instances/instance.ex b/lib/pleroma/instances/instance.ex index 74458c09a..a1f935232 100644 --- a/lib/pleroma/instances/instance.ex +++ b/lib/pleroma/instances/instance.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Instances.Instance do schema "instances" do field(:host, :string) field(:unreachable_since, :naive_datetime_usec) + field(:favicon, :string) + field(:favicon_updated_at, :naive_datetime) timestamps() end @@ -25,7 +27,7 @@ defmodule Pleroma.Instances.Instance do def changeset(struct, params \\ %{}) do struct - |> cast(params, [:host, :unreachable_since]) + |> cast(params, [:host, :unreachable_since, :favicon, :favicon_updated_at]) |> validate_required([:host]) |> unique_constraint(:host) end @@ -120,4 +122,48 @@ defmodule Pleroma.Instances.Instance do end defp parse_datetime(datetime), do: datetime + + def get_or_update_favicon(%URI{host: host} = instance_uri) do + existing_record = Repo.get_by(Instance, %{host: host}) + now = NaiveDateTime.utc_now() + + if existing_record && existing_record.favicon_updated_at && + NaiveDateTime.diff(now, existing_record.favicon_updated_at) < 86_400 do + existing_record.favicon + else + favicon = scrape_favicon(instance_uri) + + if existing_record do + existing_record + |> changeset(%{favicon: favicon, favicon_updated_at: now}) + |> Repo.update() + else + %Instance{} + |> changeset(%{host: host, favicon: favicon, favicon_updated_at: now}) + |> Repo.insert() + end + + favicon + end + end + + defp scrape_favicon(%URI{} = instance_uri) do + try do + with {:ok, %Tesla.Env{body: html}} <- + Pleroma.HTTP.get(to_string(instance_uri), [{:Accept, "text/html"}]), + favicon_rel <- + html + |> Floki.parse_document!() + |> Floki.attribute("link[rel=icon]", "href") + |> List.first(), + favicon <- URI.merge(instance_uri, favicon_rel) |> to_string(), + true <- is_binary(favicon) do + favicon + else + _ -> nil + end + rescue + _ -> nil + end + end end diff --git a/lib/pleroma/migration_helper/notification_backfill.ex b/lib/pleroma/migration_helper/notification_backfill.ex index b3770307a..d260e62ca 100644 --- a/lib/pleroma/migration_helper/notification_backfill.ex +++ b/lib/pleroma/migration_helper/notification_backfill.ex @@ -3,7 +3,6 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.MigrationHelper.NotificationBackfill do - alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User @@ -25,18 +24,27 @@ defmodule Pleroma.MigrationHelper.NotificationBackfill do |> type_from_activity() notification - |> Notification.changeset(%{type: type}) + |> Ecto.Changeset.change(%{type: type}) |> Repo.update() end) end + defp get_by_ap_id(ap_id) do + q = + from(u in User, + select: u.id + ) + + Repo.get_by(q, ap_id: ap_id) + end + # This is copied over from Notifications to keep this stable. defp type_from_activity(%{data: %{"type" => type}} = activity) do case type do "Follow" -> accepted_function = fn activity -> - with %User{} = follower <- User.get_by_ap_id(activity.data["actor"]), - %User{} = followed <- User.get_by_ap_id(activity.data["object"]) do + with %User{} = follower <- get_by_ap_id(activity.data["actor"]), + %User{} = followed <- get_by_ap_id(activity.data["object"]) do Pleroma.FollowingRelationship.following?(follower, followed) end end diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 2ef1a80c5..32bcfcaba 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -130,6 +130,7 @@ defmodule Pleroma.Notification do |> preload([n, a, o], activity: {a, object: o}) |> exclude_notification_muted(user, exclude_notification_muted_opts) |> exclude_blocked(user, exclude_blocked_opts) + |> exclude_filtered(user) |> exclude_visibility(opts) end @@ -158,6 +159,20 @@ defmodule Pleroma.Notification do |> where([n, a, o, tm], is_nil(tm.user_id)) end + defp exclude_filtered(query, user) do + case Pleroma.Filter.compose_regex(user) do + nil -> + query + + regex -> + from([_n, a, o] in query, + where: + fragment("not(?->>'content' ~* ?)", o.data, ^regex) or + fragment("?->>'actor' = ?", o.data, ^user.ap_id) + ) + end + end + @valid_visibilities ~w[direct unlisted public private] defp exclude_visibility(query, %{exclude_visibilities: visibility}) @@ -337,6 +352,7 @@ defmodule Pleroma.Notification do end end + @spec create_notifications(Activity.t(), keyword()) :: {:ok, [Notification.t()] | []} def create_notifications(activity, options \\ []) def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity, options) do @@ -481,6 +497,10 @@ defmodule Pleroma.Notification do end end + def get_potential_receiver_ap_ids(%{data: %{"type" => "Follow", "object" => object_id}}) do + [object_id] + end + def get_potential_receiver_ap_ids(activity) do [] |> Utils.maybe_notify_to_recipients(activity) @@ -555,7 +575,8 @@ defmodule Pleroma.Notification do :follows, :non_followers, :non_follows, - :recently_followed + :recently_followed, + :filtered ] |> Enum.find(&skip?(&1, activity, user)) end @@ -624,6 +645,26 @@ defmodule Pleroma.Notification do end) end + def skip?(:filtered, %{data: %{"type" => type}}, _) when type in ["Follow", "Move"], do: false + + def skip?(:filtered, activity, user) do + object = Object.normalize(activity) + + cond do + is_nil(object) -> + false + + object.data["actor"] == user.ap_id -> + false + + not is_nil(regex = Pleroma.Filter.compose_regex(user, :re)) -> + Regex.match?(regex, object.data["content"]) + + true -> + false + end + end + def skip?(_, _, _), do: false def for_user_and_activity(user, activity) do diff --git a/lib/pleroma/plugs/static_fe_plug.ex b/lib/pleroma/plugs/static_fe_plug.ex index c12776f4d..d3ae08835 100644 --- a/lib/pleroma/plugs/static_fe_plug.ex +++ b/lib/pleroma/plugs/static_fe_plug.ex @@ -10,8 +10,7 @@ defmodule Pleroma.Plugs.StaticFEPlug do def call(%{private: %{frontend: %{static: true}}} = conn, _) do action = Phoenix.Controller.action_name(conn) - if accepts_html?(conn) and - function_exported?(Pleroma.Web.Frontend.StaticController, action, 2) do + if requires_html?(conn) and has_action?(action) do conn |> Pleroma.Web.FrontendController.call(action) |> halt() @@ -22,10 +21,8 @@ defmodule Pleroma.Plugs.StaticFEPlug do def call(conn, _), do: conn - defp accepts_html?(conn) do - case get_req_header(conn, "accept") do - [accept | _] -> String.contains?(accept, "text/html") - _ -> false - end - end + defp requires_html?(conn), do: Phoenix.Controller.get_format(conn) == "html" + + defp has_action?(action), + do: function_exported?(Pleroma.Web.Frontend.StaticController, action, 2) end diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index 797555bff..0fa6b89dc 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -63,6 +63,10 @@ defmodule Pleroma.Upload do with {:ok, upload} <- prepare_upload(upload, opts), upload = %__MODULE__{upload | path: upload.path || "#{upload.id}/#{upload.name}"}, {:ok, upload} <- Pleroma.Upload.Filter.filter(opts.filters, upload), + description = Map.get(opts, :description) || upload.name, + {_, true} <- + {:description_limit, + String.length(description) <= Pleroma.Config.get([:instance, :description_limit])}, {:ok, url_spec} <- Pleroma.Uploaders.Uploader.put_file(opts.uploader, upload) do {:ok, %{ @@ -75,9 +79,12 @@ defmodule Pleroma.Upload do "href" => url_from_spec(upload, opts.base_url, url_spec) } ], - "name" => Map.get(opts, :description) || upload.name + "name" => description }} else + {:description_limit, _} -> + {:error, :description_too_long} + {:error, error} -> Logger.error( "#{__MODULE__} store (using #{inspect(opts.uploader)}) failed: #{inspect(error)}" diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 8a54546d6..9d1314f81 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -89,7 +89,7 @@ defmodule Pleroma.User do field(:keys, :string) field(:public_key, :string) field(:ap_id, :string) - field(:avatar, :map) + field(:avatar, :map, default: %{}) field(:local, :boolean, default: true) field(:follower_address, :string) field(:following_address, :string) @@ -539,14 +539,11 @@ defmodule Pleroma.User do end defp put_change_if_present(changeset, map_field, value_function) do - if value = get_change(changeset, map_field) do - with {:ok, new_value} <- value_function.(value) do - put_change(changeset, map_field, new_value) - else - _ -> changeset - end + with {:ok, value} <- fetch_change(changeset, map_field), + {:ok, new_value} <- value_function.(value) do + put_change(changeset, map_field, new_value) else - changeset + _ -> changeset end end @@ -1546,7 +1543,7 @@ defmodule Pleroma.User do fn followed_identifier -> with {:ok, %User{} = followed} <- get_or_fetch(followed_identifier), {:ok, follower} <- maybe_direct_follow(follower, followed), - {:ok, _} <- ActivityPub.follow(follower, followed) do + {:ok, _, _, _} <- CommonAPI.follow(follower, followed) do followed else err -> diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index cec59c372..42ff1de78 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -52,6 +52,7 @@ defmodule Pleroma.User.Search do |> base_query(following) |> filter_blocked_user(for_user) |> filter_invisible_users() + |> filter_internal_users() |> filter_blocked_domains(for_user) |> fts_search(query_string) |> trigram_rank(query_string) @@ -109,6 +110,10 @@ defmodule Pleroma.User.Search do from(q in query, where: q.invisible == false) end + defp filter_internal_users(query) do + from(q in query, where: q.actor_type != "Application") + end + defp filter_blocked_user(query, %User{} = blocker) do query |> join(:left, [u], b in Pleroma.UserRelationship, diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 94117202c..1c2908805 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do alias Pleroma.Constants alias Pleroma.Conversation alias Pleroma.Conversation.Participation + alias Pleroma.Filter alias Pleroma.Maps alias Pleroma.Notification alias Pleroma.Object @@ -321,28 +322,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do end end - @spec follow(User.t(), User.t(), String.t() | nil, boolean(), keyword()) :: - {:ok, Activity.t()} | {:error, any()} - def follow(follower, followed, activity_id \\ nil, local \\ true, opts \\ []) do - with {:ok, result} <- - Repo.transaction(fn -> do_follow(follower, followed, activity_id, local, opts) end) do - result - end - end - - defp do_follow(follower, followed, activity_id, local, opts) do - skip_notify_and_stream = Keyword.get(opts, :skip_notify_and_stream, false) - data = make_follow_data(follower, followed, activity_id) - - with {:ok, activity} <- insert(data, local), - _ <- skip_notify_and_stream || notify_and_stream(activity), - :ok <- maybe_federate(activity) do - {:ok, activity} - else - {:error, error} -> Repo.rollback(error) - end - end - @spec unfollow(User.t(), User.t(), String.t() | nil, boolean()) :: {:ok, Activity.t()} | nil | {:error, any()} def unfollow(follower, followed, activity_id \\ nil, local \\ true) do @@ -446,6 +425,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> maybe_set_thread_muted_field(opts) |> restrict_blocked(opts) |> restrict_recipients(recipients, opts[:user]) + |> restrict_filtered(opts) |> where( [activity], fragment( @@ -961,6 +941,26 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_instance(query, _), do: query + defp restrict_filtered(query, %{user: %User{} = user}) do + case Filter.compose_regex(user) do + nil -> + query + + regex -> + from([activity, object] in query, + where: + fragment("not(?->>'content' ~* ?)", object.data, ^regex) or + activity.actor == ^user.ap_id + ) + end + end + + defp restrict_filtered(query, %{blocking_user: %User{} = user}) do + restrict_filtered(query, %{user: user}) + end + + defp restrict_filtered(query, _), do: query + defp exclude_poll_votes(query, %{include_poll_votes: true}), do: query defp exclude_poll_votes(query, _) do @@ -1091,6 +1091,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_favorited_by(opts) |> restrict_blocked(restrict_blocked_opts) |> restrict_muted(restrict_muted_opts) + |> restrict_filtered(opts) |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_thread_visibility(opts, config) @@ -1099,6 +1100,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_instance(opts) |> restrict_announce_object_actor(opts) + |> restrict_filtered(opts) |> Activity.restrict_deactivated_users() |> exclude_poll_votes(opts) |> exclude_chat_messages(opts) diff --git a/lib/pleroma/web/activity_pub/builder.ex b/lib/pleroma/web/activity_pub/builder.ex index cabc28de9..d5f3610ed 100644 --- a/lib/pleroma/web/activity_pub/builder.ex +++ b/lib/pleroma/web/activity_pub/builder.ex @@ -14,6 +14,19 @@ defmodule Pleroma.Web.ActivityPub.Builder do require Pleroma.Constants + @spec follow(User.t(), User.t()) :: {:ok, map(), keyword()} + def follow(follower, followed) do + data = %{ + "id" => Utils.generate_activity_id(), + "actor" => follower.ap_id, + "type" => "Follow", + "object" => followed.ap_id, + "to" => [followed.ap_id] + } + + {:ok, data, []} + end + @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()} def emoji_react(actor, object, emoji) do with {:ok, data, meta} <- object_action(actor, object) do diff --git a/lib/pleroma/web/activity_pub/object_validator.ex b/lib/pleroma/web/activity_pub/object_validator.ex index bb6324460..df926829c 100644 --- a/lib/pleroma/web/activity_pub/object_validator.ex +++ b/lib/pleroma/web/activity_pub/object_validator.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do alias Pleroma.Web.ActivityPub.ObjectValidators.CreateChatMessageValidator alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator + alias Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator alias Pleroma.Web.ActivityPub.ObjectValidators.UpdateValidator @@ -25,6 +26,16 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()} def validate(object, meta) + def validate(%{"type" => "Follow"} = object, meta) do + with {:ok, object} <- + object + |> FollowValidator.cast_and_validate() + |> Ecto.Changeset.apply_action(:insert) do + object = stringify_keys(object) + {:ok, object, meta} + end + end + def validate(%{"type" => "Block"} = block_activity, meta) do with {:ok, block_activity} <- block_activity diff --git a/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex new file mode 100644 index 000000000..ca2724616 --- /dev/null +++ b/lib/pleroma/web/activity_pub/object_validators/follow_validator.ex @@ -0,0 +1,44 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.ActivityPub.ObjectValidators.FollowValidator do + use Ecto.Schema + + alias Pleroma.EctoType.ActivityPub.ObjectValidators + + import Ecto.Changeset + import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations + + @primary_key false + + embedded_schema do + field(:id, ObjectValidators.ObjectID, primary_key: true) + field(:type, :string) + field(:actor, ObjectValidators.ObjectID) + field(:to, ObjectValidators.Recipients, default: []) + field(:cc, ObjectValidators.Recipients, default: []) + field(:object, ObjectValidators.ObjectID) + field(:state, :string, default: "pending") + end + + def cast_data(data) do + %__MODULE__{} + |> cast(data, __schema__(:fields)) + end + + def validate_data(cng) do + cng + |> validate_required([:id, :type, :actor, :to, :cc, :object]) + |> validate_inclusion(:type, ["Follow"]) + |> validate_inclusion(:state, ~w{pending reject accept}) + |> validate_actor_presence() + |> validate_actor_presence(field_name: :object) + end + + def cast_and_validate(data) do + data + |> cast_data + |> validate_data + end +end diff --git a/lib/pleroma/web/activity_pub/relay.ex b/lib/pleroma/web/activity_pub/relay.ex index 484178edd..b09764d2b 100644 --- a/lib/pleroma/web/activity_pub/relay.ex +++ b/lib/pleroma/web/activity_pub/relay.ex @@ -28,7 +28,7 @@ defmodule Pleroma.Web.ActivityPub.Relay do def follow(target_instance) do with %User{} = local_user <- get_actor(), {:ok, %User{} = target_user} <- User.get_or_fetch_by_ap_id(target_instance), - {:ok, activity} <- ActivityPub.follow(local_user, target_user) do + {:ok, _, _, activity} <- CommonAPI.follow(local_user, target_user) do Logger.info("relay: followed instance: #{target_instance}; id=#{activity.data["id"]}") {:ok, activity} else diff --git a/lib/pleroma/web/activity_pub/side_effects.ex b/lib/pleroma/web/activity_pub/side_effects.ex index 61feeae4d..1d2c296a5 100644 --- a/lib/pleroma/web/activity_pub/side_effects.ex +++ b/lib/pleroma/web/activity_pub/side_effects.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do alias Pleroma.Activity.Ir.Topics alias Pleroma.Chat alias Pleroma.Chat.MessageReference + alias Pleroma.FollowingRelationship alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Repo @@ -21,6 +22,69 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do def handle(object, meta \\ []) + # Tasks this handle + # - Follows if possible + # - Sends a notification + # - Generates accept or reject if appropriate + def handle( + %{ + data: %{ + "id" => follow_id, + "type" => "Follow", + "object" => followed_user, + "actor" => following_user + } + } = object, + meta + ) do + with %User{} = follower <- User.get_cached_by_ap_id(following_user), + %User{} = followed <- User.get_cached_by_ap_id(followed_user), + {_, {:ok, _}, _, _} <- + {:following, User.follow(follower, followed, :follow_pending), follower, followed} do + if followed.local && !followed.locked do + Utils.update_follow_state_for_all(object, "accept") + FollowingRelationship.update(follower, followed, :follow_accept) + User.update_follower_count(followed) + User.update_following_count(follower) + + %{ + to: [following_user], + actor: followed, + object: follow_id, + local: true + } + |> ActivityPub.accept() + end + else + {:following, {:error, _}, follower, followed} -> + Utils.update_follow_state_for_all(object, "reject") + FollowingRelationship.update(follower, followed, :follow_reject) + + if followed.local do + %{ + to: [follower.ap_id], + actor: followed, + object: follow_id, + local: true + } + |> ActivityPub.reject() + end + + _ -> + nil + end + + {:ok, notifications} = Notification.create_notifications(object, do_send: false) + + meta = + meta + |> add_notifications(notifications) + + updated_object = Activity.get_by_ap_id(follow_id) + + {:ok, updated_object, meta} + end + # Tasks this handles: # - Unfollow and block def handle( @@ -209,14 +273,20 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do {:ok, object} end - def handle_undoing(%{data: %{"type" => "Like"}} = object) do - with %Object{} = liked_object <- Object.get_by_ap_id(object.data["object"]), - {:ok, _} <- Utils.remove_like_from_object(object, liked_object), - {:ok, _} <- Repo.delete(object) do - :ok + defp undo_like(nil, object), do: delete_object(object) + + defp undo_like(%Object{} = liked_object, object) do + with {:ok, _} <- Utils.remove_like_from_object(object, liked_object) do + delete_object(object) end end + def handle_undoing(%{data: %{"type" => "Like"}} = object) do + object.data["object"] + |> Object.get_by_ap_id() + |> undo_like(object) + end + def handle_undoing(%{data: %{"type" => "EmojiReact"}} = object) do with %Object{} = reacted_object <- Object.get_by_ap_id(object.data["object"]), {:ok, _} <- Utils.remove_emoji_reaction_from_object(object, reacted_object), @@ -246,6 +316,11 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do def handle_undoing(object), do: {:error, ["don't know how to handle", object]} + @spec delete_object(Object.t()) :: :ok | {:error, Ecto.Changeset.t()} + defp delete_object(object) do + with {:ok, _} <- Repo.delete(object), do: :ok + end + defp send_notifications(meta) do Keyword.get(meta, :notifications, []) |> Enum.each(fn notification -> diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index bc6fc4bd8..884646ceb 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -233,18 +233,24 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do is_map(url) && is_binary(url["href"]) -> url["href"] is_binary(data["url"]) -> data["url"] is_binary(data["href"]) -> data["href"] + true -> nil end - attachment_url = - %{"href" => href} - |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", Map.get(url || %{}, "type")) + if href do + attachment_url = + %{"href" => href} + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", Map.get(url || %{}, "type")) - %{"url" => [attachment_url]} - |> Maps.put_if_present("mediaType", media_type) - |> Maps.put_if_present("type", data["type"]) - |> Maps.put_if_present("name", data["name"]) + %{"url" => [attachment_url]} + |> Maps.put_if_present("mediaType", media_type) + |> Maps.put_if_present("type", data["type"]) + |> Maps.put_if_present("name", data["name"]) + else + nil + end end) + |> Enum.filter(& &1) Map.put(object, "attachment", attachments) end @@ -263,12 +269,18 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do def fix_url(%{"type" => object_type, "url" => url} = object) when object_type in ["Video", "Audio"] and is_list(url) do - first_element = Enum.at(url, 0) + attachment = + Enum.find(url, fn x -> + media_type = x["mediaType"] || x["mimeType"] || "" - link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) + is_map(x) and String.starts_with?(media_type, ["audio/", "video/"]) + end) + + link_element = + Enum.find(url, fn x -> is_map(x) and (x["mediaType"] || x["mimeType"]) == "text/html" end) object - |> Map.put("attachment", [first_element]) + |> Map.put("attachment", [attachment]) |> Map.put("url", link_element["href"]) end @@ -518,66 +530,6 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do end def handle_incoming( - %{"type" => "Follow", "object" => followed, "actor" => follower, "id" => id} = data, - _options - ) do - with %User{local: true} = followed <- - User.get_cached_by_ap_id(Containment.get_actor(%{"actor" => followed})), - {:ok, %User{} = follower} <- - User.get_or_fetch_by_ap_id(Containment.get_actor(%{"actor" => follower})), - {:ok, activity} <- - ActivityPub.follow(follower, followed, id, false, skip_notify_and_stream: true) do - with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), - {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, - {_, 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")}, - {:ok, _relationship} <- - FollowingRelationship.update(follower, followed, :follow_accept) do - ActivityPub.accept(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - else - {:user_blocked, true} -> - {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - - ActivityPub.reject(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - - {:follow, {:error, _}} -> - {:ok, _} = Utils.update_follow_state_for_all(activity, "reject") - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_reject) - - ActivityPub.reject(%{ - to: [follower.ap_id], - actor: followed, - object: data, - local: true - }) - - {:user_locked, true} -> - {:ok, _relationship} = FollowingRelationship.update(follower, followed, :follow_pending) - :noop - end - - ActivityPub.notify_and_stream(activity) - {:ok, activity} - else - _e -> - :error - end - end - - def handle_incoming( %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data, _options ) do @@ -684,7 +636,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do %{"type" => type} = data, _options ) - when type in ~w{Update Block} do + when type in ~w{Update Block Follow} do with {:ok, %User{}} <- ObjectValidator.fetch_actor(data), {:ok, activity, _} <- Pipeline.common_pipeline(data, local: false) do {:ok, activity} diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 9bde8fc0d..b8c527606 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -203,14 +203,23 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do security: [%{"oAuth" => ["follow", "write:follows"]}], description: "Follow the given account", parameters: [ - %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}, - Operation.parameter( - :reblogs, - :query, - BooleanLike, - "Receive this account's reblogs in home timeline? Defaults to true." - ) + %Reference{"$ref": "#/components/parameters/accountIdOrNickname"} ], + requestBody: + request_body( + "Parameters", + %Schema{ + type: :object, + properties: %{ + reblogs: %Schema{ + type: :boolean, + description: "Receive this account's reblogs in home timeline? Defaults to true.", + default: true + } + } + }, + required: false + ), responses: %{ 200 => Operation.response("Relationship", "application/json", AccountRelationship), 400 => Operation.response("Error", "application/json", ApiError), @@ -438,6 +447,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do } end + # TODO: This is actually a token respone, but there's no oauth operation file yet. defp create_response do %Schema{ title: "AccountCreateResponse", @@ -446,14 +456,20 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do properties: %{ token_type: %Schema{type: :string}, access_token: %Schema{type: :string}, - scope: %Schema{type: :array, items: %Schema{type: :string}}, - created_at: %Schema{type: :integer, format: :"date-time"} + refresh_token: %Schema{type: :string}, + scope: %Schema{type: :string}, + created_at: %Schema{type: :integer, format: :"date-time"}, + me: %Schema{type: :string}, + expires_in: %Schema{type: :integer} }, example: %{ + "token_type" => "Bearer", "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk", + "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz", "created_at" => 1_585_918_714, - "scope" => ["read", "write", "follow", "push"], - "token_type" => "Bearer" + "expires_in" => 600, + "scope" => "read write follow push", + "me" => "https://gensokyo.2hu/users/raymoo" } } end diff --git a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex index 90922c064..97836b2eb 100644 --- a/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/pleroma_account_operation.ex @@ -4,7 +4,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do alias OpenApiSpex.Operation - alias OpenApiSpex.Schema alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship alias Pleroma.Web.ApiSpec.Schemas.ApiError alias Pleroma.Web.ApiSpec.Schemas.FlakeID @@ -40,48 +39,6 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do } end - def update_avatar_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user avatar image", - operationId: "PleromaAPI.AccountController.update_avatar", - requestBody: - request_body("Parameters", update_avatar_or_background_request(), required: true), - security: [%{"oAuth" => ["write:accounts"]}], - responses: %{ - 200 => update_response(), - 403 => Operation.response("Forbidden", "application/json", ApiError) - } - } - end - - def update_banner_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user banner image", - operationId: "PleromaAPI.AccountController.update_banner", - requestBody: request_body("Parameters", update_banner_request(), required: true), - security: [%{"oAuth" => ["write:accounts"]}], - responses: %{ - 200 => update_response() - } - } - end - - def update_background_operation do - %Operation{ - tags: ["Accounts"], - summary: "Set/clear user background image", - operationId: "PleromaAPI.AccountController.update_background", - security: [%{"oAuth" => ["write:accounts"]}], - requestBody: - request_body("Parameters", update_avatar_or_background_request(), required: true), - responses: %{ - 200 => update_response() - } - } - end - def favourites_operation do %Operation{ tags: ["Accounts"], @@ -136,52 +93,4 @@ defmodule Pleroma.Web.ApiSpec.PleromaAccountOperation do required: true ) end - - defp update_avatar_or_background_request do - %Schema{ - title: "PleromaAccountUpdateAvatarOrBackgroundRequest", - type: :object, - properties: %{ - img: %Schema{ - nullable: true, - type: :string, - format: :binary, - description: "Image encoded using `multipart/form-data` or an empty string to clear" - } - } - } - end - - defp update_banner_request do - %Schema{ - title: "PleromaAccountUpdateBannerRequest", - type: :object, - properties: %{ - banner: %Schema{ - type: :string, - nullable: true, - format: :binary, - description: "Image encoded using `multipart/form-data` or an empty string to clear" - } - } - } - end - - defp update_response do - Operation.response("PleromaAccountUpdateResponse", "application/json", %Schema{ - type: :object, - properties: %{ - url: %Schema{ - type: :string, - format: :uri, - nullable: true, - description: "Image URL" - } - }, - example: %{ - "url" => - "https://cofe.party/media/9d0add56-bcb6-4c0f-8225-cbbd0b6dd773/13eadb6972c9ccd3f4ffa3b8196f0e0d38b4d2f27594457c52e52946c054cd9a.gif" - } - }) - end end diff --git a/lib/pleroma/web/api_spec/operations/status_operation.ex b/lib/pleroma/web/api_spec/operations/status_operation.ex index 0b7fad793..5bd4619d5 100644 --- a/lib/pleroma/web/api_spec/operations/status_operation.ex +++ b/lib/pleroma/web/api_spec/operations/status_operation.ex @@ -84,7 +84,7 @@ defmodule Pleroma.Web.ApiSpec.StatusOperation do operationId: "StatusController.delete", parameters: [id_param()], responses: %{ - 200 => empty_object_response(), + 200 => status_response(), 403 => Operation.response("Forbidden", "application/json", ApiError), 404 => Operation.response("Not Found", "application/json", ApiError) } diff --git a/lib/pleroma/web/api_spec/schemas/account.ex b/lib/pleroma/web/api_spec/schemas/account.ex index 84f18f1b6..e6f163cb7 100644 --- a/lib/pleroma/web/api_spec/schemas/account.ex +++ b/lib/pleroma/web/api_spec/schemas/account.ex @@ -102,6 +102,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Account do type: :object, description: "A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`" + }, + favicon: %Schema{ + type: :string, + format: :uri, + nullable: true, + description: "Favicon image of the user's instance" } } }, diff --git a/lib/pleroma/web/api_spec/schemas/status.ex b/lib/pleroma/web/api_spec/schemas/status.ex index 28cde963e..947e42890 100644 --- a/lib/pleroma/web/api_spec/schemas/status.ex +++ b/lib/pleroma/web/api_spec/schemas/status.ex @@ -62,6 +62,11 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Status do } }, content: %Schema{type: :string, format: :html, description: "HTML-encoded status content"}, + text: %Schema{ + type: :string, + description: "Original unformatted content in plain text", + nullable: true + }, created_at: %Schema{ type: :string, format: "date-time", diff --git a/lib/pleroma/web/common_api/activity_draft.ex b/lib/pleroma/web/common_api/activity_draft.ex index 9bcb9f587..f849b2e01 100644 --- a/lib/pleroma/web/common_api/activity_draft.ex +++ b/lib/pleroma/web/common_api/activity_draft.ex @@ -186,6 +186,7 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do draft.poll ) |> Map.put("emoji", emoji) + |> Map.put("source", draft.status) %__MODULE__{draft | object: object} end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index fd7149079..4d5b0decf 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -101,10 +101,14 @@ defmodule Pleroma.Web.CommonAPI do def follow(follower, followed) do timeout = Pleroma.Config.get([:activitypub, :follow_handshake_timeout]) - with {:ok, follower} <- User.maybe_direct_follow(follower, followed), - {:ok, activity} <- ActivityPub.follow(follower, followed), + with {:ok, follow_data, _} <- Builder.follow(follower, followed), + {:ok, activity, _} <- Pipeline.common_pipeline(follow_data, local: true), {:ok, follower, followed} <- User.wait_and_refresh(timeout, follower, followed) do - {:ok, follower, followed, activity} + if activity.data["state"] == "reject" do + {:error, :rejected} + else + {:ok, follower, followed, activity} + end end end diff --git a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex index b5008d69b..743442855 100644 --- a/lib/pleroma/web/mastodon_api/controllers/account_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/account_controller.ex @@ -27,6 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do alias Pleroma.Web.MastodonAPI.MastodonAPI alias Pleroma.Web.MastodonAPI.MastodonAPIController alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Token alias Pleroma.Web.TwitterAPI.TwitterAPI @@ -101,12 +102,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do :ok <- TwitterAPI.validate_captcha(app, params), {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do - json(conn, %{ - token_type: "Bearer", - access_token: token.token, - scope: app.scopes, - created_at: Token.Utils.format_created_at(token) - }) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else {:error, error} -> json_response(conn, :bad_request, %{error: error}) end @@ -148,6 +144,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> Enum.filter(fn {_, value} -> not is_nil(value) end) |> Enum.into(%{}) + # We use an empty string as a special value to reset + # avatars, banners, backgrounds + user_image_value = fn + "" -> {:ok, nil} + value -> {:ok, value} + end + user_params = [ :no_rich_text, @@ -168,9 +171,9 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do |> Maps.put_if_present(:name, params[:display_name]) |> Maps.put_if_present(:bio, params[:note]) |> Maps.put_if_present(:raw_bio, params[:note]) - |> Maps.put_if_present(:avatar, params[:avatar]) - |> Maps.put_if_present(:banner, params[:header]) - |> Maps.put_if_present(:background, params[:pleroma_background_image]) + |> Maps.put_if_present(:avatar, params[:avatar], user_image_value) + |> Maps.put_if_present(:banner, params[:header], user_image_value) + |> Maps.put_if_present(:background, params[:pleroma_background_image], user_image_value) |> Maps.put_if_present( :raw_fields, params[:fields_attributes], @@ -346,7 +349,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do {:error, "Can not follow yourself"} end - def follow(%{assigns: %{user: follower, account: followed}} = conn, params) do + def follow(%{body_params: params, assigns: %{user: follower, account: followed}} = conn, _) do with {:ok, follower} <- MastodonAPI.follow(follower, followed, params) do render(conn, "relationship.json", user: follower, target: followed) else diff --git a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex index e50980122..29affa7d5 100644 --- a/lib/pleroma/web/mastodon_api/controllers/search_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/search_controller.ex @@ -44,6 +44,7 @@ defmodule Pleroma.Web.MastodonAPI.SearchController do def search(conn, params), do: do_search(:v1, conn, params) defp do_search(version, %{assigns: %{user: user}} = conn, %{q: query} = params) do + query = String.trim(query) options = search_options(params, user) timeout = Keyword.get(Repo.config(), :timeout, 15_000) default_values = %{"statuses" => [], "accounts" => [], "hashtags" => []} diff --git a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex index 468b44b67..12be530c9 100644 --- a/lib/pleroma/web/mastodon_api/controllers/status_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/status_controller.ex @@ -200,11 +200,16 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do @doc "DELETE /api/v1/statuses/:id" def delete(%{assigns: %{user: user}} = conn, %{id: id}) do - with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do - json(conn, %{}) + with %Activity{} = activity <- Activity.get_by_id_with_object(id), + {:ok, %Activity{}} <- CommonAPI.delete(id, user) do + try_render(conn, "show.json", + activity: activity, + for: user, + with_direct_conversation_id: true, + with_source: true + ) else - {:error, :not_found} = e -> e - _e -> render_error(conn, :forbidden, "Can't delete this post") + _e -> {:error, :not_found} end end diff --git a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex index 4bdd46d7e..ab7b1d6aa 100644 --- a/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex +++ b/lib/pleroma/web/mastodon_api/controllers/timeline_controller.ex @@ -88,21 +88,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do ) end + defp restrict_unauthenticated?(true = _local_only) do + Pleroma.Config.get([:restrict_unauthenticated, :timelines, :local]) + end + + defp restrict_unauthenticated?(_) do + Pleroma.Config.get([:restrict_unauthenticated, :timelines, :federated]) + end + # GET /api/v1/timelines/public def public(%{assigns: %{user: user}} = conn, params) do local_only = params[:local] - cfg_key = - if local_only do - :local - else - :federated - end - - restrict? = Pleroma.Config.get([:restrict_unauthenticated, :timelines, cfg_key]) - - if restrict? and is_nil(user) do - render_error(conn, :unauthorized, "authorization required for timeline view") + if is_nil(user) and restrict_unauthenticated?(local_only) do + fail_on_bad_auth(conn) else activities = params @@ -123,6 +122,10 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do end end + defp fail_on_bad_auth(conn) do + render_error(conn, :unauthorized, "authorization required for timeline view") + end + defp hashtag_fetching(params, user, local_only) do tags = [params[:tag], params[:any]] @@ -157,15 +160,20 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do # GET /api/v1/timelines/tag/:tag def hashtag(%{assigns: %{user: user}} = conn, params) do local_only = params[:local] - activities = hashtag_fetching(params, user, local_only) - conn - |> add_link_headers(activities, %{"local" => local_only}) - |> render("index.json", - activities: activities, - for: user, - as: :activity - ) + if is_nil(user) and restrict_unauthenticated?(local_only) do + fail_on_bad_auth(conn) + else + activities = hashtag_fetching(params, user, local_only) + + conn + |> add_link_headers(activities, %{"local" => local_only}) + |> render("index.json", + activities: activities, + for: user, + as: :activity + ) + end end # GET /api/v1/timelines/list/:list_id diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index a6e64b4ab..2feba4778 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -204,6 +204,18 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do %{} end + favicon = + if Pleroma.Config.get([:instances_favicons, :enabled]) do + user + |> Map.get(:ap_id, "") + |> URI.parse() + |> URI.merge("/") + |> Pleroma.Instances.Instance.get_or_update_favicon() + |> MediaProxy.url() + else + nil + end + %{ id: to_string(user.id), username: username_from_nickname(user.nickname), @@ -245,7 +257,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do hide_favorites: user.hide_favorites, relationship: relationship, skip_thread_containment: user.skip_thread_containment, - background_image: image_url(user.background) |> MediaProxy.url() + background_image: image_url(user.background) |> MediaProxy.url(), + favicon: favicon } } |> maybe_put_role(user, opts[:for]) diff --git a/lib/pleroma/web/mastodon_api/views/instance_view.ex b/lib/pleroma/web/mastodon_api/views/instance_view.ex index 35c2fc25c..5deb0d7ed 100644 --- a/lib/pleroma/web/mastodon_api/views/instance_view.ex +++ b/lib/pleroma/web/mastodon_api/views/instance_view.ex @@ -34,10 +34,14 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do background_upload_limit: Keyword.get(instance, :background_upload_limit), banner_upload_limit: Keyword.get(instance, :banner_upload_limit), background_image: Keyword.get(instance, :background_image), + chat_limit: Keyword.get(instance, :chat_limit), + description_limit: Keyword.get(instance, :description_limit), pleroma: %{ metadata: %{ + account_activation_required: Keyword.get(instance, :account_activation_required), features: features(), - federation: federation() + federation: federation(), + fields_limits: fields_limits() }, vapid_public_key: Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) } @@ -88,4 +92,13 @@ defmodule Pleroma.Web.MastodonAPI.InstanceView do end |> Map.put(:enabled, Config.get([:instance, :federating])) end + + def fields_limits do + %{ + max_fields: Config.get([:instance, :max_account_fields]), + max_remote_fields: Config.get([:instance, :max_remote_account_fields]), + name_length: Config.get([:instance, :account_field_name_length]), + value_length: Config.get([:instance, :account_field_value_length]) + } + end end diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 6ee17f4dd..fa9d695f3 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -333,6 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do reblog: nil, card: card, content: content_html, + text: opts[:with_source] && object.data["source"], created_at: created_at, reblogs_count: announcement_count, replies_count: object.data["repliesCount"] || 0, diff --git a/lib/pleroma/web/oauth/mfa_controller.ex b/lib/pleroma/web/oauth/mfa_controller.ex index 53e19f82e..f102c93e7 100644 --- a/lib/pleroma/web/oauth/mfa_controller.ex +++ b/lib/pleroma/web/oauth/mfa_controller.ex @@ -13,6 +13,7 @@ defmodule Pleroma.Web.OAuth.MFAController do alias Pleroma.Web.Auth.TOTPAuthenticator alias Pleroma.Web.OAuth.MFAView, as: View alias Pleroma.Web.OAuth.OAuthController + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Token plug(:fetch_session when action in [:show, :verify]) @@ -74,7 +75,7 @@ defmodule Pleroma.Web.OAuth.MFAController do {:ok, %{user: user, authorization: auth}} <- MFA.Token.validate(mfa_token), {:ok, _} <- validates_challenge(user, params), {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> conn diff --git a/lib/pleroma/web/oauth/mfa_view.ex b/lib/pleroma/web/oauth/mfa_view.ex index 41d5578dc..5d87db268 100644 --- a/lib/pleroma/web/oauth/mfa_view.ex +++ b/lib/pleroma/web/oauth/mfa_view.ex @@ -5,4 +5,13 @@ defmodule Pleroma.Web.OAuth.MFAView do use Pleroma.Web, :view import Phoenix.HTML.Form + alias Pleroma.MFA + + def render("mfa_response.json", %{token: token, user: user}) do + %{ + error: "mfa_required", + mfa_token: token.token, + supported_challenge_types: MFA.supported_methods(user) + } + end end diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index c557778ca..7683589cf 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Web.OAuth.OAuthController do alias Pleroma.Web.OAuth.App alias Pleroma.Web.OAuth.Authorization alias Pleroma.Web.OAuth.MFAController + alias Pleroma.Web.OAuth.MFAView + alias Pleroma.Web.OAuth.OAuthView alias Pleroma.Web.OAuth.Scopes alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token.Strategy.RefreshToken @@ -233,9 +235,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do with {:ok, app} <- Token.Utils.fetch_app(conn), {:ok, %{user: user} = token} <- Token.get_by_refresh_token(app, token), {:ok, token} <- RefreshToken.grant(token) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else _error -> render_invalid_credentials_error(conn) end @@ -247,9 +247,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do {:ok, auth} <- Authorization.get_by_token(app, fixed_token), %User{} = user <- User.get_cached_by_id(auth.user_id), {:ok, token} <- Token.exchange_token(app, auth) do - response_attrs = %{created_at: Token.Utils.format_created_at(token)} - - json(conn, Token.Response.build(user, token, response_attrs)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -267,7 +265,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do {:ok, auth} <- Authorization.create_authorization(app, user, scopes), {:mfa_required, _, _, false} <- {:mfa_required, user, auth, MFA.require?(user)}, {:ok, token} <- Token.exchange_token(app, auth) do - json(conn, Token.Response.build(user, token)) + json(conn, OAuthView.render("token.json", %{user: user, token: token})) else error -> handle_token_exchange_error(conn, error) @@ -290,7 +288,7 @@ defmodule Pleroma.Web.OAuth.OAuthController 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)) + json(conn, OAuthView.render("token.json", %{token: token})) else _error -> handle_token_exchange_error(conn, :invalid_credentails) @@ -548,7 +546,7 @@ defmodule Pleroma.Web.OAuth.OAuthController do defp build_and_response_mfa_token(user, auth) do with {:ok, token} <- MFA.Token.create_token(user, auth) do - Token.Response.build_for_mfa_token(user, token) + MFAView.render("mfa_response.json", %{token: token, user: user}) end end diff --git a/lib/pleroma/web/oauth/oauth_view.ex b/lib/pleroma/web/oauth/oauth_view.ex index 94ddaf913..f55247ebd 100644 --- a/lib/pleroma/web/oauth/oauth_view.ex +++ b/lib/pleroma/web/oauth/oauth_view.ex @@ -5,4 +5,26 @@ defmodule Pleroma.Web.OAuth.OAuthView do use Pleroma.Web, :view import Phoenix.HTML.Form + + alias Pleroma.Web.OAuth.Token.Utils + + def render("token.json", %{token: token} = opts) do + response = %{ + token_type: "Bearer", + access_token: token.token, + refresh_token: token.refresh_token, + expires_in: expires_in(), + scope: Enum.join(token.scopes, " "), + created_at: Utils.format_created_at(token) + } + + if user = opts[:user] do + response + |> Map.put(:me, user.ap_id) + else + response + end + end + + 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 deleted file mode 100644 index 0e72c31e9..000000000 --- a/lib/pleroma/web/oauth/token/response.ex +++ /dev/null @@ -1,45 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.OAuth.Token.Response do - @moduledoc false - - alias Pleroma.MFA - alias Pleroma.User - alias Pleroma.Web.OAuth.Token.Utils - - @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(), - scope: Enum.join(token.scopes, " "), - me: user.ap_id - } - |> Map.merge(opts) - end - - def build_for_client_credentials(token) do - %{ - token_type: "Bearer", - access_token: token.token, - refresh_token: token.refresh_token, - created_at: Utils.format_created_at(token), - expires_in: expires_in(), - scope: Enum.join(token.scopes, " ") - } - end - - def build_for_mfa_token(user, mfa_token) do - %{ - error: "mfa_required", - mfa_token: mfa_token.token, - supported_challenge_types: MFA.supported_methods(user) - } - end - - defp expires_in, do: Pleroma.Config.get([:oauth2, :token_expires_in], 600) -end diff --git a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex index f3554d919..563edded7 100644 --- a/lib/pleroma/web/pleroma_api/controllers/account_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/account_controller.ex @@ -8,7 +8,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do import Pleroma.Web.ControllerHelper, only: [json_response: 3, add_link_headers: 2, assign_account_by_id: 2] - alias Ecto.Changeset alias Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.RateLimiter @@ -37,17 +36,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do plug( OAuthScopesPlug, - %{scopes: ["write:accounts"]} - # Note: the following actions are not permission-secured in Mastodon: - when action in [ - :update_avatar, - :update_banner, - :update_background - ] - ) - - plug( - OAuthScopesPlug, %{scopes: ["read:favourites"], fallback: :proceed_unauthenticated} when action == :favourites ) @@ -68,56 +56,6 @@ defmodule Pleroma.Web.PleromaAPI.AccountController do end end - @doc "PATCH /api/v1/pleroma/accounts/update_avatar" - def update_avatar(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do - {:ok, _user} = - user - |> Changeset.change(%{avatar: nil}) - |> User.update_and_set_cache() - - json(conn, %{url: nil}) - end - - def update_avatar(%{assigns: %{user: user}, body_params: params} = conn, _params) do - {:ok, %{data: data}} = ActivityPub.upload(params, type: :avatar) - {:ok, _user} = user |> Changeset.change(%{avatar: data}) |> User.update_and_set_cache() - %{"url" => [%{"href" => href} | _]} = data - - json(conn, %{url: href}) - end - - @doc "PATCH /api/v1/pleroma/accounts/update_banner" - def update_banner(%{assigns: %{user: user}, body_params: %{banner: ""}} = conn, _) do - with {:ok, _user} <- User.update_banner(user, %{}) do - json(conn, %{url: nil}) - end - end - - def update_banner(%{assigns: %{user: user}, body_params: params} = conn, _) do - with {:ok, object} <- ActivityPub.upload(%{img: params[:banner]}, type: :banner), - {:ok, _user} <- User.update_banner(user, object.data) do - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - - @doc "PATCH /api/v1/pleroma/accounts/update_background" - def update_background(%{assigns: %{user: user}, body_params: %{img: ""}} = conn, _) do - with {:ok, _user} <- User.update_background(user, %{}) do - json(conn, %{url: nil}) - end - end - - def update_background(%{assigns: %{user: user}, body_params: params} = conn, _) do - with {:ok, object} <- ActivityPub.upload(params, type: :background), - {:ok, _user} <- User.update_background(user, object.data) do - %{"url" => [%{"href" => href} | _]} = object.data - - json(conn, %{url: href}) - end - end - @doc "GET /api/v1/pleroma/accounts/:id/favourites" def favourites(%{assigns: %{account: %{hide_favorites: true}}} = conn, _params) do render_error(conn, :forbidden, "Can't get favorites") diff --git a/lib/pleroma/web/preload/status_net.ex b/lib/pleroma/web/preload/status_net.ex deleted file mode 100644 index 9b62f87a2..000000000 --- a/lib/pleroma/web/preload/status_net.ex +++ /dev/null @@ -1,25 +0,0 @@ -# Pleroma: A lightweight social networking server -# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> -# SPDX-License-Identifier: AGPL-3.0-only - -defmodule Pleroma.Web.Preload.Providers.StatusNet do - alias Pleroma.Web.Preload.Providers.Provider - alias Pleroma.Web.TwitterAPI.UtilController - - @behaviour Provider - @config_url "/api/statusnet/config.json" - - @impl Provider - def generate_terms(_params) do - %{} - |> build_config_tag() - end - - defp build_config_tag(acc) do - resp = - Plug.Test.conn(:get, @config_url |> to_string()) - |> UtilController.config(nil) - - Map.put(acc, @config_url, resp.resp_body) - end -end diff --git a/lib/pleroma/web/rich_media/parser.ex b/lib/pleroma/web/rich_media/parser.ex index ef5ead2da..c8a767935 100644 --- a/lib/pleroma/web/rich_media/parser.ex +++ b/lib/pleroma/web/rich_media/parser.ex @@ -86,7 +86,10 @@ defmodule Pleroma.Web.RichMedia.Parser do end try do - {:ok, %Tesla.Env{body: html}} = Pleroma.HTTP.get(url, [], adapter: opts) + rich_media_agent = Pleroma.Application.user_agent() <> "; Bot" + + {:ok, %Tesla.Env{body: html}} = + Pleroma.HTTP.get(url, [{"user-agent", rich_media_agent}], adapter: opts) html |> parse_html() diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c837e83fe..e25ffd317 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -328,10 +328,6 @@ defmodule Pleroma.Web.Router do delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete) post("/notifications/read", NotificationController, :mark_as_read) - patch("/accounts/update_avatar", AccountController, :update_avatar) - patch("/accounts/update_banner", AccountController, :update_banner) - patch("/accounts/update_background", AccountController, :update_background) - get("/mascot", MascotController, :show) put("/mascot", MascotController, :update) @@ -517,10 +513,6 @@ defmodule Pleroma.Web.Router do scope "/api", Pleroma.Web do pipe_through(:config) - get("/help/test", TwitterAPI.UtilController, :help_test) - post("/help/test", TwitterAPI.UtilController, :help_test) - get("/statusnet/config", TwitterAPI.UtilController, :config) - get("/statusnet/version", TwitterAPI.UtilController, :version) get("/pleroma/frontend_configurations", TwitterAPI.UtilController, :frontend_configurations) end diff --git a/lib/pleroma/web/streamer/streamer.ex b/lib/pleroma/web/streamer/streamer.ex index 73ee3e1e1..d1d70e556 100644 --- a/lib/pleroma/web/streamer/streamer.ex +++ b/lib/pleroma/web/streamer/streamer.ex @@ -104,7 +104,9 @@ defmodule Pleroma.Web.Streamer do :ok end - def filtered_by_user?(%User{} = user, %Activity{} = item) do + def filtered_by_user?(user, item, streamed_type \\ :activity) + + def filtered_by_user?(%User{} = user, %Activity{} = item, streamed_type) do %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute]) @@ -116,7 +118,9 @@ defmodule Pleroma.Web.Streamer do true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(item.actor not in &1)), true <- item.data["type"] != "Announce" || item.actor not in reblog_muted_ap_ids, - true <- !(item.data["type"] == "Announce" && parent.data["actor"] == user.ap_id), + true <- + !(streamed_type == :activity && item.data["type"] == "Announce" && + parent.data["actor"] == user.ap_id), true <- Enum.all?([blocked_ap_ids, muted_ap_ids], &(parent.data["actor"] not in &1)), true <- MapSet.disjoint?(recipients, recipient_blocks), %{host: item_host} <- URI.parse(item.actor), @@ -131,8 +135,8 @@ defmodule Pleroma.Web.Streamer do end end - def filtered_by_user?(%User{} = user, %Notification{activity: activity}) do - filtered_by_user?(user, activity) + def filtered_by_user?(%User{} = user, %Notification{activity: activity}, _) do + filtered_by_user?(user, activity, :notification) end defp do_stream("direct", item) do diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index aaca182ec..8314e75b4 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -13,9 +13,7 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Notification alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.User - alias Pleroma.Web alias Pleroma.Web.CommonAPI - alias Pleroma.Web.TwitterAPI.UtilView alias Pleroma.Web.WebFinger plug(Pleroma.Web.FederatingPlug when action == :remote_subscribe) @@ -42,12 +40,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :notifications_read) - plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) - - def help_test(conn, _params) do - json(conn, "ok") - end - def remote_subscribe(conn, %{"nickname" => nick, "profile" => _}) do with %User{} = user <- User.get_cached_by_nickname(nick), avatar = User.avatar_url(user) do @@ -89,60 +81,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do end end - def config(%{assigns: %{format: "xml"}} = conn, _params) do - instance = Pleroma.Config.get(:instance) - response = UtilView.status_net_config(instance) - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) - end - - def config(conn, _params) do - instance = Pleroma.Config.get(:instance) - - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), - private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - bool_to_val(Keyword.get(instance, :account_activation_required, false)), - invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), - safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) - } - - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) - Map.put(data, "pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end - - defp bool_to_val(true), do: "1" - defp bool_to_val(_), do: "0" - defp bool_to_val(true, val, _), do: val - defp bool_to_val(_, _, val), do: val - def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) @@ -151,18 +89,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do json(conn, config) end - def version(%{assigns: %{format: "xml"}} = conn, _params) do - version = Pleroma.Application.named_version() - - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, "<version>#{version}</version>") - end - - def version(conn, _params) do - json(conn, Pleroma.Application.named_version()) - end - def emoji(conn, _params) do emoji = Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc -> |