Merge branch 'emoji-packs-create-dir' into 'develop'

When listing emoji packs, be sure to create the directory + add an endpoint to list remote packs since JS can't do that

See merge request pleroma/pleroma!1711
This commit is contained in:
rinpatch 2019-09-25 17:12:22 +00:00
commit 98be68e91b
4 changed files with 103 additions and 38 deletions

View file

@ -99,7 +99,7 @@ defp load_pack(pack_dir, emoji_groups) do
contents["files"] contents["files"]
|> Enum.map(fn {name, rel_file} -> |> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file) filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name} {name, filename, ["pack:#{pack_name}"]}
end) end)
else else
# Load from emoji.txt / all files # Load from emoji.txt / all files

View file

@ -3,12 +3,33 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
require Logger require Logger
@emoji_dir_path Path.join( def emoji_dir_path do
Path.join(
Pleroma.Config.get!([:instance, :static_dir]), Pleroma.Config.get!([:instance, :static_dir]),
"emoji" "emoji"
) )
end
@cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file]) @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)
else
conn
|> put_status(:internal_server_error)
|> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
@doc """ @doc """
Lists the packs available on the instance as JSON. Lists the packs available on the instance as JSON.
@ -17,7 +38,10 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
a map of "pack directory name" to pack.json contents. a map of "pack directory name" to pack.json contents.
""" """
def list_packs(conn, _params) do def list_packs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) 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 = pack_infos =
results results
|> Enum.filter(&has_pack_json?/1) |> Enum.filter(&has_pack_json?/1)
@ -28,24 +52,37 @@ def list_packs(conn, _params) do
|> Enum.into(%{}) |> Enum.into(%{})
json(conn, pack_infos) json(conn, pack_infos)
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}"})
{: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}"
})
end end
end end
defp has_pack_json?(file) do defp has_pack_json?(file) do
dir_path = Path.join(@emoji_dir_path, file) dir_path = Path.join(emoji_dir_path(), file)
# Filter to only use the pack.json packs # Filter to only use the pack.json packs
File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json")) File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end end
defp load_pack(pack_name) do defp load_pack(pack_name) do
pack_path = Path.join(@emoji_dir_path, pack_name) pack_path = Path.join(emoji_dir_path(), pack_name)
pack_file = Path.join(pack_path, "pack.json") pack_file = Path.join(pack_path, "pack.json")
{pack_name, Jason.decode!(File.read!(pack_file))} {pack_name, Jason.decode!(File.read!(pack_file))}
end end
defp validate_pack({name, pack}) do defp validate_pack({name, pack}) do
pack_path = Path.join(@emoji_dir_path, name) pack_path = Path.join(emoji_dir_path(), name)
if can_download?(pack, pack_path) do if can_download?(pack, pack_path) do
archive_for_sha = make_archive(name, pack, pack_path) archive_for_sha = make_archive(name, pack, pack_path)
@ -79,7 +116,8 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)]) {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
cache_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files)) 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!( Cachex.put!(
:emoji_packs_cache, :emoji_packs_cache,
@ -115,7 +153,7 @@ defp make_archive(name, pack, pack_dir) do
to download packs that the instance shares. to download packs that the instance shares.
""" """
def download_shared(conn, %{"name" => name}) do def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
pack_file = Path.join(pack_dir, "pack.json") pack_file = Path.join(pack_dir, "pack.json")
with {_, true} <- {:exists?, File.exists?(pack_file)}, with {_, true} <- {:exists?, File.exists?(pack_file)},
@ -139,19 +177,12 @@ def download_shared(conn, %{"name" => name}) do
end end
end end
@doc """ defp shareable_packs_available(address) do
An admin endpoint to request downloading 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} = data) do
shareable_packs_available =
"#{address}/.well-known/nodeinfo" "#{address}/.well-known/nodeinfo"
|> Tesla.get!() |> Tesla.get!()
|> Map.get(:body) |> Map.get(:body)
|> Jason.decode!() |> Jason.decode!()
|> Map.get("links")
|> List.last() |> List.last()
|> Map.get("href") |> Map.get("href")
# Get the actual nodeinfo address and fetch it # Get the actual nodeinfo address and fetch it
@ -160,8 +191,19 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
|> Jason.decode!() |> Jason.decode!()
|> get_in(["metadata", "features"]) |> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs") |> Enum.member?("shareable_emoji_packs")
end
if shareable_packs_available do @doc """
An admin endpoint to request downloading 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} = data) do
address = String.trim(address)
if shareable_packs_available(address) do
full_pack = full_pack =
"#{address}/api/pleroma/emoji/packs/list" "#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!() |> Tesla.get!()
@ -195,7 +237,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
%{body: emoji_archive} <- Tesla.get!(uri), %{body: emoji_archive} <- Tesla.get!(uri),
{_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do {_, true} <- {:checksum, Base.decode16!(sha) == :crypto.hash(:sha256, emoji_archive)} do
local_name = data["as"] || name local_name = data["as"] || name
pack_dir = Path.join(@emoji_dir_path, local_name) pack_dir = Path.join(emoji_dir_path(), local_name)
File.mkdir_p!(pack_dir) File.mkdir_p!(pack_dir)
files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end) files = Enum.map(full_pack["files"], fn {_, path} -> to_charlist(path) end)
@ -233,7 +275,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
Creates an empty pack named `name` which then can be updated via the admin UI. Creates an empty pack named `name` which then can be updated via the admin UI.
""" """
def create(conn, %{"name" => name}) do def create(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
if not File.exists?(pack_dir) do if not File.exists?(pack_dir) do
File.mkdir_p!(pack_dir) File.mkdir_p!(pack_dir)
@ -257,7 +299,7 @@ def create(conn, %{"name" => name}) do
Deletes the pack `name` and all it's files. Deletes the pack `name` and all it's files.
""" """
def delete(conn, %{"name" => name}) do def delete(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name) pack_dir = Path.join(emoji_dir_path(), name)
case File.rm_rf(pack_dir) do case File.rm_rf(pack_dir) do
{:ok, _} -> {:ok, _} ->
@ -276,7 +318,7 @@ def delete(conn, %{"name" => name}) do
`new_data` is the new metadata for the pack, that will replace the old 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 def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_file_p = Path.join([@emoji_dir_path, name, "pack.json"]) pack_file_p = Path.join([emoji_dir_path(), name, "pack.json"])
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -360,7 +402,7 @@ def update_file(
conn, conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params %{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) do ) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -408,7 +450,7 @@ def update_file(conn, %{
"action" => "remove", "action" => "remove",
"shortcode" => shortcode "shortcode" => shortcode
}) do }) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -443,7 +485,7 @@ def update_file(
conn, conn,
%{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params %{"pack_name" => pack_name, "action" => "update", "shortcode" => shortcode} = params
) do ) do
pack_dir = Path.join(@emoji_dir_path, pack_name) pack_dir = Path.join(emoji_dir_path(), pack_name)
pack_file_p = Path.join(pack_dir, "pack.json") pack_file_p = Path.join(pack_dir, "pack.json")
full_pack = Jason.decode!(File.read!(pack_file_p)) full_pack = Jason.decode!(File.read!(pack_file_p))
@ -513,11 +555,11 @@ def update_file(conn, %{"action" => action}) do
assumed to be emojis and stored in the new `pack.json` file. assumed to be emojis and stored in the new `pack.json` file.
""" """
def import_from_fs(conn, _params) do def import_from_fs(conn, _params) do
with {:ok, results} <- File.ls(@emoji_dir_path) do with {:ok, results} <- File.ls(emoji_dir_path()) do
imported_pack_names = imported_pack_names =
results results
|> Enum.filter(fn file -> |> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file) dir_path = Path.join(emoji_dir_path(), file)
# Find the directories that do NOT have pack.json # Find the directories that do NOT have pack.json
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json")) File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end) end)
@ -533,7 +575,7 @@ def import_from_fs(conn, _params) do
end end
defp write_pack_json_contents(dir) do defp write_pack_json_contents(dir) do
dir_path = Path.join(@emoji_dir_path, dir) dir_path = Path.join(emoji_dir_path(), dir)
emoji_txt_path = Path.join(dir_path, "emoji.txt") emoji_txt_path = Path.join(dir_path, "emoji.txt")
files_for_pack = files_for_pack(emoji_txt_path, dir_path) files_for_pack = files_for_pack(emoji_txt_path, dir_path)

View file

@ -222,6 +222,7 @@ defmodule Pleroma.Web.Router do
put("/:name", EmojiAPIController, :create) put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete) delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from) post("/download_from", EmojiAPIController, :download_from)
post("/list_from", EmojiAPIController, :list_from)
end end
scope "/packs" do scope "/packs" do

View file

@ -33,6 +33,28 @@ test "shared & non-shared pack information in list_packs is ok" do
refute pack["pack"]["can-download"] refute pack["pack"]["can-download"]
end end
test "listing remote packs" do
admin = insert(:user, info: %{is_admin: true})
conn = build_conn() |> assign(:user, admin)
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
mock(fn
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{method: :get, url: "https://example.com/api/pleroma/emoji/packs"} ->
json(resp)
end)
assert conn
|> post(emoji_api_path(conn, :list_from), %{instance_address: "https://example.com"})
|> json_response(200) == resp
end
test "downloading a shared pack from download_shared" do test "downloading a shared pack from download_shared" do
conn = build_conn() conn = build_conn()
@ -55,13 +77,13 @@ test "downloading shared & unshared packs from another instance via download_fro
mock(fn mock(fn
%{method: :get, url: "https://old-instance/.well-known/nodeinfo"} -> %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
json([%{href: "https://old-instance/nodeinfo/2.1.json"}]) json(%{links: [%{href: "https://old-instance/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} -> %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: []}}) json(%{metadata: %{features: []}})
%{method: :get, url: "https://example.com/.well-known/nodeinfo"} -> %{method: :get, url: "https://example.com/.well-known/nodeinfo"} ->
json([%{href: "https://example.com/nodeinfo/2.1.json"}]) json(%{links: [%{href: "https://example.com/nodeinfo/2.1.json"}]})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} -> %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}}) json(%{metadata: %{features: ["shareable_emoji_packs"]}})