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 @@ defmodule Pleroma.Emoji.Loader 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
Pleroma.Config.get!([:instance, :static_dir]), Path.join(
"emoji" Pleroma.Config.get!([:instance, :static_dir]),
) "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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController 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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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,6 +177,22 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end end
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 """ @doc """
An admin endpoint to request downloading a pack named `pack_name` from the instance An admin endpoint to request downloading a pack named `pack_name` from the instance
`instance_address`. `instance_address`.
@ -147,21 +201,9 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
from that instance, otherwise it will be downloaded from the fallback source, if there is one. 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 def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
shareable_packs_available = address = String.trim(address)
"#{address}/.well-known/nodeinfo"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
|> 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")
if shareable_packs_available do 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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
%{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 @@ 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. 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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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 @@ 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. `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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
"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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest 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 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
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"]}})