From 88b16fdfb7b40877aecae5d45f6f3a1c54362f13 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sat, 11 Apr 2020 16:01:09 +0300 Subject: [#1364] Disabled notifications on activities from blocked domains. --- lib/pleroma/notification.ex | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 04ee510b9..02363ddb0 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -321,10 +321,11 @@ defmodule Pleroma.Notification do @doc """ Returns a tuple with 2 elements: - {enabled notification receivers, currently disabled receivers (blocking / [thread] muting)} + {notification-enabled receivers, currently disabled receivers (blocking / [thread] muting)} NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1 """ + @spec get_notified_from_activity(Activity.t(), boolean()) :: {list(User.t()), list(User.t())} def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) @@ -337,17 +338,22 @@ defmodule Pleroma.Notification do |> Utils.maybe_notify_followers(activity) |> Enum.uniq() - # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs - notification_enabled_ap_ids = - potential_receiver_ap_ids - |> exclude_relationship_restricted_ap_ids(activity) - |> exclude_thread_muter_ap_ids(activity) - potential_receivers = potential_receiver_ap_ids |> Enum.uniq() |> User.get_users_from_set(local_only) + activity_actor_domain = activity.actor && URI.parse(activity.actor).host + + notification_enabled_ap_ids = + for u <- potential_receivers, activity_actor_domain not in u.domain_blocks, do: u.ap_id + + # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs + notification_enabled_ap_ids = + notification_enabled_ap_ids + |> exclude_relationship_restricted_ap_ids(activity) + |> exclude_thread_muter_ap_ids(activity) + notification_enabled_users = Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end) -- cgit v1.2.3 From c556efb761a3e7fc2beb4540d6f58dbfe8e4abfe Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Sun, 12 Apr 2020 21:53:03 +0300 Subject: [#1364] Enabled notifications on followed domain-blocked users' activities. --- lib/pleroma/following_relationship.ex | 35 ++++++++++++++++++--- lib/pleroma/notification.ex | 59 ++++++++++++++++++++++++++++------- 2 files changed, 78 insertions(+), 16 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index a9538ea4e..11e06c5cc 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -69,6 +69,29 @@ defmodule Pleroma.FollowingRelationship do |> Repo.aggregate(:count, :id) end + def followers_query(%User{} = user) do + __MODULE__ + |> join(:inner, [r], u in User, on: r.follower_id == u.id) + |> where([r], r.following_id == ^user.id) + |> where([r], r.state == "accept") + end + + def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do + query = + user + |> followers_query() + |> select([r, u], u.ap_id) + + query = + if from_ap_ids do + where(query, [r, u], u.ap_id in ^from_ap_ids) + else + query + end + + Repo.all(query) + end + def following_count(%User{id: nil}), do: 0 def following_count(%User{} = user) do @@ -92,12 +115,16 @@ defmodule Pleroma.FollowingRelationship do |> Repo.exists?() end + def following_query(%User{} = user) do + __MODULE__ + |> join(:inner, [r], u in User, on: r.following_id == u.id) + |> where([r], r.follower_id == ^user.id) + |> where([r], r.state == "accept") + end + def following(%User{} = user) do following = - __MODULE__ - |> join(:inner, [r], u in User, on: r.following_id == u.id) - |> where([r], r.follower_id == ^user.id) - |> where([r], r.state == "accept") + following_query(user) |> select([r, u], u.follower_address) |> Repo.all() diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 02363ddb0..da05ff2e4 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -6,6 +6,7 @@ defmodule Pleroma.Notification do use Ecto.Schema alias Pleroma.Activity + alias Pleroma.FollowingRelationship alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Pagination @@ -81,6 +82,7 @@ defmodule Pleroma.Notification do |> exclude_visibility(opts) end + # Excludes blocked users and non-followed domain-blocked users defp exclude_blocked(query, user, opts) do blocked_ap_ids = opts[:blocked_users_ap_ids] || User.blocked_users_ap_ids(user) @@ -88,7 +90,16 @@ defmodule Pleroma.Notification do |> where([n, a], a.actor not in ^blocked_ap_ids) |> where( [n, a], - fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks + fragment( + # "NOT (actor's domain in domain_blocks) OR (actor is in followed AP IDs)" + "NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR \ + ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr \ + ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = 'accept')", + a.actor, + ^user.domain_blocks, + a.actor, + ^User.binary_id(user.id) + ) ) end @@ -338,19 +349,11 @@ defmodule Pleroma.Notification do |> Utils.maybe_notify_followers(activity) |> Enum.uniq() - potential_receivers = - potential_receiver_ap_ids - |> Enum.uniq() - |> User.get_users_from_set(local_only) - - activity_actor_domain = activity.actor && URI.parse(activity.actor).host - - notification_enabled_ap_ids = - for u <- potential_receivers, activity_actor_domain not in u.domain_blocks, do: u.ap_id + potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only) - # Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs notification_enabled_ap_ids = - notification_enabled_ap_ids + potential_receiver_ap_ids + |> exclude_domain_blocker_ap_ids(activity, potential_receivers) |> exclude_relationship_restricted_ap_ids(activity) |> exclude_thread_muter_ap_ids(activity) @@ -362,6 +365,38 @@ defmodule Pleroma.Notification do def get_notified_from_activity(_, _local_only), do: {[], []} + @doc "Filters out AP IDs of users who domain-block and not follow activity actor" + def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ []) + + def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: [] + + def exclude_domain_blocker_ap_ids(ap_ids, %Activity{} = activity, preloaded_users) do + activity_actor_domain = activity.actor && URI.parse(activity.actor).host + + users = + ap_ids + |> Enum.map(fn ap_id -> + Enum.find(preloaded_users, &(&1.ap_id == ap_id)) || + User.get_cached_by_ap_id(ap_id) + end) + |> Enum.filter(& &1) + + domain_blocker_ap_ids = for u <- users, activity_actor_domain in u.domain_blocks, do: u.ap_id + + domain_blocker_follower_ap_ids = + if Enum.any?(domain_blocker_ap_ids) do + activity + |> Activity.user_actor() + |> FollowingRelationship.followers_ap_ids(domain_blocker_ap_ids) + else + [] + end + + ap_ids + |> Kernel.--(domain_blocker_ap_ids) + |> Kernel.++(domain_blocker_follower_ap_ids) + end + @doc "Filters out AP IDs of users basing on their relationships with activity actor user" def exclude_relationship_restricted_ap_ids([], _activity), do: [] -- cgit v1.2.3 From 99b0bc198921099816a5f809f11a7579b3993274 Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Mon, 13 Apr 2020 13:24:31 +0300 Subject: [#1364] Resolved merge conflicts with `develop`. Refactoring. --- lib/pleroma/following_relationship.ex | 34 ++++++++++++++++++++++++++++++++-- lib/pleroma/notification.ex | 14 +------------- 2 files changed, 33 insertions(+), 15 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/following_relationship.ex b/lib/pleroma/following_relationship.ex index 219a64352..3a3082e72 100644 --- a/lib/pleroma/following_relationship.ex +++ b/lib/pleroma/following_relationship.ex @@ -10,11 +10,12 @@ defmodule Pleroma.FollowingRelationship do alias Ecto.Changeset alias FlakeId.Ecto.CompatType + alias Pleroma.FollowingRelationship.State alias Pleroma.Repo alias Pleroma.User schema "following_relationships" do - field(:state, Pleroma.FollowingRelationship.State, default: :follow_pending) + field(:state, State, default: :follow_pending) belongs_to(:follower, User, type: CompatType) belongs_to(:following, User, type: CompatType) @@ -22,6 +23,11 @@ defmodule Pleroma.FollowingRelationship do timestamps() end + @doc "Returns underlying integer code for state atom" + def state_int_code(state_atom), do: State.__enum_map__() |> Keyword.fetch!(state_atom) + + def accept_state_code, do: state_int_code(:follow_accept) + def changeset(%__MODULE__{} = following_relationship, attrs) do following_relationship |> cast(attrs, [:state]) @@ -86,7 +92,7 @@ defmodule Pleroma.FollowingRelationship do __MODULE__ |> join(:inner, [r], u in User, on: r.follower_id == u.id) |> where([r], r.following_id == ^user.id) - |> where([r], r.state == "accept") + |> where([r], r.state == ^:follow_accept) end def followers_ap_ids(%User{} = user, from_ap_ids \\ nil) do @@ -198,6 +204,30 @@ defmodule Pleroma.FollowingRelationship do end) end + @doc """ + For a query with joined activity, + keeps rows where activity's actor is followed by user -or- is NOT domain-blocked by user. + """ + def keep_following_or_not_domain_blocked(query, user) do + where( + query, + [_, activity], + fragment( + # "(actor's domain NOT in domain_blocks) OR (actor IS in followed AP IDs)" + """ + NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR + ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr + ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = ?) + """, + activity.actor, + ^user.domain_blocks, + activity.actor, + ^User.binary_id(user.id), + ^accept_state_code() + ) + ) + end + defp validate_not_self_relationship(%Changeset{} = changeset) do changeset |> validate_follower_id_following_id_inequality() diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index da05ff2e4..b76dd176c 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -88,19 +88,7 @@ defmodule Pleroma.Notification do query |> where([n, a], a.actor not in ^blocked_ap_ids) - |> where( - [n, a], - fragment( - # "NOT (actor's domain in domain_blocks) OR (actor is in followed AP IDs)" - "NOT (substring(? from '.*://([^/]*)') = ANY(?)) OR \ - ? = ANY(SELECT ap_id FROM users AS u INNER JOIN following_relationships AS fr \ - ON u.id = fr.following_id WHERE fr.follower_id = ? AND fr.state = 'accept')", - a.actor, - ^user.domain_blocks, - a.actor, - ^User.binary_id(user.id) - ) - ) + |> FollowingRelationship.keep_following_or_not_domain_blocked(user) end defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do -- cgit v1.2.3 From f7e623c11c4b6f4f323a4317e9489092be73f9cd Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Tue, 14 Apr 2020 20:19:08 +0300 Subject: [#1364] Resolved merge conflicts with `develop`. --- lib/pleroma/notification.ex | 1 - 1 file changed, 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index f517282f7..b76dd176c 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -7,7 +7,6 @@ defmodule Pleroma.Notification do alias Pleroma.Activity alias Pleroma.FollowingRelationship - alias Pleroma.Marker alias Pleroma.Notification alias Pleroma.Object alias Pleroma.Pagination -- cgit v1.2.3 From b03aeae8b935b0a211c51cc3be5f1c15591f5a2f Mon Sep 17 00:00:00 2001 From: Ivan Tashkinov Date: Wed, 22 Apr 2020 15:31:41 +0000 Subject: Apply suggestion to lib/pleroma/notification.ex --- lib/pleroma/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'lib') diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index d305a43ba..aaa675253 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -362,7 +362,7 @@ defmodule Pleroma.Notification do def get_notified_from_activity(_, _local_only), do: {[], []} - @doc "Filters out AP IDs of users who domain-block and not follow activity actor" + @doc "Filters out AP IDs domain-blocking and not following the activity's actor" def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ []) def exclude_domain_blocker_ap_ids([], _activity, _preloaded_users), do: [] -- cgit v1.2.3 From 8cf3a32463856f91a4e64a5cd33b5538b67c25c9 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 30 Apr 2020 00:49:59 +0300 Subject: Add exlude_replies to OpenAPI spec for account timelines --- lib/pleroma/web/api_spec/operations/account_operation.ex | 1 + 1 file changed, 1 insertion(+) (limited to 'lib') diff --git a/lib/pleroma/web/api_spec/operations/account_operation.ex b/lib/pleroma/web/api_spec/operations/account_operation.ex index 64e2e43c4..d3e8bd484 100644 --- a/lib/pleroma/web/api_spec/operations/account_operation.ex +++ b/lib/pleroma/web/api_spec/operations/account_operation.ex @@ -131,6 +131,7 @@ defmodule Pleroma.Web.ApiSpec.AccountOperation do "Include statuses from muted acccounts." ), Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"), + Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"), Operation.parameter( :exclude_visibilities, :query, -- cgit v1.2.3 From 5839e67eb86d6d14b21222247ce8e113c3b26637 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 6 Feb 2020 18:01:12 +0300 Subject: return data only for updated emoji --- .../controllers/emoji_api_controller.ex | 89 +++++++++++++--------- 1 file changed, 51 insertions(+), 38 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index e01825b48..981bac4fa 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -385,23 +385,35 @@ keeping it in cache for #{div(cache_ms, 1000)}s") json(conn, new_data) end - defp get_filename(%{"filename" => filename}), do: filename + defp get_filename(%Plug.Upload{filename: filename}), do: filename + defp get_filename(url) when is_binary(url), do: Path.basename(url) - defp get_filename(%{"file" => file}) do - case file do - %Plug.Upload{filename: filename} -> filename - url when is_binary(url) -> Path.basename(url) + defp empty?(str), do: String.trim(str) == "" + + defp update_pack_file(updated_full_pack, pack_file_p) do + content = Jason.encode!(updated_full_pack, pretty: true) + + File.write!(pack_file_p, content) + end + + defp create_subdirs(file_path) do + if String.contains?(file_path, "/") do + file_path + |> Path.dirname() + |> File.mkdir_p!() end end - defp empty?(str), do: String.trim(str) == "" + defp pack_info(pack_name) do + dir = Path.join(emoji_dir_path(), pack_name) + json_path = Path.join(dir, "pack.json") - defp update_file_and_send(conn, updated_full_pack, pack_file_p) do - # Write the emoji pack file - File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true)) + json = + json_path + |> File.read!() + |> Jason.decode!() - # Return the modified file list - json(conn, updated_full_pack["files"]) + {dir, json_path, json} end @doc """ @@ -422,23 +434,25 @@ keeping it in cache for #{div(cache_ms, 1000)}s") # Add def update_file( conn, - %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params + %{"pack_name" => pack_name, "action" => "add"} = params ) do - pack_dir = Path.join(emoji_dir_path(), pack_name) - pack_file_p = Path.join(pack_dir, "pack.json") + shortcode = + if params["shortcode"] do + params["shortcode"] + else + filename = get_filename(params["file"]) + Path.basename(filename, Path.extname(filename)) + end - full_pack = Jason.decode!(File.read!(pack_file_p)) + {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)}, - filename <- get_filename(params), + filename <- params["filename"] || get_filename(params["file"]), false <- empty?(shortcode), - false <- empty?(filename) do - file_path = Path.join(pack_dir, filename) - + false <- empty?(filename), + file_path <- Path.join(pack_dir, filename) do # If the name contains directories, create them - if String.contains?(file_path, "/") do - File.mkdir_p!(Path.dirname(file_path)) - end + create_subdirs(file_path) case params["file"] do %Plug.Upload{path: upload_path} -> @@ -451,8 +465,11 @@ keeping it in cache for #{div(cache_ms, 1000)}s") File.write!(file_path, file_contents) end - updated_full_pack = put_in(full_pack, ["files", shortcode], filename) - update_file_and_send(conn, updated_full_pack, pack_file_p) + full_pack + |> put_in(["files", shortcode], filename) + |> update_pack_file(pack_file_p) + + json(conn, %{shortcode => filename}) else {:has_shortcode, _} -> conn @@ -472,10 +489,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s") "action" => "remove", "shortcode" => shortcode }) do - pack_dir = Path.join(emoji_dir_path(), pack_name) - pack_file_p = Path.join(pack_dir, "pack.json") - - full_pack = Jason.decode!(File.read!(pack_file_p)) + {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) if Map.has_key?(full_pack["files"], shortcode) do {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode]) @@ -494,7 +508,8 @@ keeping it in cache for #{div(cache_ms, 1000)}s") end end - update_file_and_send(conn, updated_full_pack, pack_file_p) + update_pack_file(updated_full_pack, pack_file_p) + json(conn, %{shortcode => full_pack["files"][shortcode]}) else conn |> put_status(:bad_request) @@ -507,10 +522,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s") conn, %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params ) do - pack_dir = Path.join(emoji_dir_path(), pack_name) - pack_file_p = Path.join(pack_dir, "pack.json") - - full_pack = Jason.decode!(File.read!(pack_file_p)) + {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)}, %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params, @@ -522,9 +534,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s") new_emoji_file_path = Path.join(pack_dir, new_filename) # If the name contains directories, create them - if String.contains?(new_emoji_file_path, "/") do - File.mkdir_p!(Path.dirname(new_emoji_file_path)) - end + create_subdirs(new_emoji_file_path) # Move/Rename the old filename to a new filename # These are probably on the same filesystem, so just rename should work @@ -540,8 +550,11 @@ keeping it in cache for #{div(cache_ms, 1000)}s") end # Then, put in the new shortcode with the new path - updated_full_pack = put_in(updated_full_pack, ["files", new_shortcode], new_filename) - update_file_and_send(conn, updated_full_pack, pack_file_p) + updated_full_pack + |> put_in(["files", new_shortcode], new_filename) + |> update_pack_file(pack_file_p) + + json(conn, %{new_shortcode => new_filename}) else {:has_shortcode, _} -> conn -- cgit v1.2.3 From 342f55fb92c723acf7f53de2dae390b72051c94b Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 28 Mar 2020 13:34:32 +0300 Subject: refactor emoji api with fixes --- lib/pleroma/emoji/pack.ex | 509 +++++++++++++++++ .../controllers/emoji_api_controller.ex | 612 ++++++--------------- lib/pleroma/web/router.ex | 3 +- 3 files changed, 677 insertions(+), 447 deletions(-) create mode 100644 lib/pleroma/emoji/pack.ex (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex new file mode 100644 index 000000000..21ed12c78 --- /dev/null +++ b/lib/pleroma/emoji/pack.ex @@ -0,0 +1,509 @@ +defmodule Pleroma.Emoji.Pack do + @derive {Jason.Encoder, only: [:files, :pack]} + defstruct files: %{}, + pack_file: nil, + path: nil, + pack: %{}, + name: nil + + @type t() :: %__MODULE__{ + files: %{String.t() => Path.t()}, + pack_file: Path.t(), + path: Path.t(), + pack: map(), + name: String.t() + } + + alias Pleroma.Emoji + + @spec emoji_path() :: Path.t() + def emoji_path do + static = Pleroma.Config.get!([:instance, :static_dir]) + Path.join(static, "emoji") + end + + @spec create(String.t()) :: :ok | {:error, File.posix()} | {:error, :empty_values} + def create(name) when byte_size(name) > 0 do + dir = Path.join(emoji_path(), name) + + with :ok <- File.mkdir(dir) do + %__MODULE__{ + pack_file: Path.join(dir, "pack.json") + } + |> save_pack() + end + end + + def create(_), do: {:error, :empty_values} + + @spec show(String.t()) :: {:ok, t()} | {:loaded, nil} | {:error, :empty_values} + def show(name) when byte_size(name) > 0 do + with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, + {_, pack} <- validate_pack(pack) do + {:ok, pack} + end + end + + def show(_), do: {:error, :empty_values} + + @spec delete(String.t()) :: + {:ok, [binary()]} | {:error, File.posix(), binary()} | {:error, :empty_values} + def delete(name) when byte_size(name) > 0 do + emoji_path() + |> Path.join(name) + |> File.rm_rf() + end + + def delete(_), do: {:error, :empty_values} + + @spec add_file(String.t(), String.t(), Path.t(), Plug.Upload.t() | String.t()) :: + {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} + def add_file(name, shortcode, filename, file) + when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(filename) > 0 do + with {_, nil} <- {:exists, Emoji.get(shortcode)}, + {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)} do + file_path = Path.join(pack.path, filename) + + create_subdirs(file_path) + + case file do + %Plug.Upload{path: upload_path} -> + # Copy the uploaded file from the temporary directory + File.copy!(upload_path, file_path) + + url when is_binary(url) -> + # Download and write the file + file_contents = Tesla.get!(url).body + File.write!(file_path, file_contents) + end + + files = Map.put(pack.files, shortcode, filename) + + updated_pack = %{pack | files: files} + + case save_pack(updated_pack) do + :ok -> + Emoji.reload() + {:ok, updated_pack} + + e -> + e + end + end + end + + def add_file(_, _, _, _), do: {:error, :empty_values} + + defp create_subdirs(file_path) do + if String.contains?(file_path, "/") do + file_path + |> Path.dirname() + |> File.mkdir_p!() + end + end + + @spec remove_file(String.t(), String.t()) :: + {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} + def remove_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do + with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, + {_, {filename, files}} when not is_nil(filename) <- + {:exists, Map.pop(pack.files, shortcode)}, + emoji <- Path.join(pack.path, filename), + {_, true} <- {:exists, File.exists?(emoji)} do + emoji_dir = Path.dirname(emoji) + + File.rm!(emoji) + + if String.contains?(filename, "/") and File.ls!(emoji_dir) == [] do + File.rmdir!(emoji_dir) + end + + updated_pack = %{pack | files: files} + + case save_pack(updated_pack) do + :ok -> + Emoji.reload() + {:ok, updated_pack} + + e -> + e + end + end + end + + def remove_file(_, _), do: {:error, :empty_values} + + @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) :: + {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} + def update_file(name, shortcode, new_shortcode, new_filename, force) + when byte_size(name) > 0 and byte_size(shortcode) > 0 and byte_size(new_shortcode) > 0 and + byte_size(new_filename) > 0 do + with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, + {_, {filename, files}} when not is_nil(filename) <- + {:exists, Map.pop(pack.files, shortcode)}, + {_, true} <- {:not_used, force or is_nil(Emoji.get(new_shortcode))} do + old_path = Path.join(pack.path, filename) + old_dir = Path.dirname(old_path) + new_path = Path.join(pack.path, new_filename) + + create_subdirs(new_path) + + :ok = File.rename(old_path, new_path) + + if String.contains?(filename, "/") and File.ls!(old_dir) == [] do + File.rmdir!(old_dir) + end + + files = Map.put(files, new_shortcode, new_filename) + + updated_pack = %{pack | files: files} + + case save_pack(updated_pack) do + :ok -> + Emoji.reload() + {:ok, updated_pack} + + e -> + e + end + end + end + + def update_file(_, _, _, _, _), do: {:error, :empty_values} + + @spec import_from_filesystem() :: {:ok, [String.t()]} | {:error, atom()} + def import_from_filesystem do + emoji_path = emoji_path() + + with {:ok, %{access: :read_write}} <- File.stat(emoji_path), + {:ok, results} <- File.ls(emoji_path) do + names = + results + |> Enum.map(&Path.join(emoji_path, &1)) + |> Enum.reject(fn path -> + File.dir?(path) and File.exists?(Path.join(path, "pack.json")) + end) + |> Enum.map(&write_pack_contents/1) + |> Enum.filter(& &1) + + {:ok, names} + else + {:ok, %{access: _}} -> {:error, :not_writable} + e -> e + end + end + + defp write_pack_contents(path) do + pack = %__MODULE__{ + files: files_from_path(path), + path: path, + pack_file: Path.join(path, "pack.json") + } + + case save_pack(pack) do + :ok -> Path.basename(path) + _ -> nil + end + end + + defp files_from_path(path) do + txt_path = Path.join(path, "emoji.txt") + + if File.exists?(txt_path) do + # There's an emoji.txt file, it's likely from a pack installed by the pack manager. + # Make a pack.json file from the contents of that emoji.txt file + + # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2 + + # Create a map of shortcodes to filenames from emoji.txt + File.read!(txt_path) + |> String.split("\n") + |> Enum.map(&String.trim/1) + |> Enum.map(fn line -> + case String.split(line, ~r/,\s*/) do + # This matches both strings with and without tags + # and we don't care about tags here + [name, file | _] -> + file_dir_name = Path.dirname(file) + + file = + if String.ends_with?(path, file_dir_name) do + Path.basename(file) + else + file + end + + {name, file} + + _ -> + nil + end + end) + |> Enum.filter(& &1) + |> Enum.into(%{}) + else + # If there's no emoji.txt, assume all files + # that are of certain extensions from the config are emojis and import them all + pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions]) + Emoji.Loader.make_shortcode_to_file_map(path, pack_extensions) + end + end + + @spec list_remote_packs(String.t()) :: {:ok, map()} + def list_remote_packs(url) do + uri = + url + |> String.trim() + |> URI.parse() + + with {_, true} <- {:shareable, shareable_packs_available?(uri)} do + packs = + uri + |> URI.merge("/api/pleroma/emoji/packs") + |> to_string() + |> Tesla.get!() + |> Map.get(:body) + |> Jason.decode!() + + {:ok, packs} + end + end + + @spec list_local_packs() :: {:ok, map()} + def list_local_packs do + emoji_path = emoji_path() + + # Create the directory first if it does not exist. This is probably the first request made + # with the API so it should be sufficient + with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_path)}, + {:ls, {:ok, results}} <- {:ls, File.ls(emoji_path)} do + packs = + results + |> Enum.map(&load_pack/1) + |> Enum.filter(& &1) + |> Enum.map(&validate_pack/1) + |> Map.new() + + {:ok, packs} + end + end + + defp validate_pack(pack) do + if downloadable?(pack) do + archive = fetch_archive(pack) + archive_sha = :crypto.hash(:sha256, archive) |> Base.encode16() + + info = + pack.pack + |> Map.put("can-download", true) + |> Map.put("download-sha256", archive_sha) + + {pack.name, Map.put(pack, :pack, info)} + else + info = Map.put(pack.pack, "can-download", false) + {pack.name, Map.put(pack, :pack, info)} + end + end + + defp downloadable?(pack) do + # If the pack is set as shared, check if it can be downloaded + # That means that when asked, the pack can be packed and sent to the remote + # Otherwise, they'd have to download it from external-src + pack.pack["share-files"] && + Enum.all?(pack.files, fn {_, file} -> + File.exists?(Path.join(pack.path, file)) + end) + end + + @spec download(String.t()) :: {:ok, binary()} + def download(name) do + with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)}, + {_, true} <- {:can_download?, downloadable?(pack)} do + {:ok, fetch_archive(pack)} + end + end + + defp fetch_archive(pack) do + hash = :crypto.hash(:md5, File.read!(pack.pack_file)) + + case Cachex.get!(:emoji_packs_cache, pack.name) do + %{hash: ^hash, pack_data: archive} -> + archive + + _ -> + create_archive_and_cache(pack, hash) + end + end + + defp create_archive_and_cache(pack, hash) do + files = ['pack.json' | Enum.map(pack.files, fn {_, file} -> to_charlist(file) end)] + + {:ok, {_, result}} = + :zip.zip('#{pack.name}.zip', files, [:memory, cwd: to_charlist(pack.path)]) + + ttl_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) + overall_ttl = :timer.seconds(ttl_per_file * Enum.count(files)) + + Cachex.put!( + :emoji_packs_cache, + pack.name, + # if pack.json MD5 changes, the cache is not valid anymore + %{hash: hash, pack_data: result}, + # Add a minute to cache time for every file in the pack + ttl: overall_ttl + ) + + result + end + + @spec download_from_source(String.t(), String.t(), String.t()) :: :ok + def download_from_source(name, url, as) do + uri = + url + |> String.trim() + |> URI.parse() + + with {_, true} <- {:shareable, shareable_packs_available?(uri)} do + # TODO: why do we load all packs, if we know the name of pack we need + remote_pack = + uri + |> URI.merge("/api/pleroma/emoji/packs/#{name}") + |> to_string() + |> Tesla.get!() + |> Map.get(:body) + |> Jason.decode!() + + result = + case remote_pack["pack"] do + %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> + {:ok, + %{ + sha: sha, + url: + URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/download_shared") |> to_string() + }} + + %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> + {:ok, + %{ + sha: sha, + url: src, + fallback: true + }} + + _ -> + {:error, + "The pack was not set as shared and there is no fallback src to download from"} + end + + with {:ok, %{sha: sha, url: url} = pinfo} <- result, + %{body: archive} <- Tesla.get!(url), + {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, archive)} do + local_name = as || name + + path = Path.join(emoji_path(), local_name) + + pack = %__MODULE__{ + name: local_name, + path: path, + files: remote_pack["files"], + pack_file: Path.join(path, "pack.json") + } + + File.mkdir_p!(pack.path) + + files = Enum.map(remote_pack["files"], fn {_, path} -> to_charlist(path) end) + # Fallback cannot contain a pack.json file + files = if pinfo[:fallback], do: files, else: ['pack.json' | files] + + {:ok, _} = :zip.unzip(archive, cwd: to_charlist(pack.path), file_list: files) + + # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 + # in it to depend on itself + if pinfo[:fallback] do + save_pack(pack) + end + + :ok + end + end + end + + defp save_pack(pack), do: File.write(pack.pack_file, Jason.encode!(pack, pretty: true)) + + @spec save_metadata(map(), t()) :: {:ok, t()} | {:error, File.posix()} + def save_metadata(metadata, %__MODULE__{} = pack) do + pack = Map.put(pack, :pack, metadata) + + with :ok <- save_pack(pack) do + {:ok, pack} + end + end + + @spec update_metadata(String.t(), map()) :: {:ok, t()} | {:error, File.posix()} + def update_metadata(name, data) do + pack = load_pack(name) + + fb_sha_changed? = + not is_nil(data["fallback-src"]) and data["fallback-src"] != pack.pack["fallback-src"] + + with {_, true} <- {:update?, fb_sha_changed?}, + {:ok, %{body: zip}} <- Tesla.get(data["fallback-src"]), + {:ok, f_list} <- :zip.unzip(zip, [:memory]), + {_, true} <- {:has_all_files?, has_all_files?(pack.files, f_list)} do + fallback_sha = :crypto.hash(:sha256, zip) |> Base.encode16() + + data + |> Map.put("fallback-src-sha256", fallback_sha) + |> save_metadata(pack) + else + {:update?, _} -> save_metadata(data, pack) + e -> e + end + end + + # Check if all files from the pack.json are in the archive + defp has_all_files?(files, f_list) do + Enum.all?(files, fn {_, from_manifest} -> + List.keyfind(f_list, to_charlist(from_manifest), 0) + end) + end + + @spec load_pack(String.t()) :: t() | nil + def load_pack(name) do + pack_file = Path.join([emoji_path(), name, "pack.json"]) + + if File.exists?(pack_file) do + pack_file + |> File.read!() + |> from_json() + |> Map.put(:pack_file, pack_file) + |> Map.put(:path, Path.dirname(pack_file)) + |> Map.put(:name, name) + end + end + + defp from_json(json) do + map = Jason.decode!(json) + + struct(__MODULE__, %{files: map["files"], pack: map["pack"]}) + end + + defp shareable_packs_available?(uri) do + uri + |> URI.merge("/.well-known/nodeinfo") + |> to_string() + |> Tesla.get!() + |> Map.get(:body) + |> Jason.decode!() + |> Map.get("links") + |> List.last() + |> Map.get("href") + # Get the actual nodeinfo address and fetch it + |> Tesla.get!() + |> Map.get(:body) + |> Jason.decode!() + |> get_in(["metadata", "features"]) + |> Enum.member?("shareable_emoji_packs") + end +end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 981bac4fa..9fa857474 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,18 +1,15 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do use Pleroma.Web, :controller - alias Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug - alias Pleroma.Plugs.OAuthScopesPlug - - require Logger + alias Pleroma.Emoji.Pack plug( - OAuthScopesPlug, + Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ :create, :delete, - :save_from, + :download_from, :import_from_fs, :update_file, :update_metadata @@ -21,17 +18,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( :skip_plug, - [OAuthScopesPlug, ExpectPublicOrAuthenticatedCheckPlug] + [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] when action in [:download_shared, :list_packs, :list_from] ) - defp emoji_dir_path do - Path.join( - Pleroma.Config.get!([:instance, :static_dir]), - "emoji" - ) - end - @doc """ Lists packs from the remote instance. @@ -39,17 +29,13 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do be done by the server """ def list_from(conn, %{"instance_address" => address}) do - address = String.trim(address) - - if shareable_packs_available(address) do - list_resp = - "#{address}/api/pleroma/emoji/packs" |> Tesla.get!() |> Map.get(:body) |> Jason.decode!() - - json(conn, list_resp) + with {:ok, packs} <- Pack.list_remote_packs(address) do + json(conn, packs) else - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) + {:shareable, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) end end @@ -60,113 +46,44 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do a map of "pack directory name" to pack.json contents. """ def list_packs(conn, _params) do - # Create the directory first if it does not exist. This is probably the first request made - # with the API so it should be sufficient - with {:create_dir, :ok} <- {:create_dir, File.mkdir_p(emoji_dir_path())}, - {:ls, {:ok, results}} <- {:ls, File.ls(emoji_dir_path())} do - pack_infos = - results - |> Enum.filter(&has_pack_json?/1) - |> Enum.map(&load_pack/1) - # Check if all the files are in place and can be sent - |> Enum.map(&validate_pack/1) - # Transform into a map of pack-name => pack-data - |> Enum.into(%{}) - - json(conn, pack_infos) + emoji_path = + Path.join( + Pleroma.Config.get!([:instance, :static_dir]), + "emoji" + ) + + with {:ok, packs} <- Pack.list_local_packs() do + json(conn, packs) else {:create_dir, {:error, e}} -> conn |> put_status(:internal_server_error) - |> json(%{error: "Failed to create the emoji pack directory at #{emoji_dir_path()}: #{e}"}) + |> json(%{error: "Failed to create the emoji pack directory at #{emoji_path}: #{e}"}) {:ls, {:error, e}} -> conn |> put_status(:internal_server_error) |> json(%{ - error: - "Failed to get the contents of the emoji pack directory at #{emoji_dir_path()}: #{e}" + error: "Failed to get the contents of the emoji pack directory at #{emoji_path}: #{e}" }) end end - defp has_pack_json?(file) do - dir_path = Path.join(emoji_dir_path(), file) - # Filter to only use the pack.json packs - File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json")) - end - - defp load_pack(pack_name) do - pack_path = Path.join(emoji_dir_path(), pack_name) - pack_file = Path.join(pack_path, "pack.json") - - {pack_name, Jason.decode!(File.read!(pack_file))} - end - - defp validate_pack({name, pack}) do - pack_path = Path.join(emoji_dir_path(), name) - - if can_download?(pack, pack_path) do - archive_for_sha = make_archive(name, pack, pack_path) - archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16() + def show(conn, %{"name" => name}) do + name = String.trim(name) - pack = - pack - |> put_in(["pack", "can-download"], true) - |> put_in(["pack", "download-sha256"], archive_sha) - - {name, pack} + with {:ok, pack} <- Pack.show(name) do + json(conn, pack) else - {name, put_in(pack, ["pack", "can-download"], false)} - end - end - - defp can_download?(pack, pack_path) do - # If the pack is set as shared, check if it can be downloaded - # That means that when asked, the pack can be packed and sent to the remote - # Otherwise, they'd have to download it from external-src - pack["pack"]["share-files"] && - Enum.all?(pack["files"], fn {_, path} -> - File.exists?(Path.join(pack_path, path)) - end) - end - - defp create_archive_and_cache(name, pack, pack_dir, md5) do - files = - ['pack.json'] ++ - (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end)) - - {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)]) - - cache_seconds_per_file = Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) - cache_ms = :timer.seconds(cache_seconds_per_file * Enum.count(files)) - - Cachex.put!( - :emoji_packs_cache, - name, - # if pack.json MD5 changes, the cache is not valid anymore - %{pack_json_md5: md5, pack_data: zip_result}, - # Add a minute to cache time for every file in the pack - ttl: cache_ms - ) - - Logger.debug("Created an archive for the '#{name}' emoji pack, \ -keeping it in cache for #{div(cache_ms, 1000)}s") - - zip_result - end - - defp make_archive(name, pack, pack_dir) do - # Having a different pack.json md5 invalidates cache - pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json"))) - - case Cachex.get!(:emoji_packs_cache, name) do - %{pack_file_md5: ^pack_file_md5, pack_data: zip_result} -> - Logger.debug("Using cache for the '#{name}' shared emoji pack") - zip_result + {:loaded, _} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) - _ -> - create_archive_and_cache(name, pack, pack_dir, pack_file_md5) + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) end end @@ -175,21 +92,15 @@ keeping it in cache for #{div(cache_ms, 1000)}s") to download packs that the instance shares. """ def download_shared(conn, %{"name" => name}) do - pack_dir = Path.join(emoji_dir_path(), name) - pack_file = Path.join(pack_dir, "pack.json") - - with {_, true} <- {:exists?, File.exists?(pack_file)}, - pack = Jason.decode!(File.read!(pack_file)), - {_, true} <- {:can_download?, can_download?(pack, pack_dir)} do - zip_result = make_archive(name, pack, pack_dir) - send_download(conn, {:binary, zip_result}, filename: "#{name}.zip") + with {:ok, archive} <- Pack.download(name) do + send_download(conn, {:binary, archive}, filename: "#{name}.zip") else {:can_download?, _} -> conn |> put_status(:forbidden) |> json(%{ - error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\ - was disabled for this pack or some files are missing" + error: + "Pack #{name} cannot be downloaded from this instance, either pack sharing was disabled for this pack or some files are missing" }) {:exists?, _} -> @@ -199,22 +110,6 @@ keeping it in cache for #{div(cache_ms, 1000)}s") end end - defp shareable_packs_available(address) do - "#{address}/.well-known/nodeinfo" - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> Map.get("links") - |> List.last() - |> Map.get("href") - # Get the actual nodeinfo address and fetch it - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> get_in(["metadata", "features"]) - |> Enum.member?("shareable_emoji_packs") - end - @doc """ An admin endpoint to request downloading and storing a pack named `pack_name` from the instance `instance_address`. @@ -222,74 +117,24 @@ keeping it in cache for #{div(cache_ms, 1000)}s") If the requested instance's admin chose to share the pack, it will be downloaded from that instance, otherwise it will be downloaded from the fallback source, if there is one. """ - def save_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do - address = String.trim(address) - - if shareable_packs_available(address) do - full_pack = - "#{address}/api/pleroma/emoji/packs/list" - |> Tesla.get!() - |> Map.get(:body) - |> Jason.decode!() - |> Map.get(name) - - pack_info_res = - case full_pack["pack"] do - %{"share-files" => true, "can-download" => true, "download-sha256" => sha} -> - {:ok, - %{ - sha: sha, - uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}" - }} - - %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> - {:ok, - %{ - sha: sha, - uri: src, - fallback: true - }} - - _ -> - {:error, - "The pack was not set as shared and there is no fallback src to download from"} - end - - with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res, - %{body: emoji_archive} <- Tesla.get!(uri), - {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do - local_name = data["as"] || name - pack_dir = Path.join(emoji_dir_path(), local_name) - File.mkdir_p!(pack_dir) - - files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end) - # Fallback cannot contain a pack.json file - files = if pinfo[:fallback], do: files, else: ['pack.json'] ++ files - - {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files) - - # Fallback can't contain a pack.json file, since that would cause the fallback-src-sha256 - # in it to depend on itself - if pinfo[:fallback] do - pack_file_path = Path.join(pack_dir, "pack.json") - - File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true)) - end - - json(conn, "ok") - else - {:error, e} -> - conn |> put_status(:internal_server_error) |> json(%{error: e}) - - {:checksum, _} -> - conn - |> put_status(:internal_server_error) - |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) - end + def download_from(conn, %{"instance_address" => address, "pack_name" => name} = params) do + with :ok <- Pack.download_from_source(name, address, params["as"]) do + json(conn, "ok") else - conn - |> put_status(:internal_server_error) - |> json(%{error: "The requested instance does not support sharing emoji packs"}) + {:shareable, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "The requested instance does not support sharing emoji packs"}) + + {:checksum, _} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"}) + + {:error, e} -> + conn + |> put_status(:internal_server_error) + |> json(%{error: e}) end end @@ -297,23 +142,27 @@ keeping it in cache for #{div(cache_ms, 1000)}s") Creates an empty pack named `name` which then can be updated via the admin UI. """ def create(conn, %{"name" => name}) do - pack_dir = Path.join(emoji_dir_path(), name) + name = String.trim(name) - if not File.exists?(pack_dir) do - File.mkdir_p!(pack_dir) - - pack_file_p = Path.join(pack_dir, "pack.json") + with :ok <- Pack.create(name) do + json(conn, "ok") + else + {:error, :eexist} -> + conn + |> put_status(:conflict) + |> json(%{error: "A pack named \"#{name}\" already exists"}) - File.write!( - pack_file_p, - Jason.encode!(%{pack: %{}, files: %{}}, pretty: true) - ) + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) - conn |> json("ok") - else - conn - |> put_status(:conflict) - |> json(%{error: "A pack named \"#{name}\" already exists"}) + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while creating pack." + ) end end @@ -321,11 +170,20 @@ keeping it in cache for #{div(cache_ms, 1000)}s") Deletes the pack `name` and all it's files. """ def delete(conn, %{"name" => name}) do - pack_dir = Path.join(emoji_dir_path(), name) + name = String.trim(name) + + with {:ok, deleted} when deleted != [] <- Pack.delete(name) do + json(conn, "ok") + else + {:ok, []} -> + conn + |> put_status(:not_found) + |> json(%{error: "Pack #{name} does not exist"}) - case File.rm_rf(pack_dir) do - {:ok, _} -> - conn |> json("ok") + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name cannot be empty"}) {:error, _, _} -> conn @@ -340,82 +198,23 @@ keeping it in cache for #{div(cache_ms, 1000)}s") `new_data` is the new metadata for the pack, that will replace the old metadata. """ def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do - pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"]) - - full_pack = Jason.decode!(File.read!(pack_file_p)) - - # The new fallback-src is in the new data and it's not the same as it was in the old data - should_update_fb_sha = - not is_nil(new_data["fallback-src"]) and - new_data["fallback-src"] != full_pack["pack"]["fallback-src"] - - with {_, true} <- {:should_update?, should_update_fb_sha}, - %{body: pack_arch} <- Tesla.get!(new_data["fallback-src"]), - {:ok, flist} <- :zip.unzip(pack_arch, [:memory]), - {_, true} <- {:has_all_files?, has_all_files?(full_pack, flist)} do - fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16() - - new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha) - update_metadata_and_send(conn, full_pack, new_data, pack_file_p) + with {:ok, pack} <- Pack.update_metadata(name, new_data) do + json(conn, pack.pack) else - {:should_update?, _} -> - update_metadata_and_send(conn, full_pack, new_data, pack_file_p) - {:has_all_files?, _} -> conn |> put_status(:bad_request) |> json(%{error: "The fallback archive does not have all files specified in pack.json"}) - end - end - - # Check if all files from the pack.json are in the archive - defp has_all_files?(%{"files" => files}, flist) do - Enum.all?(files, fn {_, from_manifest} -> - Enum.find(flist, fn {from_archive, _} -> - to_string(from_archive) == from_manifest - end) - end) - end - - defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do - full_pack = Map.put(full_pack, "pack", new_data) - File.write!(pack_file_p, Jason.encode!(full_pack, pretty: true)) - - # Send new data back with fallback sha filled - json(conn, new_data) - end - - defp get_filename(%Plug.Upload{filename: filename}), do: filename - defp get_filename(url) when is_binary(url), do: Path.basename(url) - - defp empty?(str), do: String.trim(str) == "" - - defp update_pack_file(updated_full_pack, pack_file_p) do - content = Jason.encode!(updated_full_pack, pretty: true) - - File.write!(pack_file_p, content) - end - defp create_subdirs(file_path) do - if String.contains?(file_path, "/") do - file_path - |> Path.dirname() - |> File.mkdir_p!() + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating pack metadata." + ) end end - defp pack_info(pack_name) do - dir = Path.join(emoji_dir_path(), pack_name) - json_path = Path.join(dir, "pack.json") - - json = - json_path - |> File.read!() - |> Jason.decode!() - - {dir, json_path, json} - end - @doc """ Updates a file in a pack. @@ -436,50 +235,33 @@ keeping it in cache for #{div(cache_ms, 1000)}s") conn, %{"pack_name" => pack_name, "action" => "add"} = params ) do - shortcode = - if params["shortcode"] do - params["shortcode"] - else - filename = get_filename(params["file"]) - Path.basename(filename, Path.extname(filename)) - end - - {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) - - with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)}, - filename <- params["filename"] || get_filename(params["file"]), - false <- empty?(shortcode), - false <- empty?(filename), - file_path <- Path.join(pack_dir, filename) do - # If the name contains directories, create them - create_subdirs(file_path) - - case params["file"] do - %Plug.Upload{path: upload_path} -> - # Copy the uploaded file from the temporary directory - File.copy!(upload_path, file_path) - - url when is_binary(url) -> - # Download and write the file - file_contents = Tesla.get!(url).body - File.write!(file_path, file_contents) - end - - full_pack - |> put_in(["files", shortcode], filename) - |> update_pack_file(pack_file_p) - - json(conn, %{shortcode => filename}) + filename = params["filename"] || get_filename(params["file"]) + shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename)) + + with {:ok, pack} <- Pack.add_file(pack_name, shortcode, filename, params["file"]) do + json(conn, pack.files) else - {:has_shortcode, _} -> + {:exists, _} -> conn |> put_status(:conflict) |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"}) - true -> + {:loaded, _} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) + + {:error, :empty_values} -> conn |> put_status(:bad_request) - |> json(%{error: "shortcode or filename cannot be empty"}) + |> json(%{error: "pack name, shortcode or filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while adding file to pack." + ) end end @@ -489,87 +271,74 @@ keeping it in cache for #{div(cache_ms, 1000)}s") "action" => "remove", "shortcode" => shortcode }) do - {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) - - if Map.has_key?(full_pack["files"], shortcode) do - {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode]) - - emoji_file_path = Path.join(pack_dir, emoji_file_path) - - # Delete the emoji file - File.rm!(emoji_file_path) + with {:ok, pack} <- Pack.remove_file(pack_name, shortcode) do + json(conn, pack.files) + else + {:exists, _} -> + conn + |> put_status(:bad_request) + |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - # If the old directory has no more files, remove it - if String.contains?(emoji_file_path, "/") do - dir = Path.dirname(emoji_file_path) + {:loaded, _} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack \"#{pack_name}\" is not found"}) - if Enum.empty?(File.ls!(dir)) do - File.rmdir!(dir) - end - end + {:error, :empty_values} -> + conn + |> put_status(:bad_request) + |> json(%{error: "pack name or shortcode cannot be empty"}) - update_pack_file(updated_full_pack, pack_file_p) - json(conn, %{shortcode => full_pack["files"][shortcode]}) - else - conn - |> put_status(:bad_request) - |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while removing file from pack." + ) end end # Update def update_file( conn, - %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params + %{"pack_name" => name, "action" => "update", "shortcode" => shortcode} = params ) do - {pack_dir, pack_file_p, full_pack} = pack_info(pack_name) - - with {_, true} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)}, - %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params, - false <- empty?(new_shortcode), - false <- empty?(new_filename) do - # First, remove the old shortcode, saving the old path - {old_emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode]) - old_emoji_file_path = Path.join(pack_dir, old_emoji_file_path) - new_emoji_file_path = Path.join(pack_dir, new_filename) - - # If the name contains directories, create them - create_subdirs(new_emoji_file_path) - - # Move/Rename the old filename to a new filename - # These are probably on the same filesystem, so just rename should work - :ok = File.rename(old_emoji_file_path, new_emoji_file_path) - - # If the old directory has no more files, remove it - if String.contains?(old_emoji_file_path, "/") do - dir = Path.dirname(old_emoji_file_path) - - if Enum.empty?(File.ls!(dir)) do - File.rmdir!(dir) - end - end - - # Then, put in the new shortcode with the new path - updated_full_pack - |> put_in(["files", new_shortcode], new_filename) - |> update_pack_file(pack_file_p) - - json(conn, %{new_shortcode => new_filename}) + new_shortcode = params["new_shortcode"] + new_filename = params["new_filename"] + force = params["force"] == true + + with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do + json(conn, pack.files) else - {:has_shortcode, _} -> + {:exists, _} -> conn |> put_status(:bad_request) |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - true -> + {:not_used, _} -> + conn + |> put_status(:conflict) + |> json(%{ + error: + "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" + }) + + {:loaded, _} -> conn |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + |> json(%{error: "pack \"#{name}\" is not found"}) - _ -> + {:error, :empty_values} -> conn |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_file were not specified"}) + |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + + {:error, _} -> + render_error( + conn, + :internal_server_error, + "Unexpected error occurred while updating file in pack." + ) end end @@ -589,23 +358,12 @@ keeping it in cache for #{div(cache_ms, 1000)}s") neither, all the files with specific configured extenstions will be assumed to be emojis and stored in the new `pack.json` file. """ + def import_from_fs(conn, _params) do - emoji_path = emoji_dir_path() - - with {:ok, %{access: :read_write}} <- File.stat(emoji_path), - {:ok, results} <- File.ls(emoji_path) do - imported_pack_names = - results - |> Enum.filter(fn file -> - dir_path = Path.join(emoji_path, file) - # Find the directories that do NOT have pack.json - File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json")) - end) - |> Enum.map(&write_pack_json_contents/1) - - json(conn, imported_pack_names) + with {:ok, names} <- Pack.import_from_filesystem() do + json(conn, names) else - {:ok, %{access: _}} -> + {:error, :not_writable} -> conn |> put_status(:internal_server_error) |> json(%{error: "Error: emoji pack directory must be writable"}) @@ -617,44 +375,6 @@ keeping it in cache for #{div(cache_ms, 1000)}s") end end - defp write_pack_json_contents(dir) do - dir_path = Path.join(emoji_dir_path(), dir) - emoji_txt_path = Path.join(dir_path, "emoji.txt") - - files_for_pack = files_for_pack(emoji_txt_path, dir_path) - pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack}) - - File.write!(Path.join(dir_path, "pack.json"), pack_json_contents) - - dir - end - - defp files_for_pack(emoji_txt_path, dir_path) do - if File.exists?(emoji_txt_path) do - # There's an emoji.txt file, it's likely from a pack installed by the pack manager. - # Make a pack.json file from the contents of that emoji.txt fileh - - # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2 - - # Create a map of shortcodes to filenames from emoji.txt - File.read!(emoji_txt_path) - |> String.split("\n") - |> Enum.map(&String.trim/1) - |> Enum.map(fn line -> - case String.split(line, ~r/,\s*/) do - # This matches both strings with and without tags - # and we don't care about tags here - [name, file | _] -> {name, file} - _ -> nil - end - end) - |> Enum.filter(fn x -> not is_nil(x) end) - |> Enum.into(%{}) - else - # If there's no emoji.txt, assume all files - # that are of certain extensions from the config are emojis and import them all - pack_extensions = Pleroma.Config.get!([:emoji, :pack_extensions]) - Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions) - end - end + defp get_filename(%Plug.Upload{filename: filename}), do: filename + defp get_filename(url) when is_binary(url), do: Path.basename(url) end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index becce3098..0fcb517cf 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -221,12 +221,13 @@ defmodule Pleroma.Web.Router do delete("/:name", EmojiAPIController, :delete) # Note: /download_from downloads and saves to instance, not to requester - post("/download_from", EmojiAPIController, :save_from) + post("/download_from", EmojiAPIController, :download_from) end # Pack info / downloading scope "/packs" do get("/", EmojiAPIController, :list_packs) + get("/:name", EmojiAPIController, :show) get("/:name/download_shared/", EmojiAPIController, :download_shared) get("/list_from", EmojiAPIController, :list_from) -- cgit v1.2.3 From ddb757f7434c7216eec1b6ba4fa8b0b7a54157c1 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 28 Mar 2020 21:15:14 +0300 Subject: emoji api packs changes in routes with docs update --- lib/pleroma/emoji/pack.ex | 26 ++-- .../controllers/emoji_api_controller.ex | 158 ++++++--------------- lib/pleroma/web/router.ex | 23 ++- 3 files changed, 64 insertions(+), 143 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index 21ed12c78..eb50e52fa 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -102,9 +102,9 @@ defmodule Pleroma.Emoji.Pack do end end - @spec remove_file(String.t(), String.t()) :: + @spec delete_file(String.t(), String.t()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} - def remove_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do + def delete_file(name, shortcode) when byte_size(name) > 0 and byte_size(shortcode) > 0 do with {_, %__MODULE__{} = pack} <- {:loaded, load_pack(name)}, {_, {filename, files}} when not is_nil(filename) <- {:exists, Map.pop(pack.files, shortcode)}, @@ -131,7 +131,7 @@ defmodule Pleroma.Emoji.Pack do end end - def remove_file(_, _), do: {:error, :empty_values} + def delete_file(_, _), do: {:error, :empty_values} @spec update_file(String.t(), String.t(), String.t(), String.t(), boolean()) :: {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} @@ -249,8 +249,8 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_remote_packs(String.t()) :: {:ok, map()} - def list_remote_packs(url) do + @spec list_remote(String.t()) :: {:ok, map()} + def list_remote(url) do uri = url |> String.trim() @@ -269,8 +269,8 @@ defmodule Pleroma.Emoji.Pack do end end - @spec list_local_packs() :: {:ok, map()} - def list_local_packs do + @spec list_local() :: {:ok, map()} + def list_local do emoji_path = emoji_path() # Create the directory first if it does not exist. This is probably the first request made @@ -315,8 +315,8 @@ defmodule Pleroma.Emoji.Pack do end) end - @spec download(String.t()) :: {:ok, binary()} - def download(name) do + @spec get_archive(String.t()) :: {:ok, binary()} + def get_archive(name) do with {_, %__MODULE__{} = pack} <- {:exists?, load_pack(name)}, {_, true} <- {:can_download?, downloadable?(pack)} do {:ok, fetch_archive(pack)} @@ -356,15 +356,14 @@ defmodule Pleroma.Emoji.Pack do result end - @spec download_from_source(String.t(), String.t(), String.t()) :: :ok - def download_from_source(name, url, as) do + @spec download(String.t(), String.t(), String.t()) :: :ok + def download(name, url, as) do uri = url |> String.trim() |> URI.parse() with {_, true} <- {:shareable, shareable_packs_available?(uri)} do - # TODO: why do we load all packs, if we know the name of pack we need remote_pack = uri |> URI.merge("/api/pleroma/emoji/packs/#{name}") @@ -379,8 +378,7 @@ defmodule Pleroma.Emoji.Pack do {:ok, %{ sha: sha, - url: - URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/download_shared") |> to_string() + url: URI.merge(uri, "/api/pleroma/emoji/packs/#{name}/archive") |> to_string() }} %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) -> diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 9fa857474..83a7f03e8 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,12 +7,15 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ + :import, + :remote, + :download, :create, + :update, :delete, - :download_from, - :import_from_fs, + :add_file, :update_file, - :update_metadata + :delete_file ] ) @@ -22,14 +25,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do when action in [:download_shared, :list_packs, :list_from] ) - @doc """ - Lists packs from the remote instance. - - Since JS cannot ask remote instances for their packs due to CPS, it has to - be done by the server - """ - def list_from(conn, %{"instance_address" => address}) do - with {:ok, packs} <- Pack.list_remote_packs(address) do + def remote(conn, %{"url" => url}) do + with {:ok, packs} <- Pack.list_remote(url) do json(conn, packs) else {:shareable, _} -> @@ -39,20 +36,14 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - Lists the packs available on the instance as JSON. - - The information is public and does not require authentication. The format is - a map of "pack directory name" to pack.json contents. - """ - def list_packs(conn, _params) do + def list(conn, _params) do emoji_path = Path.join( Pleroma.Config.get!([:instance, :static_dir]), "emoji" ) - with {:ok, packs} <- Pack.list_local_packs() do + with {:ok, packs} <- Pack.list_local() do json(conn, packs) else {:create_dir, {:error, e}} -> @@ -87,12 +78,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - An endpoint for other instances (via admin UI) or users (via browser) - to download packs that the instance shares. - """ - def download_shared(conn, %{"name" => name}) do - with {:ok, archive} <- Pack.download(name) do + def archive(conn, %{"name" => name}) do + with {:ok, archive} <- Pack.get_archive(name) do send_download(conn, {:binary, archive}, filename: "#{name}.zip") else {:can_download?, _} -> @@ -110,15 +97,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - An admin endpoint to request downloading and storing a pack named `pack_name` from the instance - `instance_address`. - - If the requested instance's admin chose to share the pack, it will be downloaded - from that instance, otherwise it will be downloaded from the fallback source, if there is one. - """ - def download_from(conn, %{"instance_address" => address, "pack_name" => name} = params) do - with :ok <- Pack.download_from_source(name, address, params["as"]) do + def download(conn, %{"url" => url, "name" => name} = params) do + with :ok <- Pack.download(name, url, params["as"]) do json(conn, "ok") else {:shareable, _} -> @@ -138,9 +118,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - Creates an empty pack named `name` which then can be updated via the admin UI. - """ def create(conn, %{"name" => name}) do name = String.trim(name) @@ -166,9 +143,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - Deletes the pack `name` and all it's files. - """ def delete(conn, %{"name" => name}) do name = String.trim(name) @@ -192,13 +166,8 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - An endpoint to update `pack_names`'s metadata. - - `new_data` is the new metadata for the pack, that will replace the old metadata. - """ - def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do - with {:ok, pack} <- Pack.update_metadata(name, new_data) do + def update(conn, %{"name" => name, "metadata" => metadata}) do + with {:ok, pack} <- Pack.update_metadata(name, metadata) do json(conn, pack.pack) else {:has_all_files?, _} -> @@ -215,30 +184,11 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - @doc """ - Updates a file in a pack. - - Updating can mean three things: - - - `add` adds an emoji named `shortcode` to the pack `pack_name`, - that means that the emoji file needs to be uploaded with the request - (thus requiring it to be a multipart request) and be named `file`. - There can also be an optional `filename` that will be the new emoji file name - (if it's not there, the name will be taken from the uploaded file). - - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file - (from the current filename to `new_filename`) - - `remove` removes the emoji named `shortcode` and it's associated file - """ - - # Add - def update_file( - conn, - %{"pack_name" => pack_name, "action" => "add"} = params - ) do + def add_file(conn, %{"name" => name} = params) do filename = params["filename"] || get_filename(params["file"]) shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename)) - with {:ok, pack} <- Pack.add_file(pack_name, shortcode, filename, params["file"]) do + with {:ok, pack} <- Pack.add_file(name, shortcode, filename, params["file"]) do json(conn, pack.files) else {:exists, _} -> @@ -249,7 +199,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do {:loaded, _} -> conn |> put_status(:bad_request) - |> json(%{error: "pack \"#{pack_name}\" is not found"}) + |> json(%{error: "pack \"#{name}\" is not found"}) {:error, :empty_values} -> conn @@ -265,13 +215,12 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do end end - # Remove - def update_file(conn, %{ - "pack_name" => pack_name, - "action" => "remove", - "shortcode" => shortcode - }) do - with {:ok, pack} <- Pack.remove_file(pack_name, shortcode) do + def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do + new_shortcode = params["new_shortcode"] + new_filename = params["new_filename"] + force = params["force"] == true + + with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do json(conn, pack.files) else {:exists, _} -> @@ -279,35 +228,35 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do |> put_status(:bad_request) |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) + {:not_used, _} -> + conn + |> put_status(:conflict) + |> json(%{ + error: + "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" + }) + {:loaded, _} -> conn |> put_status(:bad_request) - |> json(%{error: "pack \"#{pack_name}\" is not found"}) + |> json(%{error: "pack \"#{name}\" is not found"}) {:error, :empty_values} -> conn |> put_status(:bad_request) - |> json(%{error: "pack name or shortcode cannot be empty"}) + |> json(%{error: "new_shortcode or new_filename cannot be empty"}) {:error, _} -> render_error( conn, :internal_server_error, - "Unexpected error occurred while removing file from pack." + "Unexpected error occurred while updating file in pack." ) end end - # Update - def update_file( - conn, - %{"pack_name" => name, "action" => "update", "shortcode" => shortcode} = params - ) do - new_shortcode = params["new_shortcode"] - new_filename = params["new_filename"] - force = params["force"] == true - - with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do + def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do + with {:ok, pack} <- Pack.delete_file(name, shortcode) do json(conn, pack.files) else {:exists, _} -> @@ -315,14 +264,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do |> put_status(:bad_request) |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) - {:not_used, _} -> - conn - |> put_status(:conflict) - |> json(%{ - error: - "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" - }) - {:loaded, _} -> conn |> put_status(:bad_request) @@ -331,35 +272,18 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do {:error, :empty_values} -> conn |> put_status(:bad_request) - |> json(%{error: "new_shortcode or new_filename cannot be empty"}) + |> json(%{error: "pack name or shortcode cannot be empty"}) {:error, _} -> render_error( conn, :internal_server_error, - "Unexpected error occurred while updating file in pack." + "Unexpected error occurred while removing file from pack." ) end end - def update_file(conn, %{"action" => action}) do - conn - |> put_status(:bad_request) - |> json(%{error: "Unknown action: #{action}"}) - end - - @doc """ - Imports emoji from the filesystem. - - Importing means checking all the directories in the - `$instance_static/emoji/` for directories which do not have - `pack.json`. If one has an emoji.txt file, that file will be used - to create a `pack.json` file with it's contents. If the directory has - neither, all the files with specific configured extenstions will be - assumed to be emojis and stored in the new `pack.json` file. - """ - - def import_from_fs(conn, _params) do + def import_from_filesystem(conn, _params) do with {:ok, names} <- Pack.import_from_filesystem() do json(conn, names) else diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0fcb517cf..a7e1f2f57 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -214,25 +214,24 @@ defmodule Pleroma.Web.Router do scope "/packs" do pipe_through(:admin_api) - post("/import_from_fs", EmojiAPIController, :import_from_fs) - post("/:pack_name/update_file", EmojiAPIController, :update_file) - post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata) - put("/:name", EmojiAPIController, :create) + get("/import", EmojiAPIController, :import_from_filesystem) + get("/remote", EmojiAPIController, :remote) + post("/download", EmojiAPIController, :download) + + post("/:name", EmojiAPIController, :create) + patch("/:name", EmojiAPIController, :update) delete("/:name", EmojiAPIController, :delete) - # Note: /download_from downloads and saves to instance, not to requester - post("/download_from", EmojiAPIController, :download_from) + post("/:name/files", EmojiAPIController, :add_file) + patch("/:name/files", EmojiAPIController, :update_file) + delete("/:name/files", EmojiAPIController, :delete_file) end # Pack info / downloading scope "/packs" do - get("/", EmojiAPIController, :list_packs) + get("/", EmojiAPIController, :list) get("/:name", EmojiAPIController, :show) - get("/:name/download_shared/", EmojiAPIController, :download_shared) - get("/list_from", EmojiAPIController, :list_from) - - # Deprecated: POST /api/pleroma/emoji/packs/list_from (use GET instead) - post("/list_from", EmojiAPIController, :list_from) + get("/:name/archive", EmojiAPIController, :archive) end end -- cgit v1.2.3 From 36abeedf9fdd5f90c2c03a4366f8d2cc563f9386 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Mon, 30 Mar 2020 09:09:27 +0300 Subject: error rename --- lib/pleroma/emoji/pack.ex | 2 +- lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex index eb50e52fa..242344374 100644 --- a/lib/pleroma/emoji/pack.ex +++ b/lib/pleroma/emoji/pack.ex @@ -188,7 +188,7 @@ defmodule Pleroma.Emoji.Pack do {:ok, names} else - {:ok, %{access: _}} -> {:error, :not_writable} + {:ok, %{access: _}} -> {:error, :no_read_write} e -> e end end diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 83a7f03e8..7af9d38a1 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -287,7 +287,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do with {:ok, names} <- Pack.import_from_filesystem() do json(conn, names) else - {:error, :not_writable} -> + {:error, :no_read_write} -> conn |> put_status(:internal_server_error) |> json(%{error: "Error: emoji pack directory must be writable"}) -- cgit v1.2.3 From 4a487e4d0b744edac0d83306b80e4bd0cdef5358 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Thu, 30 Apr 2020 17:50:57 +0300 Subject: fix for auth check --- lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'lib') diff --git a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex index 7af9d38a1..d276b96a4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -7,7 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do Pleroma.Plugs.OAuthScopesPlug, %{scopes: ["write"], admin: true} when action in [ - :import, + :import_from_filesystem, :remote, :download, :create, @@ -22,7 +22,7 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do plug( :skip_plug, [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] - when action in [:download_shared, :list_packs, :list_from] + when action in [:archive, :show, :list] ) def remote(conn, %{"url" => url}) do -- cgit v1.2.3 From bef34568f0d005baabca266b99ac0f6e620e6899 Mon Sep 17 00:00:00 2001 From: eugenijm Date: Thu, 30 Apr 2020 15:02:35 +0300 Subject: Dismiss the follow request notification on rejection --- lib/pleroma/notification.ex | 10 ++++++++++ lib/pleroma/web/common_api/common_api.ex | 2 ++ 2 files changed, 12 insertions(+) (limited to 'lib') diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index aaa675253..9a109dfab 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -261,6 +261,16 @@ defmodule Pleroma.Notification do |> Repo.delete_all() end + def dismiss(%Pleroma.Activity{} = activity) do + Notification + |> where([n], n.activity_id == ^activity.id) + |> Repo.delete_all() + |> case do + {_, notifications} -> {:ok, notifications} + _ -> {:error, "Cannot dismiss notification"} + end + end + def dismiss(%{id: user_id} = _user, id) do notification = Repo.get(Notification, id) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index d1efe0c36..4112e441a 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.ActivityExpiration alias Pleroma.Conversation.Participation alias Pleroma.FollowingRelationship + alias Pleroma.Notification alias Pleroma.Object alias Pleroma.ThreadMute alias Pleroma.User @@ -61,6 +62,7 @@ defmodule Pleroma.Web.CommonAPI do with %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), {:ok, follow_activity} <- Utils.update_follow_state_for_all(follow_activity, "reject"), {:ok, _relationship} <- FollowingRelationship.update(follower, followed, :follow_reject), + {:ok, _notifications} <- Notification.dismiss(follow_activity), {:ok, _activity} <- ActivityPub.reject(%{ to: [follower.ap_id], -- cgit v1.2.3