diff options
Diffstat (limited to 'lib')
| -rw-r--r-- | lib/pleroma/emoji/pack.ex | 507 | ||||
| -rw-r--r-- | lib/pleroma/following_relationship.ex | 67 | ||||
| -rw-r--r-- | lib/pleroma/notification.ex | 61 | ||||
| -rw-r--r-- | lib/pleroma/web/api_spec/operations/account_operation.ex | 1 | ||||
| -rw-r--r-- | lib/pleroma/web/common_api/common_api.ex | 2 | ||||
| -rw-r--r-- | lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex | 693 | ||||
| -rw-r--r-- | lib/pleroma/web/router.ex | 24 | 
7 files changed, 809 insertions, 546 deletions
| diff --git a/lib/pleroma/emoji/pack.ex b/lib/pleroma/emoji/pack.ex new file mode 100644 index 000000000..242344374 --- /dev/null +++ b/lib/pleroma/emoji/pack.ex @@ -0,0 +1,507 @@ +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 delete_file(String.t(), String.t()) :: +          {:ok, t()} | {:error, File.posix()} | {:error, :empty_values} +  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)}, +         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 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} +  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, :no_read_write} +      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(String.t()) :: {:ok, map()} +  def list_remote(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() :: {: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 +    # 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 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)} +    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(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 +      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}/archive") |> 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/following_relationship.ex b/lib/pleroma/following_relationship.ex index 9ccf40495..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]) @@ -82,6 +88,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 == ^:follow_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 @@ -105,12 +134,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 == ^:follow_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 == ^:follow_accept) +      following_query(user)        |> select([r, u], u.follower_address)        |> Repo.all() @@ -171,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 73e19bf97..9a109dfab 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,15 +82,13 @@ 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)      query      |> where([n, a], a.actor not in ^blocked_ap_ids) -    |> where( -      [n, a], -      fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.domain_blocks -    ) +    |> FollowingRelationship.keep_following_or_not_domain_blocked(user)    end    defp exclude_notification_muted(query, _, %{@include_muted_option => true}) do @@ -262,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) @@ -330,10 +339,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) @@ -346,17 +356,14 @@ 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 +    potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only) +      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) -    potential_receivers = -      potential_receiver_ap_ids -      |> Enum.uniq() -      |> User.get_users_from_set(local_only) -      notification_enabled_users =        Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end) @@ -365,6 +372,38 @@ defmodule Pleroma.Notification do    def get_notified_from_activity(_, _local_only), do: {[], []} +  @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: [] + +  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: [] 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, diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 7cb8e47d0..adf15b69f 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], 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..d276b96a4 100644 --- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex @@ -1,195 +1,93 @@  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 [ +           :import_from_filesystem, +           :remote, +           :download,             :create, +           :update,             :delete, -           :save_from, -           :import_from_fs, +           :add_file,             :update_file, -           :update_metadata +           :delete_file           ]    )    plug(      :skip_plug, -    [OAuthScopesPlug, ExpectPublicOrAuthenticatedCheckPlug] -    when action in [:download_shared, :list_packs, :list_from] +    [Pleroma.Plugs.OAuthScopesPlug, Pleroma.Plugs.ExpectPublicOrAuthenticatedCheckPlug] +    when action in [:archive, :show, :list]    ) -  defp emoji_dir_path do -    Path.join( -      Pleroma.Config.get!([:instance, :static_dir]), -      "emoji" -    ) -  end - -  @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 -    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) +  def remote(conn, %{"url" => url}) do +    with {:ok, packs} <- Pack.list_remote(url) 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 -  @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 -    # 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) +  def list(conn, _params) do +    emoji_path = +      Path.join( +        Pleroma.Config.get!([:instance, :static_dir]), +        "emoji" +      ) + +    with {:ok, packs} <- Pack.list_local() 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 -  @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 -    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") +  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?, _} ->          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,133 +97,67 @@ 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`. - -  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(conn, %{"url" => url, "name" => name} = params) do +    with :ok <- Pack.download(name, url, 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 -  @doc """ -  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 -  @doc """ -  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) -    case File.rm_rf(pack_dir) do -      {:ok, _} -> -        conn |> json("ok") +    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"}) + +      {:error, :empty_values} -> +        conn +        |> put_status(:bad_request) +        |> json(%{error: "pack name cannot be empty"})        {:error, _, _} ->          conn @@ -334,265 +166,128 @@ keeping it in cache for #{div(cache_ms, 1000)}s")      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 -    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) +  def update(conn, %{"name" => name, "metadata" => metadata}) do +    with {:ok, pack} <- Pack.update_metadata(name, metadata) 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(%{"filename" => filename}), do: filename - -  defp get_filename(%{"file" => file}) do -    case file do -      %Plug.Upload{filename: filename} -> filename -      url when is_binary(url) -> Path.basename(url) +      {:error, _} -> +        render_error( +          conn, +          :internal_server_error, +          "Unexpected error occurred while updating pack metadata." +        )      end    end -  defp empty?(str), do: String.trim(str) == "" - -  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)) - -    # Return the modified file list -    json(conn, updated_full_pack["files"]) -  end +  def add_file(conn, %{"name" => name} = params) do +    filename = params["filename"] || get_filename(params["file"]) +    shortcode = params["shortcode"] || Path.basename(filename, Path.extname(filename)) -  @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", "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)) - -    with {_, false} <- {:has_shortcode, Map.has_key?(full_pack["files"], shortcode)}, -         filename <- get_filename(params), -         false <- empty?(shortcode), -         false <- empty?(filename) do -      file_path = Path.join(pack_dir, filename) - -      # If the name contains directories, create them -      if String.contains?(file_path, "/") do -        File.mkdir_p!(Path.dirname(file_path)) -      end - -      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 - -      updated_full_pack = put_in(full_pack, ["files", shortcode], filename) -      update_file_and_send(conn, updated_full_pack, pack_file_p) +    with {:ok, pack} <- Pack.add_file(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: "shortcode or filename cannot be empty"}) -    end -  end +        |> json(%{error: "pack \"#{name}\" is not found"}) -  # Remove -  def update_file(conn, %{ -        "pack_name" => pack_name, -        "action" => "remove", -        "shortcode" => shortcode -      }) do -    pack_dir = Path.join(emoji_dir_path(), pack_name) -    pack_file_p = Path.join(pack_dir, "pack.json") +      {:error, :empty_values} -> +        conn +        |> put_status(:bad_request) +        |> json(%{error: "pack name, shortcode or filename cannot be empty"}) -    full_pack = Jason.decode!(File.read!(pack_file_p)) +      {:error, _} -> +        render_error( +          conn, +          :internal_server_error, +          "Unexpected error occurred while adding file to pack." +        ) +    end +  end -    if Map.has_key?(full_pack["files"], shortcode) do -      {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode]) +  def update_file(conn, %{"name" => name, "shortcode" => shortcode} = params) do +    new_shortcode = params["new_shortcode"] +    new_filename = params["new_filename"] +    force = params["force"] == true -      emoji_file_path = Path.join(pack_dir, emoji_file_path) +    with {:ok, pack} <- Pack.update_file(name, shortcode, new_shortcode, new_filename, force) do +      json(conn, pack.files) +    else +      {:exists, _} -> +        conn +        |> put_status(:bad_request) +        |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) -      # Delete the emoji file -      File.rm!(emoji_file_path) +      {:not_used, _} -> +        conn +        |> put_status(:conflict) +        |> json(%{ +          error: +            "New shortcode \"#{new_shortcode}\" is already used. If you want to override emoji use 'force' option" +        }) -      # 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 \"#{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: "new_shortcode or new_filename cannot be empty"}) -      update_file_and_send(conn, updated_full_pack, pack_file_p) -    else -      conn -      |> put_status(:bad_request) -      |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) +      {:error, _} -> +        render_error( +          conn, +          :internal_server_error, +          "Unexpected error occurred while updating file in pack." +        )      end    end -  # Update -  def update_file( -        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)) - -    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 -      if String.contains?(new_emoji_file_path, "/") do -        File.mkdir_p!(Path.dirname(new_emoji_file_path)) -      end - -      # 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(updated_full_pack, ["files", new_shortcode], new_filename) -      update_file_and_send(conn, updated_full_pack, pack_file_p) +  def delete_file(conn, %{"name" => name, "shortcode" => shortcode}) do +    with {:ok, pack} <- Pack.delete_file(name, shortcode) do +      json(conn, pack.files)      else -      {:has_shortcode, _} -> +      {:exists, _} ->          conn          |> put_status(:bad_request)          |> json(%{error: "Emoji \"#{shortcode}\" does not exist"}) -      true -> +      {: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"}) -    end -  end +        |> json(%{error: "pack name or shortcode cannot be empty"}) -  def update_file(conn, %{"action" => action}) do -    conn -    |> put_status(:bad_request) -    |> json(%{error: "Unknown action: #{action}"}) +      {:error, _} -> +        render_error( +          conn, +          :internal_server_error, +          "Unexpected error occurred while removing file from pack." +        ) +    end    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 -    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) +  def import_from_filesystem(conn, _params) do +    with {:ok, names} <- Pack.import_from_filesystem() do +      json(conn, names)      else -      {:ok, %{access: _}} -> +      {:error, :no_read_write} ->          conn          |> put_status(:internal_server_error)          |> json(%{error: "Error: emoji pack directory must be writable"}) @@ -604,44 +299,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..a7e1f2f57 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -214,24 +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, :save_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("/: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("/", EmojiAPIController, :list) +      get("/:name", EmojiAPIController, :show) +      get("/:name/archive", EmojiAPIController, :archive)      end    end | 
